From 88a66af9f334456b4c7f9a1407ab1c45cd98132d Mon Sep 17 00:00:00 2001 From: Florian Date: Fri, 6 May 2016 13:55:59 +0000 Subject: Update github.md --- doc/integration/github.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/integration/github.md b/doc/integration/github.md index e7497e475c9..0c2e1756a50 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -19,7 +19,7 @@ GitHub will generate an application ID and secret key for you to use. - Application name: This can be anything. Consider something like "\'s GitLab" or "\'s GitLab" or something else descriptive. - Homepage URL: The URL to your GitLab installation. 'https://gitlab.company.com' - Application description: Fill this in if you wish. - - Default authorization callback URL is '${YOUR_DOMAIN}/import/github/callback' + - Default authorization callback URL is '${YOUR_DOMAIN}' 1. Select "Register application". 1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). @@ -48,13 +48,14 @@ GitHub will generate an application ID and secret key for you to use. For omnibus package: + For GitHub.com: + ```ruby gitlab_rails['omniauth_providers'] = [ { "name" => "github", "app_id" => "YOUR_APP_ID", "app_secret" => "YOUR_APP_SECRET", - "url" => "https://github.com/", "args" => { "scope" => "user:email" } } ] @@ -86,7 +87,7 @@ GitHub will generate an application ID and secret key for you to use. 1. Change 'YOUR_APP_SECRET' to the client secret from the GitHub application page from step 7. -1. Save the configuration file. +1. Save the configuration file and run `sudo gitlab-ctl reconfigure`. 1. Restart GitLab for the changes to take effect. -- cgit v1.2.1 From f7605c08dd25ef5b8abb24b49c0c97bab5b6033c Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 22 Jun 2016 16:39:25 +0800 Subject: Add some clarification for some files under config/* I didn't add a lot of them because I am not very familiar about how they work and I am not sure if we really need to explain files like config/database.yml because if you know Rails you must know how that works. At any rate, I think this is a beginning and we could keep documenting more files in the future. Closes #18373 --- config/README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 config/README.md diff --git a/config/README.md b/config/README.md new file mode 100644 index 00000000000..db49bcb775f --- /dev/null +++ b/config/README.md @@ -0,0 +1,21 @@ +# Configuration files Documentation + +Note that most configuration files (`config/*.*`) committed into +[gitlab-ce](https://gitlab.com/gitlab-org/gitlab-ce) **would not be used** for +[omnibus-gitlab](https://gitlab.com/gitlab-org/omnibus-gitlab). Configuration +files committed into gitlab-ce are only used for development. + +## gitlab.yml + +You could find most of GitLab configuration here. + +## mail_room.yml + +It's intended to be an ERB file because `mail_room` would use ERB to evaluate +it before parsing it as a YAML file. It would try to read values from +`gitlab.yml` so you should configure it there. + +## resque.yml + +It's called `resque.yml` for historical reason, and we're not using rescue +at the moment. It's served as a **Redis configuration file** instead. -- cgit v1.2.1 From deb39a017685c303cbaad2e9a07c8fb9b1636e67 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 23 Jun 2016 11:45:06 +0800 Subject: Update wordings by feedback from: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4851 --- config/README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/config/README.md b/config/README.md index db49bcb775f..0a5ea2424e0 100644 --- a/config/README.md +++ b/config/README.md @@ -1,21 +1,22 @@ # Configuration files Documentation Note that most configuration files (`config/*.*`) committed into -[gitlab-ce](https://gitlab.com/gitlab-org/gitlab-ce) **would not be used** for +[gitlab-ce](https://gitlab.com/gitlab-org/gitlab-ce) **will not be used** for [omnibus-gitlab](https://gitlab.com/gitlab-org/omnibus-gitlab). Configuration files committed into gitlab-ce are only used for development. ## gitlab.yml -You could find most of GitLab configuration here. +You can find most of GitLab configuration settings here. ## mail_room.yml -It's intended to be an ERB file because `mail_room` would use ERB to evaluate -it before parsing it as a YAML file. It would try to read values from -`gitlab.yml` so you should configure it there. +This file is actually an YML wrapped inside an ERB file to enable templated +values to be specified from `gitlab.yml`. mail_room loads this file first as +an ERB file and then loads the resulting YML as its configuration. ## resque.yml -It's called `resque.yml` for historical reason, and we're not using rescue -at the moment. It's served as a **Redis configuration file** instead. +This file is called `resque.yml` for historical reasons. We are **NOT** +using Resque at the moment. It is used to specify Redis configuration +values instead. -- cgit v1.2.1 From 94e0ca004959196d0c8481f8f8eb28084ae021ae Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 21 Jul 2016 12:42:37 +0100 Subject: Allow GFM autocomplete to be trigger without the preceding space Closes #19975 --- app/assets/javascripts/gfm_auto_complete.js.coffee | 6 ++++++ spec/features/issues/gfm_autocomplete_spec.rb | 24 ++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 spec/features/issues/gfm_autocomplete_spec.rb diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee index 4a851d9c9fb..31c6a3b4d5e 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.coffee +++ b/app/assets/javascripts/gfm_auto_complete.js.coffee @@ -84,6 +84,7 @@ GitLab.GfmAutoComplete = @Loading.template insertTpl: ':${name}:' data: ['loading'] + startWithSpace: false callbacks: sorter: @DefaultOptions.sorter filter: @DefaultOptions.filter @@ -100,6 +101,7 @@ GitLab.GfmAutoComplete = insertTpl: '${atwho-at}${username}' searchKey: 'search' data: ['loading'] + startWithSpace: false callbacks: sorter: @DefaultOptions.sorter filter: @DefaultOptions.filter @@ -126,6 +128,7 @@ GitLab.GfmAutoComplete = @Loading.template data: ['loading'] insertTpl: '${atwho-at}${id}' + startWithSpace: false callbacks: sorter: @DefaultOptions.sorter filter: @DefaultOptions.filter @@ -149,6 +152,7 @@ GitLab.GfmAutoComplete = @Loading.template insertTpl: '${atwho-at}"${title}"' data: ['loading'] + startWithSpace: false callbacks: beforeSave: (milestones) -> $.map milestones, (m) -> @@ -169,6 +173,7 @@ GitLab.GfmAutoComplete = @Loading.template data: ['loading'] insertTpl: '${atwho-at}${id}' + startWithSpace: false callbacks: sorter: @DefaultOptions.sorter filter: @DefaultOptions.filter @@ -187,6 +192,7 @@ GitLab.GfmAutoComplete = searchKey: 'search' displayTpl: @Labels.template insertTpl: '${atwho-at}${title}' + startWithSpace: false callbacks: beforeSave: (merges) -> sanitizeLabelTitle = (title)-> diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb new file mode 100644 index 00000000000..000b190de77 --- /dev/null +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +feature 'GFM autocomplete', feature: true, js: true do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:issue) { create(:issue, project: project) } + + before do + project.team << [user, :master] + login_as(user) + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it 'opens autocomplete menu when doesnt starts with space' do + sleep 2 + + page.within '.timeline-content-form' do + find('#note_note').native.send_keys('testing') + find('#note_note').native.send_keys('@') + end + + expect(page).to have_selector('.atwho-view') + end +end -- cgit v1.2.1 From 45fa7fd4ddf35314602168cd869ee4a67c44250b Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 21 Jul 2016 14:41:27 +0100 Subject: Correctly checks for character before GFM input char It must not be letter or number to work --- app/assets/javascripts/gfm_auto_complete.js.coffee | 17 +++++++++++++++ spec/features/issues/gfm_autocomplete_spec.rb | 24 +++++++++++++++++++--- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee index 31c6a3b4d5e..8ef4641cf81 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.coffee +++ b/app/assets/javascripts/gfm_auto_complete.js.coffee @@ -43,6 +43,17 @@ GitLab.GfmAutoComplete = @at else value + matcher: (flag, subtext, should_startWithSpace) -> + # escape RegExp + flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&") + + # À + _a = decodeURI("%C3%80") + # ÿ + _y = decodeURI("%C3%BF") + regexp = new RegExp "(?:\\B|\\W|\\s)#{flag}([A-Za-z#{_a}-#{_y}0-9_\'\.\+\-]*)|([^\\x00-\\xff]*)$", 'gi' + match = regexp.exec subtext + if match then match[2] || match[1] else null # Add GFM auto-completion to all input fields, that accept GFM input. setup: (wrap) -> @@ -89,6 +100,7 @@ GitLab.GfmAutoComplete = sorter: @DefaultOptions.sorter filter: @DefaultOptions.filter beforeInsert: @DefaultOptions.beforeInsert + matcher: @DefaultOptions.matcher # Team Members @input.atwho @@ -106,6 +118,7 @@ GitLab.GfmAutoComplete = sorter: @DefaultOptions.sorter filter: @DefaultOptions.filter beforeInsert: @DefaultOptions.beforeInsert + matcher: @DefaultOptions.matcher beforeSave: (members) -> $.map members, (m) -> return m if not m.username? @@ -133,6 +146,7 @@ GitLab.GfmAutoComplete = sorter: @DefaultOptions.sorter filter: @DefaultOptions.filter beforeInsert: @DefaultOptions.beforeInsert + matcher: @DefaultOptions.matcher beforeSave: (issues) -> $.map issues, (i) -> return i if not i.title? @@ -154,6 +168,7 @@ GitLab.GfmAutoComplete = data: ['loading'] startWithSpace: false callbacks: + matcher: @DefaultOptions.matcher beforeSave: (milestones) -> $.map milestones, (m) -> return m if not m.title? @@ -178,6 +193,7 @@ GitLab.GfmAutoComplete = sorter: @DefaultOptions.sorter filter: @DefaultOptions.filter beforeInsert: @DefaultOptions.beforeInsert + matcher: @DefaultOptions.matcher beforeSave: (merges) -> $.map merges, (m) -> return m if not m.title? @@ -194,6 +210,7 @@ GitLab.GfmAutoComplete = insertTpl: '${atwho-at}${title}' startWithSpace: false callbacks: + matcher: @DefaultOptions.matcher beforeSave: (merges) -> sanitizeLabelTitle = (title)-> if /[\w\?&]+\s+[\w\?&]+/g.test(title) diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 000b190de77..7e602672d30 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -9,16 +9,34 @@ feature 'GFM autocomplete', feature: true, js: true do project.team << [user, :master] login_as(user) visit namespace_project_issue_path(project.namespace, project, issue) - end - it 'opens autocomplete menu when doesnt starts with space' do sleep 2 + end + it 'opens autocomplete menu when field starts with text' do page.within '.timeline-content-form' do - find('#note_note').native.send_keys('testing') + find('#note_note').native.send_keys('') find('#note_note').native.send_keys('@') end expect(page).to have_selector('.atwho-view') end + + it 'opens autocomplete menu when field is prefixed with non-text character' do + page.within '.timeline-content-form' do + find('#note_note').native.send_keys('') + find('#note_note').native.send_keys('@') + end + + expect(page).to have_selector('.atwho-view') + end + + it 'doesnt open autocomplete menu character is prefixed with text' do + page.within '.timeline-content-form' do + find('#note_note').native.send_keys('testing') + find('#note_note').native.send_keys('@') + end + + expect(page).not_to have_selector('.atwho-view') + end end -- cgit v1.2.1 From c2d9ab5e9079eafb848890ebc49457ab6c51170f Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Fri, 22 Jul 2016 03:49:27 +0000 Subject: Add omnibus rake task. Fix source env --- doc/install/database_mysql.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md index e8093f0b257..322680f0cf4 100644 --- a/doc/install/database_mysql.md +++ b/doc/install/database_mysql.md @@ -57,8 +57,15 @@ We do not recommend using MySQL due to various issues. For example, case [(in)se After installation or upgrade, remember to run the `add_limits_mysql` Rake task: +**Omnibus GitLab installations** ``` -bundle exec rake add_limits_mysql +sudo gitlab-rake add_limits_mysql +``` + +**Installations from source** + +``` +bundle exec rake add_limits_mysql RAILS_ENV=production ``` The `text` type in MySQL has a different size limit than the `text` type in -- cgit v1.2.1 From cfde451d36aa1c518654532c3e2cfffe5dfc2d98 Mon Sep 17 00:00:00 2001 From: Florian Date: Fri, 6 May 2016 13:55:59 +0000 Subject: Update github.md --- doc/integration/github.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/doc/integration/github.md b/doc/integration/github.md index 8a01afd1177..479c697b933 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -48,6 +48,21 @@ GitHub will generate an application ID and secret key for you to use. For omnibus package: + For GitHub.com: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + "name" => "github", + "app_id" => "YOUR_APP_ID", + "app_secret" => "YOUR_APP_SECRET", + "args" => { "scope" => "user:email" } + } + ] + ``` + + For GitHub Enterprise: + ```ruby gitlab_rails['omniauth_providers'] = [ { @@ -86,7 +101,7 @@ GitHub will generate an application ID and secret key for you to use. 1. Change 'YOUR_APP_SECRET' to the client secret from the GitHub application page from step 7. -1. Save the configuration file. +1. Save the configuration file and run `sudo gitlab-ctl reconfigure`. 1. Restart GitLab for the changes to take effect. -- cgit v1.2.1 From 31704680cc967f0a053edc70db828ff780d7d0e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Wed, 2 Nov 2016 00:32:20 +0200 Subject: post_receive: accept any user email from last commit --- changelogs/unreleased/post_receive-any-email.yml | 4 ++++ lib/gitlab/identifier.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/post_receive-any-email.yml diff --git a/changelogs/unreleased/post_receive-any-email.yml b/changelogs/unreleased/post_receive-any-email.yml new file mode 100644 index 00000000000..3710b1b4b46 --- /dev/null +++ b/changelogs/unreleased/post_receive-any-email.yml @@ -0,0 +1,4 @@ +--- +title: "post_receive: accept any user email from last commit" +merge_request: 7225 +author: Elan Ruusamäe diff --git a/lib/gitlab/identifier.rb b/lib/gitlab/identifier.rb index f8809db21aa..c5acf18beb5 100644 --- a/lib/gitlab/identifier.rb +++ b/lib/gitlab/identifier.rb @@ -24,7 +24,7 @@ module Gitlab email = commit.author_email identify_with_cache(:email, email) do - User.find_by(email: email) + User.find_by_any_email(email) end end -- cgit v1.2.1 From 2ad630fb51da9eacfb8716a4615e250efd44e0a7 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 21 Sep 2016 11:34:16 -0500 Subject: Fix deselecting calendar days on contribution graph --- app/assets/javascripts/users/calendar.js | 1 + .../unreleased/disable-calendar-deselection.yml | 4 +++ spec/features/calendar_spec.rb | 41 ++++++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 changelogs/unreleased/disable-calendar-deselection.yml diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js index 0ec878e7e60..7701f32772c 100644 --- a/app/assets/javascripts/users/calendar.js +++ b/app/assets/javascripts/users/calendar.js @@ -206,6 +206,7 @@ } }); } else { + this.currentSelectedDate = ''; return $('.user-calendar-activities').html(''); } }; diff --git a/changelogs/unreleased/disable-calendar-deselection.yml b/changelogs/unreleased/disable-calendar-deselection.yml new file mode 100644 index 00000000000..060797bba34 --- /dev/null +++ b/changelogs/unreleased/disable-calendar-deselection.yml @@ -0,0 +1,4 @@ +--- +title: Fix deselecting calendar days on contribution graph +merge_request: 6453 +author: ClemMakesApps diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb index 7fa0c95cae2..3e0b6364e0d 100644 --- a/spec/features/calendar_spec.rb +++ b/spec/features/calendar_spec.rb @@ -52,6 +52,10 @@ feature 'Contributions Calendar', js: true, feature: true do Event.create(push_params) end + def get_first_cell_content + find('.user-calendar-activities').text + end + before do login_as :user visit @user.username @@ -62,6 +66,43 @@ feature 'Contributions Calendar', js: true, feature: true do expect(page).to have_css('.js-contrib-calendar') end + describe 'select calendar day', js: true do + let(:cells) { page.all('.user-contrib-cell') } + let(:first_cell_content_before) { get_first_cell_content } + + before do + cells[0].click + wait_for_ajax + first_cell_content_before + end + + it 'displays calendar day activities', js: true do + expect(get_first_cell_content).not_to eq('') + end + + describe 'select another calendar day', js: true do + before do + cells[1].click + wait_for_ajax + end + + it 'displays different calendar day activities', js: true do + expect(get_first_cell_content).not_to eq(first_cell_content_before) + end + end + + describe 'deselect calendar day', js: true do + before do + cells[0].click + wait_for_ajax + end + + it 'hides calendar day activities', js: true do + expect(get_first_cell_content).to eq('') + end + end + end + describe '1 calendar activity' do before do Issues::CreateService.new(contributed_project, @user, issue_params).execute -- cgit v1.2.1 From b9176afbc887b2bac4e07fdad65f3560cc3ceedf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Sun, 6 Nov 2016 20:27:58 +0200 Subject: update existing test --- spec/lib/gitlab/identifier_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/gitlab/identifier_spec.rb b/spec/lib/gitlab/identifier_spec.rb index 47d6f1007d1..f42c4453dd1 100644 --- a/spec/lib/gitlab/identifier_spec.rb +++ b/spec/lib/gitlab/identifier_spec.rb @@ -65,7 +65,7 @@ describe Gitlab::Identifier do commit = double(:commit, author_email: user.email) expect(project).to receive(:commit).with('123').twice.and_return(commit) - expect(User).to receive(:find_by).once.and_call_original + expect(User).to receive(:find_by_any_email).once.and_call_original 2.times do expect(identifier.identify_using_commit(project, '123')).to eq(user) -- cgit v1.2.1 From 18a71c47603d703de73c46fef4889887f685bebe Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 7 Nov 2016 23:44:11 +0800 Subject: Show commit status from latest pipeline Rather than compound status from all pipelines. Closes #20560 --- app/models/ci/pipeline.rb | 6 ++++- app/models/commit.rb | 16 +++++++------ .../show-commit-status-from-latest-pipeline.yml | 4 ++++ spec/models/commit_spec.rb | 27 +++++++++------------- 4 files changed, 29 insertions(+), 24 deletions(-) create mode 100644 changelogs/unreleased/show-commit-status-from-latest-pipeline.yml diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index d3432632899..3ab19938c0f 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -83,9 +83,13 @@ module Ci end end + scope :latest, -> { order(id: :desc) } + # ref can't be HEAD or SHA, can only be branch/tag name + scope :latest_for, ->(ref) { where(ref: ref).latest } + def self.latest_successful_for(ref) - where(ref: ref).order(id: :desc).success.first + latest_for(ref).success.first end def self.truncate_sha(sha) diff --git a/app/models/commit.rb b/app/models/commit.rb index 9e7fde9503d..2134ba2d75f 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -232,13 +232,15 @@ class Commit def status(ref = nil) @statuses ||= {} - if @statuses.key?(ref) - @statuses[ref] - elsif ref - @statuses[ref] = pipelines.where(ref: ref).status - else - @statuses[ref] = pipelines.status - end + return @statuses[ref] if @statuses.key?(ref) + + latest_pipeline = if ref + pipelines.latest_for(ref) + else + pipelines.latest + end.first + + @statuses[ref] = latest_pipeline.try(:status) end def revert_branch_name diff --git a/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml b/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml new file mode 100644 index 00000000000..bbd7a217493 --- /dev/null +++ b/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml @@ -0,0 +1,4 @@ +--- +title: Show commit status from latest pipeline +merge_request: 7333 +author: diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index e3bb3482d67..ca277601970 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -206,23 +206,18 @@ eos end describe '#status' do - context 'without arguments for compound status' do - shared_examples 'giving the status from pipeline' do - it do - expect(commit.status).to eq(Ci::Pipeline.status) - end - end - - context 'with pipelines' do - let!(:pipeline) do - create(:ci_empty_pipeline, project: project, sha: commit.sha) + context 'without arguments' do + before do + 5.times do + create(:ci_empty_pipeline, + project: project, + sha: commit.sha, + status: Ci::Pipeline.all_state_names.sample) end - - it_behaves_like 'giving the status from pipeline' end - context 'without pipelines' do - it_behaves_like 'giving the status from pipeline' + it 'gives the status from latest pipeline' do + expect(commit.status).to eq(Ci::Pipeline.latest.first.status) end end @@ -248,8 +243,8 @@ eos expect(commit.status('fix')).to eq(pipeline_from_fix.status) end - it 'gives compound status if ref is nil' do - expect(commit.status(nil)).to eq(commit.status) + it 'gives status from latest pipeline for whatever branch' do + expect(commit.status(nil)).to eq(Ci::Pipeline.latest.first.status) end end end -- cgit v1.2.1 From 590d61a2de453b2359c81f7070caae09df6e788d Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 8 Nov 2016 02:33:04 +0800 Subject: Also show latest pipeline for ImageForBuildService --- app/services/ci/image_for_build_service.rb | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb index 75d847d5bee..17ce95073cb 100644 --- a/app/services/ci/image_for_build_service.rb +++ b/app/services/ci/image_for_build_service.rb @@ -1,11 +1,18 @@ module Ci class ImageForBuildService def execute(project, opts) - sha = opts[:sha] || ref_sha(project, opts[:ref]) + ref = opts[:ref] + sha = opts[:sha] || ref_sha(project, ref) pipelines = project.pipelines.where(sha: sha) - pipelines = pipelines.where(ref: opts[:ref]) if opts[:ref] - image_name = image_for_status(pipelines.status) + + latest_pipeline = if ref + pipelines.latest_for(ref) + else + pipelines.latest + end.first + + image_name = image_for_status(latest_pipeline.status) image_path = Rails.root.join('public/ci', image_name) OpenStruct.new(path: image_path, name: image_name) -- cgit v1.2.1 From f593abbc70ab02823cd99d2db11598b629cbb3a0 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 8 Nov 2016 03:44:25 +0800 Subject: There's not always a pipeline --- app/services/ci/image_for_build_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb index 17ce95073cb..026a727a8f9 100644 --- a/app/services/ci/image_for_build_service.rb +++ b/app/services/ci/image_for_build_service.rb @@ -12,7 +12,7 @@ module Ci pipelines.latest end.first - image_name = image_for_status(latest_pipeline.status) + image_name = image_for_status(latest_pipeline.try(:status)) image_path = Rails.root.join('public/ci', image_name) OpenStruct.new(path: image_path, name: image_name) -- cgit v1.2.1 From 03a235783f697572fe201332cb82746401a01daf Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 14 Nov 2016 10:34:10 +0000 Subject: Tests fix --- spec/features/issues/gfm_autocomplete_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index c0d093fb155..c421da97d76 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -20,7 +20,7 @@ feature 'GFM autocomplete', feature: true, js: true do find('#note_note').native.send_keys('@') end - expect(page).to have_selector('.atwho-view') + expect(page).to have_selector('.atwho-container') end it 'opens autocomplete menu when field is prefixed with non-text character' do @@ -29,7 +29,7 @@ feature 'GFM autocomplete', feature: true, js: true do find('#note_note').native.send_keys('@') end - expect(page).to have_selector('.atwho-view') + expect(page).to have_selector('.atwho-container') end it 'doesnt open autocomplete menu character is prefixed with text' do -- cgit v1.2.1 From d97ad73c82e5299fba893cc9b273eced8ec37d3e Mon Sep 17 00:00:00 2001 From: Ryan O'Boyle Date: Wed, 9 Nov 2016 03:35:07 -0500 Subject: Fix typos in API doc examples --- doc/api/build_triggers.md | 8 ++++---- doc/api/build_variables.md | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/doc/api/build_triggers.md b/doc/api/build_triggers.md index 1b7a1840138..b6459971420 100644 --- a/doc/api/build_triggers.md +++ b/doc/api/build_triggers.md @@ -15,7 +15,7 @@ GET /projects/:id/triggers | `id` | integer | yes | The ID of a project | ``` -curl --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers" ``` ```json @@ -51,7 +51,7 @@ GET /projects/:id/triggers/:token | `token` | string | yes | The `token` of a trigger | ``` -curl --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" ``` ```json @@ -77,7 +77,7 @@ POST /projects/:id/triggers | `id` | integer | yes | The ID of a project | ``` -curl --request POST --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers" ``` ```json @@ -104,7 +104,7 @@ DELETE /projects/:id/triggers/:token | `token` | string | yes | The `token` of a trigger | ``` -curl --request DELETE --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" ``` ```json diff --git a/doc/api/build_variables.md b/doc/api/build_variables.md index a21751a49ea..917e9773913 100644 --- a/doc/api/build_variables.md +++ b/doc/api/build_variables.md @@ -13,7 +13,7 @@ GET /projects/:id/variables | `id` | integer | yes | The ID of a project | ``` -curl --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" ``` ```json @@ -43,7 +43,7 @@ GET /projects/:id/variables/:key | `key` | string | yes | The `key` of a variable | ``` -curl --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/TEST_VARIABLE_1" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/TEST_VARIABLE_1" ``` ```json @@ -68,7 +68,7 @@ POST /projects/:id/variables | `value` | string | yes | The `value` of a variable | ``` -curl --request POST --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" --form "key=NEW_VARIABLE" --form "value=new value" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" --form "key=NEW_VARIABLE" --form "value=new value" ``` ```json @@ -93,7 +93,7 @@ PUT /projects/:id/variables/:key | `value` | string | yes | The `value` of a variable | ``` -curl --request PUT --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/NEW_VARIABLE" --form "value=updated value" +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/NEW_VARIABLE" --form "value=updated value" ``` ```json @@ -117,7 +117,7 @@ DELETE /projects/:id/variables/:key | `key` | string | yes | The `key` of a variable | ``` -curl --request DELETE --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1" +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1" ``` ```json -- cgit v1.2.1 From 6d1c5761cd520f3cb7fa4dbb1a1937c29f468884 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 17 Nov 2016 00:57:50 +0800 Subject: Improve how we could cancel pipelines: * Introduce `HasStatus.cancelable` which we might be able to cancel * Cancel and check upon `cancelable` * Also cancel on `CommitStatus` rather than just `Ci::Build` Fixes #23635 Fixes #17845 --- app/models/ci/pipeline.rb | 4 +-- app/models/concerns/has_status.rb | 4 +++ changelogs/unreleased/fix-cancelling-pipelines.yml | 4 +++ spec/features/projects/pipelines_spec.rb | 4 +-- spec/models/ci/pipeline_spec.rb | 40 ++++++++++++++++++++++ 5 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/fix-cancelling-pipelines.yml diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 3fee6c18770..4eb85f62ee4 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -167,11 +167,11 @@ module Ci end def cancelable? - builds.running_or_pending.any? + statuses.cancelable.any? end def cancel_running - builds.running_or_pending.each(&:cancel) + statuses.cancelable.each(&:cancel) end def retry_failed(user) diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index ef3e73a4072..1332743429d 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -73,6 +73,10 @@ module HasStatus scope :skipped, -> { where(status: 'skipped') } scope :running_or_pending, -> { where(status: [:running, :pending]) } scope :finished, -> { where(status: [:success, :failed, :canceled]) } + + scope :cancelable, -> do + where(status: [:running, :pending, :created]) + end end def started? diff --git a/changelogs/unreleased/fix-cancelling-pipelines.yml b/changelogs/unreleased/fix-cancelling-pipelines.yml new file mode 100644 index 00000000000..c21e663093a --- /dev/null +++ b/changelogs/unreleased/fix-cancelling-pipelines.yml @@ -0,0 +1,4 @@ +--- +title: Fix cancelling created or external pipelines +merge_request: 7508 +author: diff --git a/spec/features/projects/pipelines_spec.rb b/spec/features/projects/pipelines_spec.rb index db56a50e058..b3ef9a11d56 100644 --- a/spec/features/projects/pipelines_spec.rb +++ b/spec/features/projects/pipelines_spec.rb @@ -90,8 +90,8 @@ describe "Pipelines" do visit namespace_project_pipelines_path(project.namespace, project) end - it 'is not cancelable' do - expect(page).not_to have_link('Cancel') + it 'is cancelable' do + expect(page).to have_link('Cancel') end it 'has pipeline running' do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 71b7628ef10..d013dc7b31a 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -402,6 +402,46 @@ describe Ci::Pipeline, models: true do end end + describe '#cancelable?' do + subject { pipeline.cancelable? } + + %i[created running pending].each do |status| + context "when there is a build #{status}" do + before do + create(:ci_build, status, pipeline: pipeline) + end + + it { is_expected.to be_truthy } + end + + context "when there is an external job #{status}" do + before do + create(:generic_commit_status, status, pipeline: pipeline) + end + + it { is_expected.to be_truthy } + end + end + + %i[success failed canceled].each do |status| + context "when there is a build #{status}" do + before do + create(:ci_build, status, pipeline: pipeline) + end + + it { is_expected.to be_falsey } + end + + context "when there is an external job #{status}" do + before do + create(:generic_commit_status, status, pipeline: pipeline) + end + + it { is_expected.to be_falsey } + end + end + end + describe '#execute_hooks' do let!(:build_a) { create_build('a', 0) } let!(:build_b) { create_build('b', 1) } -- cgit v1.2.1 From 8dd9a8b6e00cbd91f8455218397c8da716fc9b00 Mon Sep 17 00:00:00 2001 From: Oren Kanner Date: Thu, 17 Nov 2016 23:21:02 -0500 Subject: Allow admins to stop impersonating users without e-mail addresses Resolves #24576 Modify the guard clause of the `ApplicationController#require_email` before action to skip requests where an admin is impersonating the current user. --- app/controllers/application_controller.rb | 2 +- .../unreleased/24576_cant_stop_impersonating.yml | 4 ++++ .../admin/impersonations_controller_spec.rb | 28 ++++++++++++++++------ 3 files changed, 26 insertions(+), 8 deletions(-) create mode 100644 changelogs/unreleased/24576_cant_stop_impersonating.yml diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 517ad4f03f3..2713c0f1dc8 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -224,7 +224,7 @@ class ApplicationController < ActionController::Base end def require_email - if current_user && current_user.temp_oauth_email? + if current_user && current_user.temp_oauth_email? && session[:impersonator_id].nil? redirect_to profile_path, notice: 'Please complete your profile with email address' and return end end diff --git a/changelogs/unreleased/24576_cant_stop_impersonating.yml b/changelogs/unreleased/24576_cant_stop_impersonating.yml new file mode 100644 index 00000000000..8fa6eeca756 --- /dev/null +++ b/changelogs/unreleased/24576_cant_stop_impersonating.yml @@ -0,0 +1,4 @@ +--- +title: Allow admins to stop impersonating users without e-mail addresses +merge_request: 7550 +author: Oren Kanner diff --git a/spec/controllers/admin/impersonations_controller_spec.rb b/spec/controllers/admin/impersonations_controller_spec.rb index 8be662974a0..8f1f0ba89ff 100644 --- a/spec/controllers/admin/impersonations_controller_spec.rb +++ b/spec/controllers/admin/impersonations_controller_spec.rb @@ -76,18 +76,32 @@ describe Admin::ImpersonationsController do end context "when the impersonator is not blocked" do - it "redirects to the impersonated user's page" do - expect(Gitlab::AppLogger).to receive(:info).with("User #{impersonator.username} has stopped impersonating #{user.username}").and_call_original + shared_examples_for "successfully stops impersonating" do + it "redirects to the impersonated user's page" do + expect(Gitlab::AppLogger).to receive(:info).with("User #{impersonator.username} has stopped impersonating #{user.username}").and_call_original - delete :destroy + delete :destroy + + expect(response).to redirect_to(admin_user_path(user)) + end + + it "signs us in as the impersonator" do + delete :destroy - expect(response).to redirect_to(admin_user_path(user)) + expect(warden.user).to eq(impersonator) + end end - it "signs us in as the impersonator" do - delete :destroy + # base case + it_behaves_like "successfully stops impersonating" + + context "and the user has a temporary oauth e-mail address" do + before do + allow(user).to receive(:temp_oauth_email?).and_return(true) + allow(controller).to receive(:current_user).and_return(user) + end - expect(warden.user).to eq(impersonator) + it_behaves_like "successfully stops impersonating" end end end -- cgit v1.2.1 From eef360912320ecfcb427684edf35672b643a87c0 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 18 Nov 2016 14:52:39 +0200 Subject: Add shortcuts for adding users to a project team with a specific role This also updates _some_ specs to use these new methods, just to serve as an example for others going forward, but by no means is this exhaustive. Original implementations at !5992 and !6012. Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/20944 --- app/models/project.rb | 1 + app/models/project_team.rb | 16 ++++++ changelogs/unreleased/rs-project-team-helpers.yml | 4 ++ spec/controllers/autocomplete_controller_spec.rb | 30 ++++++------ spec/models/project_spec.rb | 7 +++ spec/models/project_team_spec.rb | 34 ++++++------- spec/services/notification_service_spec.rb | 60 +++++++++++------------ 7 files changed, 90 insertions(+), 62 deletions(-) create mode 100644 changelogs/unreleased/rs-project-team-helpers.yml diff --git a/app/models/project.rb b/app/models/project.rb index f9bcc547c36..facaddb3aa4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -158,6 +158,7 @@ class Project < ActiveRecord::Base delegate :name, to: :owner, allow_nil: true, prefix: true delegate :members, to: :team, prefix: true delegate :add_user, to: :team + delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team # Validations validates :creator, presence: true, on: :create diff --git a/app/models/project_team.rb b/app/models/project_team.rb index a6e911df9bd..65549d8cd08 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -21,6 +21,22 @@ class ProjectTeam end end + def add_guest(user, current_user: nil) + self << [user, :guest, current_user] + end + + def add_reporter(user, current_user: nil) + self << [user, :reporter, current_user] + end + + def add_developer(user, current_user: nil) + self << [user, :developer, current_user] + end + + def add_master(user, current_user: nil) + self << [user, :master, current_user] + end + def find_member(user_id) member = project.members.find_by(user_id: user_id) diff --git a/changelogs/unreleased/rs-project-team-helpers.yml b/changelogs/unreleased/rs-project-team-helpers.yml new file mode 100644 index 00000000000..79abcbce1e3 --- /dev/null +++ b/changelogs/unreleased/rs-project-team-helpers.yml @@ -0,0 +1,4 @@ +--- +title: Add shortcuts for adding users to a project team with a specific role +merge_request: +author: Nikolay Ponomarev and Dino M diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index a121cb2fc97..d9a86346c81 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -11,7 +11,7 @@ describe AutocompleteController do context 'project members' do before do sign_in(user) - project.team << [user, :master] + project.add_master(user) end describe 'GET #users with project ID' do @@ -69,7 +69,7 @@ describe AutocompleteController do before do sign_in(non_member) - project.team << [user, :master] + project.add_master(user) end let(:body) { JSON.parse(response.body) } @@ -103,7 +103,7 @@ describe AutocompleteController do describe 'GET #users with public project' do before do - public_project.team << [user, :guest] + public_project.add_guest(user) get(:users, project_id: public_project.id) end @@ -129,7 +129,7 @@ describe AutocompleteController do describe 'GET #users with inaccessible group' do before do - project.team << [user, :guest] + project.add_guest(user) get(:users, group_id: user.namespace.id) end @@ -186,12 +186,12 @@ describe AutocompleteController do before do sign_in(user) - project.team << [user, :master] + project.add_master(user) end context 'authorized projects' do before do - authorized_project.team << [user, :master] + authorized_project.add_master(user) end describe 'GET #projects with project ID' do @@ -216,8 +216,8 @@ describe AutocompleteController do context 'authorized projects and search' do before do - authorized_project.team << [user, :master] - authorized_search_project.team << [user, :master] + authorized_project.add_master(user) + authorized_search_project.add_master(user) end describe 'GET #projects with project ID and search' do @@ -242,9 +242,9 @@ describe AutocompleteController do authorized_project2 = create(:project) authorized_project3 = create(:project) - authorized_project.team << [user, :master] - authorized_project2.team << [user, :master] - authorized_project3.team << [user, :master] + authorized_project.add_master(user) + authorized_project2.add_master(user) + authorized_project3.add_master(user) stub_const 'MoveToProjectFinder::PAGE_SIZE', 2 end @@ -268,9 +268,9 @@ describe AutocompleteController do authorized_project2 = create(:project) authorized_project3 = create(:project) - authorized_project.team << [user, :master] - authorized_project2.team << [user, :master] - authorized_project3.team << [user, :master] + authorized_project.add_master(user) + authorized_project2.add_master(user) + authorized_project3.add_master(user) end describe 'GET #projects with project ID and offset_id' do @@ -289,7 +289,7 @@ describe AutocompleteController do context 'authorized projects without admin_issue ability' do before(:each) do - authorized_project.team << [user, :guest] + authorized_project.add_guest(user) expect(user.can?(:admin_issue, authorized_project)).to eq(false) end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 46fa00a79c4..2fae3dde58c 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -241,6 +241,13 @@ describe Project, models: true do it { is_expected.to respond_to(:path_with_namespace) } end + describe 'delegation' do + it { is_expected.to delegate_method(:add_guest).to(:team) } + it { is_expected.to delegate_method(:add_reporter).to(:team) } + it { is_expected.to delegate_method(:add_developer).to(:team) } + it { is_expected.to delegate_method(:add_master).to(:team) } + end + describe '#name_with_namespace' do let(:project) { build_stubbed(:empty_project) } diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 12228425579..eb6b009c7cf 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -10,9 +10,9 @@ describe ProjectTeam, models: true do let(:project) { create(:empty_project) } before do - project.team << [master, :master] - project.team << [reporter, :reporter] - project.team << [guest, :guest] + project.add_master(master) + project.add_reporter(reporter) + project.add_guest(guest) end describe 'members collection' do @@ -47,8 +47,8 @@ describe ProjectTeam, models: true do # If user is a group and a project member - GitLab uses highest permission # So we add group guest as master and add group master as guest # to this project to test highest access - project.team << [guest, :master] - project.team << [master, :guest] + project.add_master(guest) + project.add_guest(master) end describe 'members collection' do @@ -79,14 +79,14 @@ describe ProjectTeam, models: true do it 'returns project members' do user = create(:user) - project.team << [user, :guest] + project.add_guest(user) expect(project.team.members).to contain_exactly(user) end it 'returns project members of a specified level' do user = create(:user) - project.team << [user, :reporter] + project.add_reporter(user) expect(project.team.guests).to be_empty expect(project.team.reporters).to contain_exactly(user) @@ -141,9 +141,9 @@ describe ProjectTeam, models: true do let(:requester) { create(:user) } before do - project.team << [master, :master] - project.team << [reporter, :reporter] - project.team << [guest, :guest] + project.add_master(master) + project.add_reporter(reporter) + project.add_guest(guest) project.request_access(requester) end @@ -204,9 +204,9 @@ describe ProjectTeam, models: true do context 'when project is not shared with group' do before do - project.team << [master, :master] - project.team << [reporter, :reporter] - project.team << [guest, :guest] + project.add_master(master) + project.add_reporter(reporter) + project.add_guest(guest) project.request_access(requester) end @@ -281,10 +281,10 @@ describe ProjectTeam, models: true do guest = create(:user) project = create(:project) - project.team << [master, :master] - project.team << [reporter, :reporter] - project.team << [promoted_guest, :guest] - project.team << [guest, :guest] + project.add_master(master) + project.add_reporter(reporter) + project.add_guest(promoted_guest) + project.add_guest(guest) group = create(:group) group_developer = create(:user) diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 8726c9eaa55..08ae61708a5 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -64,9 +64,9 @@ describe NotificationService, services: true do before do build_team(note.project) - project.team << [issue.author, :master] - project.team << [issue.assignee, :master] - project.team << [note.author, :master] + project.add_master(issue.author) + project.add_master(issue.assignee) + project.add_master(note.author) create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@subscribed_participant cc this guy') update_custom_notification(:new_note, @u_guest_custom, project) update_custom_notification(:new_note, @u_custom_global) @@ -168,8 +168,8 @@ describe NotificationService, services: true do let(:guest_watcher) { create_user_with_notification(:watch, "guest-watcher-confidential") } it 'filters out users that can not read the issue' do - project.team << [member, :developer] - project.team << [guest, :guest] + project.add_developer(member) + project.add_guest(guest) expect(SentNotification).to receive(:record).with(confidential_issue, any_args).exactly(4).times @@ -195,7 +195,7 @@ describe NotificationService, services: true do before do build_team(note.project) - note.project.team << [note.author, :master] + note.project.add_master(note.author) reset_delivered_emails! end @@ -237,7 +237,7 @@ describe NotificationService, services: true do before do build_team(note.project) - note.project.team << [note.author, :master] + note.project.add_master(note.author) reset_delivered_emails! end @@ -324,8 +324,8 @@ describe NotificationService, services: true do before do build_team(note.project) - project.team << [merge_request.author, :master] - project.team << [merge_request.assignee, :master] + project.add_master(merge_request.author) + project.add_master(merge_request.assignee) end describe '#new_note' do @@ -409,8 +409,8 @@ describe NotificationService, services: true do let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignee: assignee) } it "emails subscribers of the issue's labels that can read the issue" do - project.team << [member, :developer] - project.team << [guest, :guest] + project.add_developer(member) + project.add_guest(guest) label = create(:label, project: project, issues: [confidential_issue]) confidential_issue.reload @@ -621,8 +621,8 @@ describe NotificationService, services: true do let!(:label_2) { create(:label, project: project) } it "emails subscribers of the issue's labels that can read the issue" do - project.team << [member, :developer] - project.team << [guest, :guest] + project.add_developer(member) + project.add_guest(guest) label_2.toggle_subscription(non_member, project) label_2.toggle_subscription(author, project) @@ -1210,7 +1210,7 @@ describe NotificationService, services: true do let(:member) { create(:user) } before(:each) do - project.team << [member, :developer, project.owner] + project.add_developer(member, current_user: project.owner) end it do @@ -1233,9 +1233,9 @@ describe NotificationService, services: true do let(:note) { create(:note, noteable: merge_request, project: private_project) } before do - private_project.team << [assignee, :developer] - private_project.team << [developer, :developer] - private_project.team << [guest, :guest] + private_project.add_developer(assignee) + private_project.add_developer(developer) + private_project.add_guest(guest) ActionMailer::Base.deliveries.clear end @@ -1297,15 +1297,15 @@ describe NotificationService, services: true do @u_guest_watcher = create_user_with_notification(:watch, 'guest_watching') @u_guest_custom = create_user_with_notification(:custom, 'guest_custom') - project.team << [@u_watcher, :master] - project.team << [@u_participating, :master] - project.team << [@u_participant_mentioned, :master] - project.team << [@u_disabled, :master] - project.team << [@u_mentioned, :master] - project.team << [@u_committer, :master] - project.team << [@u_not_mentioned, :master] - project.team << [@u_lazy_participant, :master] - project.team << [@u_custom_global, :master] + project.add_master(@u_watcher) + project.add_master(@u_participating) + project.add_master(@u_participant_mentioned) + project.add_master(@u_disabled) + project.add_master(@u_mentioned) + project.add_master(@u_committer) + project.add_master(@u_not_mentioned) + project.add_master(@u_lazy_participant) + project.add_master(@u_custom_global) end def create_global_setting_for(user, level) @@ -1339,10 +1339,10 @@ describe NotificationService, services: true do @subscribed_participant = create_global_setting_for(create(:user, username: 'subscribed_participant'), :participating) @watcher_and_subscriber = create_global_setting_for(create(:user), :watch) - project.team << [@subscribed_participant, :master] - project.team << [@subscriber, :master] - project.team << [@unsubscriber, :master] - project.team << [@watcher_and_subscriber, :master] + project.add_master(@subscribed_participant) + project.add_master(@subscriber) + project.add_master(@unsubscriber) + project.add_master(@watcher_and_subscriber) issuable.subscriptions.create(user: @subscriber, project: project, subscribed: true) issuable.subscriptions.create(user: @subscribed_participant, project: project, subscribed: true) -- cgit v1.2.1 From 4be28d28ab515c26b01881bdf550778cc1757dc2 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Fri, 18 Nov 2016 07:50:42 -0700 Subject: Unify all MR widget text colors and background colors --- app/assets/stylesheets/pages/merge_requests.scss | 37 +----------------------- 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index b6a82460f25..1525abc3767 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -62,6 +62,7 @@ .ci_widget { border-bottom: 1px solid $well-inner-border; + color: $gl-gray; svg { margin-right: 4px; @@ -70,48 +71,12 @@ overflow: visible; } - &.ci-success { - color: $gl-success; - - a.environment, - a.pipeline { - color: inherit; - } - } - &.ci-success_with_warnings { - color: $gl-success; i { color: $gl-warning; } } - - &.ci-skipped { - background-color: #eee; - color: #888; - } - - &.ci-pending { - color: $gl-warning; - } - - &.ci-running { - color: $blue-normal; - } - - &.ci-failed, - &.ci-error { - color: $gl-danger; - } - - &.ci-canceled { - color: $gl-gray; - } - - a.monospace { - color: inherit; - } } .mr-widget-body, -- cgit v1.2.1 From 9a0201473e24e5036506f0cc8761290da1ca743b Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 18 Nov 2016 23:16:51 +0800 Subject: Add when cancelling for external jobs, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7508#note_18622182 --- spec/features/projects/pipelines_spec.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/spec/features/projects/pipelines_spec.rb b/spec/features/projects/pipelines_spec.rb index b3ef9a11d56..544372108ed 100644 --- a/spec/features/projects/pipelines_spec.rb +++ b/spec/features/projects/pipelines_spec.rb @@ -97,6 +97,13 @@ describe "Pipelines" do it 'has pipeline running' do expect(page).to have_selector('.ci-running') end + + context 'when canceling' do + before { click_link('Cancel') } + + it { expect(page).not_to have_link('Cancel') } + it { expect(page).to have_selector('.ci-canceled') } + end end context 'when failed' do -- cgit v1.2.1 From 6c0f104f67dc34311e8b430282b3171a0fe04f02 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Fri, 18 Nov 2016 08:23:15 -0700 Subject: Remove view details link from MR widget --- app/views/projects/merge_requests/widget/_heading.html.haml | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index a82c846baa7..0f0fc097e98 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -12,7 +12,6 @@ = succeed "." do = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace" %span.ci-coverage - = link_to "View details", pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'pipelines'} - elsif @merge_request.has_ci? - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX @@ -29,8 +28,6 @@ = succeed "." do = link_to commit.short_id, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, commit), class: "monospace" %span.ci-coverage - - if details_path = ci_build_details_path(@merge_request) - = link_to "View details", details_path, :"data-no-turbolink" => "data-no-turbolink" .ci_widget = icon("spinner spin") -- cgit v1.2.1 From 100076ecbbdf3eae361a6356ddfb55b1694e4741 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 18 Nov 2016 23:27:06 +0800 Subject: Add tests against two jobs having different status Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7508#note_18622469 --- spec/models/ci/pipeline_spec.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index d013dc7b31a..2cc6d1be606 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -421,6 +421,18 @@ describe Ci::Pipeline, models: true do it { is_expected.to be_truthy } end + + %i[success failed canceled].each do |status2| + context "when there are two builds for #{status} and #{status2}" do + before do + build = %i[ci_build generic_commit_status] + create(build.sample, status, pipeline: pipeline) + create(build.sample, status2, pipeline: pipeline) + end + + it { is_expected.to be_truthy } + end + end end %i[success failed canceled].each do |status| -- cgit v1.2.1 From 512aa57d367527e7d5d61417981a99685db6e2f0 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Fri, 18 Nov 2016 17:28:02 +0100 Subject: If Build running change accept merge request when build succeeds button from orange to blue --- app/assets/stylesheets/pages/merge_requests.scss | 2 +- .../merge_requests/widget/open/_merge_when_build_succeeds.html.haml | 2 +- .../24266-Afraid to press the Orange button on Merge request screen | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/24266-Afraid to press the Orange button on Merge request screen diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index b6a82460f25..72995f951c7 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -24,7 +24,7 @@ .accept_merge_request { &.ci-pending, &.ci-running { - @include btn-orange; + @include btn-blue; } &.ci-skipped, diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml index 2b6b5e05e86..1aeb12e4661 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -21,5 +21,5 @@ Remove Source Branch When Merged - if user_can_cancel_automatic_merge - = link_to cancel_merge_when_build_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-warning btn-sm" do + = link_to cancel_merge_when_build_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-sm" do Cancel Automatic Merge diff --git a/changelogs/unreleased/24266-Afraid to press the Orange button on Merge request screen b/changelogs/unreleased/24266-Afraid to press the Orange button on Merge request screen new file mode 100644 index 00000000000..28ca20c7dcc --- /dev/null +++ b/changelogs/unreleased/24266-Afraid to press the Orange button on Merge request screen @@ -0,0 +1,4 @@ +--- +title: If Build running change accept merge request when build succeeds button from orange to blue +merge_request: 7577 +author: -- cgit v1.2.1 From b6a7a4783435a7fa34f26dbf3b16ab8e7ed21b88 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Sat, 19 Nov 2016 01:02:49 +0800 Subject: Add a lot of tests for scopes, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7508#note_18622499 --- spec/factories/ci/builds.rb | 4 ++ spec/factories/commit_statuses.rb | 4 ++ spec/models/concerns/has_status_spec.rb | 77 +++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+) diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 0c93bbdfe26..e7fe489e5eb 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -38,6 +38,10 @@ FactoryGirl.define do status 'canceled' end + trait :skipped do + status 'skipped' + end + trait :running do status 'running' end diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb index 995f2080f10..756b341ecba 100644 --- a/spec/factories/commit_statuses.rb +++ b/spec/factories/commit_statuses.rb @@ -19,6 +19,10 @@ FactoryGirl.define do status 'canceled' end + trait :skipped do + status 'skipped' + end + trait :running do status 'running' end diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb index 87bffbdc54e..24cd435256e 100644 --- a/spec/models/concerns/has_status_spec.rb +++ b/spec/models/concerns/has_status_spec.rb @@ -123,4 +123,81 @@ describe HasStatus do it_behaves_like 'build status summary' end end + + def self.random_type + %i[ci_build generic_commit_status].sample + end + + context 'for scope with one status' do + shared_examples 'having a job' do |type, status| + context "when it's #{status} #{type} job" do + let!(:job) { create(type, status) } + + describe ".#{status}" do + subject { CommitStatus.public_send(status).all } + + it { is_expected.to contain_exactly(job) } + end + + describe '.relevant' do + subject { CommitStatus.relevant.all } + + it do + case status + when :created + is_expected.to be_empty + else + is_expected.to contain_exactly(job) + end + end + end + end + end + + %i[created running pending success + failed canceled skipped].each do |status| + it_behaves_like 'having a job', random_type, status + end + end + + context 'for scope with more statuses' do + shared_examples 'having a job' do |type, status, excluded_status| + context "when it's #{status} #{type} job" do + let!(:job) { create(type, status) } + + it do + case status + when excluded_status + is_expected.to be_empty + else + is_expected.to contain_exactly(job) + end + end + end + end + + describe '.running_or_pending' do + subject { CommitStatus.running_or_pending } + + %i[running pending created].each do |status| + it_behaves_like 'having a job', random_type, status, :created + end + end + + describe '.finished' do + subject { CommitStatus.finished } + + %i[success failed canceled created].each do |status| + it_behaves_like 'having a job', random_type, status, :created + end + end + + describe '.cancelable' do + subject { CommitStatus.cancelable } + + %i[running pending created failed].each do |status| + it_behaves_like 'having a job', random_type, status, :failed + end + end + end end -- cgit v1.2.1 From 92a83aaee1c5715bf95715649aa8ced50a70dea8 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Sat, 19 Nov 2016 01:17:40 +0800 Subject: Add a test for Ci::Pipeline#cancel_running: Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7508#note_18622559 I didn't write an exhaustive one because we already have it on HasStatus, from: https://gitlab.com/gitlab-org/gitlab-ce/commit/b6a7a4783435a7fa34f26dbf3b16ab8e7ed21b88 --- spec/models/ci/pipeline_spec.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 2cc6d1be606..74579e0c832 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -454,6 +454,22 @@ describe Ci::Pipeline, models: true do end end + describe '#cancel_running' do + context 'when there is a running external job and created build' do + before do + create(:generic_commit_status, :running, pipeline: pipeline) + create(:ci_build, :created, pipeline: pipeline) + + pipeline.cancel_running + end + + it 'cancels both jobs' do + expect(pipeline.statuses.pluck(:status)). + to contain_exactly('canceled', 'canceled') + end + end + end + describe '#execute_hooks' do let!(:build_a) { create_build('a', 0) } let!(:build_b) { create_build('b', 1) } -- cgit v1.2.1 From 1fe77740388680c188f80ad9262c348f5593ce54 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Fri, 18 Nov 2016 22:44:36 +0000 Subject: Notify broken master on #development AKA: - move all conversations to #core - beating will continue until morale improves - Learn by pain - Pain driven development --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 436e9ec6c60..e48d17647dd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -377,7 +377,7 @@ notify:slack: SETUP_DB: "false" USE_BUNDLE_INSTALL: "false" script: - - ./scripts/notify_slack.sh "#builds" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See " + - ./scripts/notify_slack.sh "#development" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See " when: on_failure only: - master@gitlab-org/gitlab-ce -- cgit v1.2.1 From ca639c9b824d6c8effb620bc71255eb0895ab2cc Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 19 Nov 2016 14:04:11 +0100 Subject: Allow to retry failed or canceled builds and fix cancel running specs failure --- app/models/ci/pipeline.rb | 14 +++++---- app/models/concerns/has_status.rb | 1 + spec/models/ci/pipeline_spec.rb | 63 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 69 insertions(+), 9 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 4eb85f62ee4..c0f2c8ba787 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -161,9 +161,7 @@ module Ci end def retryable? - builds.latest.any? do |build| - (build.failed? || build.canceled?) && build.retryable? - end + builds.latest.failed_or_canceled.any?(&:retryable?) end def cancelable? @@ -171,12 +169,16 @@ module Ci end def cancel_running - statuses.cancelable.each(&:cancel) + Gitlab::OptimisticLocking.retry_lock(statuses.cancelable) do |cancelable| + cancelable.each(&:cancel) + end end def retry_failed(user) - builds.latest.failed.select(&:retryable?).each do |build| - Ci::Build.retry(build, user) + Gitlab::OptimisticLocking.retry_lock(builds.latest.failed_or_canceled) do |failed| + failed.select(&:retryable?).each do |build| + Ci::Build.retry(build, user) + end end end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 1332743429d..2f5aa91a964 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -73,6 +73,7 @@ module HasStatus scope :skipped, -> { where(status: 'skipped') } scope :running_or_pending, -> { where(status: [:running, :pending]) } scope :finished, -> { where(status: [:success, :failed, :canceled]) } + scope :failed_or_canceled, -> { where(status: [:failed, :canceled]) } scope :cancelable, -> do where(status: [:running, :pending, :created]) diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 74579e0c832..af619a02ed9 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -455,17 +455,74 @@ describe Ci::Pipeline, models: true do end describe '#cancel_running' do + let(:latest_status) { pipeline.statuses.pluck(:status) } + context 'when there is a running external job and created build' do before do + create(:ci_build, :running, pipeline: pipeline) create(:generic_commit_status, :running, pipeline: pipeline) - create(:ci_build, :created, pipeline: pipeline) pipeline.cancel_running end it 'cancels both jobs' do - expect(pipeline.statuses.pluck(:status)). - to contain_exactly('canceled', 'canceled') + expect(latest_status).to contain_exactly('canceled', 'canceled') + end + end + + context 'when builds are in different stages' do + before do + create(:ci_build, :running, stage_idx: 0, pipeline: pipeline) + create(:ci_build, :running, stage_idx: 1, pipeline: pipeline) + + pipeline.cancel_running + end + + it 'cancels both jobs' do + expect(latest_status).to contain_exactly('canceled', 'canceled') + end + end + end + + describe '#retry_failed' do + let(:latest_status) { pipeline.statuses.latest.pluck(:status) } + + context 'when there is a failed build and failed external status' do + before do + create(:ci_build, :failed, name: 'build', pipeline: pipeline) + create(:generic_commit_status, :failed, name: 'jenkins', pipeline: pipeline) + + pipeline.retry_failed(nil) + end + + it 'retries only build' do + expect(latest_status).to contain_exactly('pending', 'failed') + end + end + + context 'when builds are in different stages' do + before do + create(:ci_build, :failed, name: 'build', stage_idx: 0, pipeline: pipeline) + create(:ci_build, :failed, name: 'jenkins', stage_idx: 1, pipeline: pipeline) + + pipeline.retry_failed(nil) + end + + it 'retries both builds' do + expect(latest_status).to contain_exactly('pending', 'pending') + end + end + + context 'when there are canceled and failed' do + before do + create(:ci_build, :failed, name: 'build', stage_idx: 0, pipeline: pipeline) + create(:ci_build, :canceled, name: 'jenkins', stage_idx: 1, pipeline: pipeline) + + pipeline.retry_failed(nil) + end + + it 'retries both builds' do + expect(latest_status).to contain_exactly('pending', 'pending') end end end -- cgit v1.2.1 From c4ded595ccf520bc30bde90403366ad14ba8b594 Mon Sep 17 00:00:00 2001 From: David Wagner Date: Fri, 18 Nov 2016 12:57:25 +0100 Subject: Fix broken external links in help/index.html An external link was recently added but was broken because 'https://gitlab.com/help/' was prepended to every link in the page. Since no link in the main help readme begins with "help" and since doing so wouldn't make sense, the substitution conditionaly prepending "help" can be simplified and reused. Signed-off-by: David Wagner --- app/controllers/help_controller.rb | 6 +++--- spec/controllers/help_controller_spec.rb | 14 +++----------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index 4b3c71874be..a10cdcce72b 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -6,9 +6,9 @@ class HelpController < ApplicationController def index @help_index = File.read(Rails.root.join('doc', 'README.md')) - # Prefix Markdown links with `help/` unless they already have been - # See http://rubular.com/r/ie2MlpdUMq - @help_index.gsub!(/(\]\()(\/?help\/)?([^\)\(]+\))/, '\1/help/\3') + # Prefix Markdown links with `help/` unless they are external links + # See http://rubular.com/r/MioSrVLK3S + @help_index.gsub!(%r{(\]\()(?!.+://)([^\)\(]+\))}, '\1/help/\2') end def show diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb index 6fc6ea95e13..cffed987f6b 100644 --- a/spec/controllers/help_controller_spec.rb +++ b/spec/controllers/help_controller_spec.rb @@ -16,14 +16,6 @@ describe HelpController do end end - context 'when url prefixed with help/' do - it 'will be an absolute path' do - stub_readme("[API](help/api/README.md)") - get :index - expect(assigns[:help_index]).to eq '[API](/help/api/README.md)' - end - end - context 'when url prefixed with help' do it 'will be an absolute path' do stub_readme("[API](helpful_hints/README.md)") @@ -32,11 +24,11 @@ describe HelpController do end end - context 'when url prefixed with /help/' do + context 'when url is an external link' do it 'will not be changed' do - stub_readme("[API](/help/api/README.md)") + stub_readme("[external](https://some.external.link)") get :index - expect(assigns[:help_index]).to eq '[API](/help/api/README.md)' + expect(assigns[:help_index]).to eq '[external](https://some.external.link)' end end end -- cgit v1.2.1 From f75127cfe870e259008f9a3bbd8c7af8bf455fc2 Mon Sep 17 00:00:00 2001 From: Nur Rony Date: Mon, 21 Nov 2016 13:25:46 +0600 Subject: showing unconfirmed email status in profile --- app/assets/javascripts/profile/profile.js.es6 | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6 index 73858388261..3eb81808bd6 100644 --- a/app/assets/javascripts/profile/profile.js.es6 +++ b/app/assets/javascripts/profile/profile.js.es6 @@ -35,7 +35,6 @@ } onSubmitForm(e) { - e.preventDefault(); return this.saveForm(); } -- cgit v1.2.1 From 08d2e03058ffd9696ff76780e5aa10a23b07a09e Mon Sep 17 00:00:00 2001 From: Nur Rony Date: Mon, 21 Nov 2016 13:50:44 +0600 Subject: changelog entry file added --- changelogs/unreleased/24413-show-unconfirmed-email-status.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/24413-show-unconfirmed-email-status.yml diff --git a/changelogs/unreleased/24413-show-unconfirmed-email-status.yml b/changelogs/unreleased/24413-show-unconfirmed-email-status.yml new file mode 100644 index 00000000000..972eaed95e0 --- /dev/null +++ b/changelogs/unreleased/24413-show-unconfirmed-email-status.yml @@ -0,0 +1,4 @@ +--- +title: Shows unconfirmed email status in profile +merge_request: 7611 +author: -- cgit v1.2.1 From f50fdacae4fa1b6313ec9c36541749678267a082 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Tue, 15 Nov 2016 22:57:28 +0100 Subject: Upgrade grape-entity Fixes gitlab-org/gitlab-ce#14329 --- Gemfile | 2 +- Gemfile.lock | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 9e815925a1f..14ea7ec7ea0 100644 --- a/Gemfile +++ b/Gemfile @@ -68,7 +68,7 @@ gem 'github-linguist', '~> 4.7.0', require: 'linguist' # API gem 'grape', '~> 0.15.0' -gem 'grape-entity', '~> 0.4.2' +gem 'grape-entity', '~> 0.5.2' gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' # Pagination diff --git a/Gemfile.lock b/Gemfile.lock index bdc60552480..331ffd89148 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -316,8 +316,7 @@ GEM rack-accept rack-mount virtus (>= 1.0.0) - grape-entity (0.4.8) - activesupport + grape-entity (0.5.2) multi_json (>= 1.3.2) haml (4.0.7) tilt @@ -869,7 +868,7 @@ DEPENDENCIES gollum-rugged_adapter (~> 0.4.2) gon (~> 6.1.0) grape (~> 0.15.0) - grape-entity (~> 0.4.2) + grape-entity (~> 0.5.2) haml_lint (~> 0.18.2) hamlit (~> 2.6.1) health_check (~> 2.2.0) -- cgit v1.2.1 From 2282a3bd67d32555517d150c1e471d714e51d410 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Tue, 15 Nov 2016 23:37:22 +0100 Subject: Add changelog entry for grape-entity upgrade [ci skip] --- changelogs/unreleased/zj-upgrade-grape.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/zj-upgrade-grape.yml diff --git a/changelogs/unreleased/zj-upgrade-grape.yml b/changelogs/unreleased/zj-upgrade-grape.yml new file mode 100644 index 00000000000..75a72283d1e --- /dev/null +++ b/changelogs/unreleased/zj-upgrade-grape.yml @@ -0,0 +1,4 @@ +--- +title: Update grape entity to 0.5.2 +merge_request: 7491 +author: -- cgit v1.2.1 From 01f238893aaf8f5aa24eec7e33edc479d4e1315d Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 27 Oct 2016 14:04:43 +0200 Subject: Rename MWBS service to Merge When Pipeline Succeeds --- .../projects/merge_requests_controller.rb | 13 +- .../merge_when_build_succeeds_service.rb | 45 ------ .../merge_when_pipeline_succeeds_service.rb | 45 ++++++ app/workers/pipeline_success_worker.rb | 2 +- lib/api/merge_requests.rb | 14 +- .../projects/merge_requests_controller_spec.rb | 4 +- .../merge_when_build_succeeds_service_spec.rb | 163 -------------------- .../merge_when_pipeline_succeeds_service_spec.rb | 169 +++++++++++++++++++++ spec/workers/pipeline_success_worker_spec.rb | 2 +- 9 files changed, 237 insertions(+), 220 deletions(-) delete mode 100644 app/services/merge_requests/merge_when_build_succeeds_service.rb create mode 100644 app/services/merge_requests/merge_when_pipeline_succeeds_service.rb delete mode 100644 spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb create mode 100644 spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 036fde87619..5a2136303c9 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -302,9 +302,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def cancel_merge_when_build_succeeds - return access_denied! unless @merge_request.can_cancel_merge_when_build_succeeds?(current_user) + unless @merge_request.can_cancel_merge_when_build_succeeds?(current_user) + return access_denied! + end - MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user).cancel(@merge_request) + MergeRequests::MergeWhenPipelineSucceedsService + .new(@project, current_user) + .cancel(@merge_request) end def merge @@ -331,8 +335,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController end if @merge_request.pipeline.active? - MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params) - .execute(@merge_request) + MergeRequests::MergeWhenPipelineSucceedsService + .new(@project, current_user, merge_params) + .execute(@merge_request) @status = :merge_when_build_succeeds elsif @merge_request.pipeline.success? # This can be triggered when a user clicks the auto merge button while diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb deleted file mode 100644 index dc159de0058..00000000000 --- a/app/services/merge_requests/merge_when_build_succeeds_service.rb +++ /dev/null @@ -1,45 +0,0 @@ -module MergeRequests - class MergeWhenBuildSucceedsService < MergeRequests::BaseService - # Marks the passed `merge_request` to be merged when the build succeeds or - # updates the params for the automatic merge - def execute(merge_request) - merge_request.merge_params.merge!(params) - - # The service is also called when the merge params are updated. - already_approved = merge_request.merge_when_build_succeeds? - - unless already_approved - merge_request.merge_when_build_succeeds = true - merge_request.merge_user = @current_user - - SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.diff_head_commit) - end - - merge_request.save - end - - # Triggers the automatic merge of merge_request once the pipeline succeeds - def trigger(pipeline) - return unless pipeline.success? - - pipeline_merge_requests(pipeline) do |merge_request| - next unless merge_request.merge_when_build_succeeds? - next unless merge_request.mergeable? - - MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params) - end - end - - # Cancels the automatic merge - def cancel(merge_request) - if merge_request.merge_when_build_succeeds? && merge_request.open? - merge_request.reset_merge_when_build_succeeds - SystemNoteService.cancel_merge_when_build_succeeds(merge_request, @project, @current_user) - - success - else - error("Can't cancel the automatic merge", 406) - end - end - end -end diff --git a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb new file mode 100644 index 00000000000..5616edf8b4a --- /dev/null +++ b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb @@ -0,0 +1,45 @@ +module MergeRequests + class MergeWhenPipelineSucceedsService < MergeRequests::BaseService + # Marks the passed `merge_request` to be merged when the build succeeds or + # updates the params for the automatic merge + def execute(merge_request) + merge_request.merge_params.merge!(params) + + # The service is also called when the merge params are updated. + already_approved = merge_request.merge_when_build_succeeds? + + unless already_approved + merge_request.merge_when_build_succeeds = true + merge_request.merge_user = @current_user + + SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.diff_head_commit) + end + + merge_request.save + end + + # Triggers the automatic merge of merge_request once the pipeline succeeds + def trigger(pipeline) + return unless pipeline.success? + + pipeline_merge_requests(pipeline) do |merge_request| + next unless merge_request.merge_when_build_succeeds? + next unless merge_request.mergeable? + + MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params) + end + end + + # Cancels the automatic merge + def cancel(merge_request) + if merge_request.merge_when_build_succeeds? && merge_request.open? + merge_request.reset_merge_when_build_succeeds + SystemNoteService.cancel_merge_when_build_succeeds(merge_request, @project, @current_user) + + success + else + error("Can't cancel the automatic merge", 406) + end + end + end +end diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb index 2aa6fff24da..cc0eb708cf9 100644 --- a/app/workers/pipeline_success_worker.rb +++ b/app/workers/pipeline_success_worker.rb @@ -4,7 +4,7 @@ class PipelineSuccessWorker def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline| - MergeRequests::MergeWhenBuildSucceedsService + MergeRequests::MergeWhenPipelineSucceedsService .new(pipeline.project, nil) .trigger(pipeline) end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 4176c7eec06..8c55f09a9ce 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -205,11 +205,13 @@ module API } if params[:merge_when_build_succeeds] && merge_request.pipeline && merge_request.pipeline.active? - ::MergeRequests::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user, merge_params). - execute(merge_request) + ::MergeRequests::MergeWhenPipelineSucceedsService + .new(merge_request.target_project, current_user, merge_params) + .execute(merge_request) else - ::MergeRequests::MergeService.new(merge_request.target_project, current_user, merge_params). - execute(merge_request) + ::MergeRequests::MergeService + .new(merge_request.target_project, current_user, merge_params) + .execute(merge_request) end present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project @@ -223,7 +225,9 @@ module API unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user) - ::MergeRequest::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user).cancel(merge_request) + ::MergeRequest::MergeWhenPipelineSucceedsService + .new(merge_request.target_project, current_user) + .cancel(merge_request) end desc 'Get the comments of a merge request' do diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 1d0750d1719..9e0b80205d8 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -292,7 +292,9 @@ describe Projects::MergeRequestsController do it 'sets the MR to merge when the build succeeds' do service = double(:merge_when_build_succeeds_service) - expect(MergeRequests::MergeWhenBuildSucceedsService).to receive(:new).with(project, anything, anything).and_return(service) + expect(MergeRequests::MergeWhenPipelineSucceedsService) + .to receive(:new).with(project, anything, anything) + .and_return(service) expect(service).to receive(:execute).with(merge_request) merge_when_build_succeeds diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb deleted file mode 100644 index 1f90efdbd6a..00000000000 --- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb +++ /dev/null @@ -1,163 +0,0 @@ -require 'spec_helper' - -describe MergeRequests::MergeWhenBuildSucceedsService do - let(:user) { create(:user) } - let(:project) { create(:project) } - - let(:mr_merge_if_green_enabled) do - create(:merge_request, merge_when_build_succeeds: true, merge_user: user, - source_branch: "master", target_branch: 'feature', - source_project: project, target_project: project, state: "opened") - end - - let(:pipeline) { create(:ci_pipeline_with_one_job, ref: mr_merge_if_green_enabled.source_branch, project: project) } - let(:service) { MergeRequests::MergeWhenBuildSucceedsService.new(project, user, commit_message: 'Awesome message') } - - describe "#execute" do - let(:merge_request) do - create(:merge_request, target_project: project, source_project: project, - source_branch: "feature", target_branch: 'master') - end - - context 'first time enabling' do - before do - allow(merge_request).to receive(:pipeline).and_return(pipeline) - service.execute(merge_request) - end - - it 'sets the params, merge_user, and flag' do - expect(merge_request).to be_valid - expect(merge_request.merge_when_build_succeeds).to be_truthy - expect(merge_request.merge_params).to eq commit_message: 'Awesome message' - expect(merge_request.merge_user).to be user - end - - it 'creates a system note' do - note = merge_request.notes.last - expect(note.note).to match /Enabled an automatic merge when the build for (\w+\/\w+@)?[0-9a-z]{8}/ - end - end - - context 'already approved' do - let(:service) { MergeRequests::MergeWhenBuildSucceedsService.new(project, user, new_key: true) } - let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch) } - - before do - allow(mr_merge_if_green_enabled).to receive(:pipeline).and_return(pipeline) - allow(mr_merge_if_green_enabled).to receive(:mergeable?).and_return(true) - allow(pipeline).to receive(:success?).and_return(true) - end - - it 'updates the merge params' do - expect(SystemNoteService).not_to receive(:merge_when_build_succeeds) - - service.execute(mr_merge_if_green_enabled) - expect(mr_merge_if_green_enabled.merge_params).to have_key(:new_key) - end - end - end - - describe "#trigger" do - let(:merge_request_ref) { mr_merge_if_green_enabled.source_branch } - let(:merge_request_head) do - project.commit(mr_merge_if_green_enabled.source_branch).id - end - - context 'when triggered by pipeline with valid ref and sha' do - let(:triggering_pipeline) do - create(:ci_pipeline, project: project, ref: merge_request_ref, - sha: merge_request_head, status: 'success') - end - - it "merges all merge requests with merge when build succeeds enabled" do - expect(MergeWorker).to receive(:perform_async) - service.trigger(triggering_pipeline) - end - end - - context 'when triggered by an old pipeline' do - let(:old_pipeline) do - create(:ci_pipeline, project: project, ref: merge_request_ref, - sha: '1234abcdef', status: 'success') - end - - it 'it does not merge merge request' do - expect(MergeWorker).not_to receive(:perform_async) - service.trigger(old_pipeline) - end - end - - context 'when triggered by pipeline from a different branch' do - let(:unrelated_pipeline) do - create(:ci_pipeline, project: project, ref: 'feature', - sha: merge_request_head, status: 'success') - end - - it 'does not merge request' do - expect(MergeWorker).not_to receive(:perform_async) - service.trigger(unrelated_pipeline) - end - end - end - - describe "#cancel" do - before do - service.cancel(mr_merge_if_green_enabled) - end - - it "resets all the merge_when_build_succeeds params" do - expect(mr_merge_if_green_enabled.merge_when_build_succeeds).to be_falsey - expect(mr_merge_if_green_enabled.merge_params).to eq({}) - expect(mr_merge_if_green_enabled.merge_user).to be nil - end - - it 'Posts a system note' do - note = mr_merge_if_green_enabled.notes.last - expect(note.note).to include 'Canceled the automatic merge' - end - end - - describe 'pipeline integration' do - context 'when there are multiple stages in the pipeline' do - let(:ref) { mr_merge_if_green_enabled.source_branch } - let(:sha) { project.commit(ref).id } - - let(:pipeline) do - create(:ci_empty_pipeline, ref: ref, sha: sha, project: project) - end - - let!(:build) do - create(:ci_build, :created, pipeline: pipeline, ref: ref, - name: 'build', stage: 'build') - end - - let!(:test) do - create(:ci_build, :created, pipeline: pipeline, ref: ref, - name: 'test', stage: 'test') - end - - before do - # This behavior of MergeRequest: we instantiate a new object - allow_any_instance_of(MergeRequest).to receive(:pipeline).and_wrap_original do - Ci::Pipeline.find(pipeline.id) - end - end - - it "doesn't merge if any of stages failed" do - expect(MergeWorker).not_to receive(:perform_async) - - build.success - test.reload - test.drop - end - - it 'merges when all stages succeeded' do - expect(MergeWorker).to receive(:perform_async) - - build.success - test.reload - test.success - end - end - end -end diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb new file mode 100644 index 00000000000..793806bd69a --- /dev/null +++ b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb @@ -0,0 +1,169 @@ +require 'spec_helper' + +describe MergeRequests::MergeWhenPipelineSucceedsService do + let(:user) { create(:user) } + let(:project) { create(:project) } + + let(:mr_merge_if_green_enabled) do + create(:merge_request, merge_when_build_succeeds: true, merge_user: user, + source_branch: "master", target_branch: 'feature', + source_project: project, target_project: project, state: "opened") + end + + let(:pipeline) do + create(:ci_pipeline_with_one_job, ref: mr_merge_if_green_enabled.source_branch, + project: project) + end + + let(:service) do + described_class.new(project, user, commit_message: 'Awesome message') + end + + describe "#execute" do + let(:merge_request) do + create(:merge_request, target_project: project, source_project: project, + source_branch: "feature", target_branch: 'master') + end + + context 'first time enabling' do + before do + allow(merge_request).to receive(:pipeline).and_return(pipeline) + service.execute(merge_request) + end + + it 'sets the params, merge_user, and flag' do + expect(merge_request).to be_valid + expect(merge_request.merge_when_build_succeeds).to be_truthy + expect(merge_request.merge_params).to eq commit_message: 'Awesome message' + expect(merge_request.merge_user).to be user + end + + it 'creates a system note' do + note = merge_request.notes.last + expect(note.note).to match /Enabled an automatic merge when the build for (\w+\/\w+@)?[0-9a-z]{8}/ + end + end + + context 'already approved' do + let(:service) { described_class.new(project, user, new_key: true) } + let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch) } + + before do + allow(mr_merge_if_green_enabled).to receive(:pipeline).and_return(pipeline) + allow(mr_merge_if_green_enabled).to receive(:mergeable?).and_return(true) + allow(pipeline).to receive(:success?).and_return(true) + end + + it 'updates the merge params' do + expect(SystemNoteService).not_to receive(:merge_when_build_succeeds) + + service.execute(mr_merge_if_green_enabled) + expect(mr_merge_if_green_enabled.merge_params).to have_key(:new_key) + end + end + end + + describe "#trigger" do + let(:merge_request_ref) { mr_merge_if_green_enabled.source_branch } + let(:merge_request_head) do + project.commit(mr_merge_if_green_enabled.source_branch).id + end + + context 'when triggered by pipeline with valid ref and sha' do + let(:triggering_pipeline) do + create(:ci_pipeline, project: project, ref: merge_request_ref, + sha: merge_request_head, status: 'success') + end + + it "merges all merge requests with merge when build succeeds enabled" do + expect(MergeWorker).to receive(:perform_async) + service.trigger(triggering_pipeline) + end + end + + context 'when triggered by an old pipeline' do + let(:old_pipeline) do + create(:ci_pipeline, project: project, ref: merge_request_ref, + sha: '1234abcdef', status: 'success') + end + + it 'it does not merge merge request' do + expect(MergeWorker).not_to receive(:perform_async) + service.trigger(old_pipeline) + end + end + + context 'when triggered by pipeline from a different branch' do + let(:unrelated_pipeline) do + create(:ci_pipeline, project: project, ref: 'feature', + sha: merge_request_head, status: 'success') + end + + it 'does not merge request' do + expect(MergeWorker).not_to receive(:perform_async) + service.trigger(unrelated_pipeline) + end + end + end + + describe "#cancel" do + before do + service.cancel(mr_merge_if_green_enabled) + end + + it "resets all the merge_when_build_succeeds params" do + expect(mr_merge_if_green_enabled.merge_when_build_succeeds).to be_falsey + expect(mr_merge_if_green_enabled.merge_params).to eq({}) + expect(mr_merge_if_green_enabled.merge_user).to be nil + end + + it 'Posts a system note' do + note = mr_merge_if_green_enabled.notes.last + expect(note.note).to include 'Canceled the automatic merge' + end + end + + describe 'pipeline integration' do + context 'when there are multiple stages in the pipeline' do + let(:ref) { mr_merge_if_green_enabled.source_branch } + let(:sha) { project.commit(ref).id } + + let(:pipeline) do + create(:ci_empty_pipeline, ref: ref, sha: sha, project: project) + end + + let!(:build) do + create(:ci_build, :created, pipeline: pipeline, ref: ref, + name: 'build', stage: 'build') + end + + let!(:test) do + create(:ci_build, :created, pipeline: pipeline, ref: ref, + name: 'test', stage: 'test') + end + + before do + # This behavior of MergeRequest: we instantiate a new object + allow_any_instance_of(MergeRequest).to receive(:pipeline).and_wrap_original do + Ci::Pipeline.find(pipeline.id) + end + end + + it "doesn't merge if any of stages failed" do + expect(MergeWorker).not_to receive(:perform_async) + + build.success + test.reload + test.drop + end + + it 'merges when all stages succeeded' do + expect(MergeWorker).to receive(:perform_async) + + build.success + test.reload + test.success + end + end + end +end diff --git a/spec/workers/pipeline_success_worker_spec.rb b/spec/workers/pipeline_success_worker_spec.rb index 5e31cc2c8e7..d1c84adda6f 100644 --- a/spec/workers/pipeline_success_worker_spec.rb +++ b/spec/workers/pipeline_success_worker_spec.rb @@ -7,7 +7,7 @@ describe PipelineSuccessWorker do it 'performs "merge when pipeline succeeds"' do expect_any_instance_of( - MergeRequests::MergeWhenBuildSucceedsService + MergeRequests::MergeWhenPipelineSucceedsService ).to receive(:trigger) described_class.new.perform(pipeline.id) -- cgit v1.2.1 From bd3ae192bb62d522912281b5943073f6ad444fe4 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 21 Nov 2016 10:55:54 +0100 Subject: Rename MWPS in system notes and related tests --- app/services/system_note_service.rb | 2 +- .../merge_requests/widget/open/_merge_when_build_succeeds.html.haml | 2 +- spec/features/merge_requests/merge_when_build_succeeds_spec.rb | 4 ++-- .../merge_requests/merge_when_pipeline_succeeds_service_spec.rb | 2 +- spec/services/system_note_service_spec.rb | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 1ce66d50368..925e4938784 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -135,7 +135,7 @@ module SystemNoteService # Called when 'merge when build succeeds' is executed def merge_when_build_succeeds(noteable, project, author, last_commit) - body = "Enabled an automatic merge when the build for #{last_commit.to_reference(project)} succeeds" + body = "Enabled an automatic merge when the pipeline for #{last_commit.to_reference(project)} succeeds" create_note(noteable: noteable, project: project, author: author, note: body) end diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml index 2b6b5e05e86..cea3e28ceb3 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -1,6 +1,6 @@ %h4 Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)} - to be merged automatically when the build succeeds. + to be merged automatically when the pipeline succeeds. %div %p = succeed '.' do diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb index 8eceaad2457..a710ca66f70 100644 --- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb @@ -40,11 +40,11 @@ feature 'Merge When Build Succeeds', feature: true, js: true do it 'activates Merge When Build Succeeds feature' do expect(page).to have_link "Cancel Automatic Merge" - expect(page).to have_content "Set by #{user.name} to be merged automatically when the build succeeds." + expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds." expect(page).to have_content "The source branch will not be removed." visit_merge_request(merge_request) # Needed to refresh the page - expect(page).to have_content /Enabled an automatic merge when the build for [0-9a-f]{8} succeeds/i + expect(page).to have_content /Enabled an automatic merge when the pipeline for [0-9a-f]{8} succeeds/i end end end diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb index 793806bd69a..3b7c9204a35 100644 --- a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb @@ -40,7 +40,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do it 'creates a system note' do note = merge_request.notes.last - expect(note.note).to match /Enabled an automatic merge when the build for (\w+\/\w+@)?[0-9a-z]{8}/ + expect(note.note).to match /Enabled an automatic merge when the pipeline for (\w+\/\w+@)?[0-9a-z]{8}/ end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 56d39e9a005..7f0da4bf03c 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -225,8 +225,8 @@ describe SystemNoteService, services: true do it_behaves_like 'a system note' - it "posts the Merge When Build Succeeds system note" do - expect(subject.note).to match /Enabled an automatic merge when the build for (\w+\/\w+@)?[0-9a-f]{40} succeeds/ + it "posts the 'merge when pipeline succeeds' system note" do + expect(subject.note).to match /Enabled an automatic merge when the pipeline for (\w+\/\w+@)?[0-9a-f]{40} succeeds/ end end @@ -239,7 +239,7 @@ describe SystemNoteService, services: true do it_behaves_like 'a system note' - it "posts the Merge When Build Succeeds system note" do + it "posts the 'merge when pipeline succeeds' system note" do expect(subject.note).to eq "Canceled the automatic merge" end end -- cgit v1.2.1 From d07ef089c8870b4a5a1c69555c54c93ff1f1fa24 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 21 Nov 2016 11:20:53 +0100 Subject: Rename MWPS in user interface and feature tests --- .../merge_requests/widget/open/_accept.html.haml | 4 +- .../merge_when_build_succeeds_spec.rb | 108 -------------------- .../merge_when_pipeline_succeeds_spec.rb | 109 +++++++++++++++++++++ 3 files changed, 111 insertions(+), 110 deletions(-) delete mode 100644 spec/features/merge_requests/merge_when_build_succeeds_spec.rb create mode 100644 spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index ce43ca3a286..435fe835fae 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -9,7 +9,7 @@ - if @pipeline && @pipeline.active? %span.btn-group = button_tag class: "btn btn-create js-merge-button merge_when_build_succeeds" do - Merge When Build Succeeds + Merge When Pipeline Succeeds - unless @project.only_allow_merge_if_build_succeeds? = button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do = icon('caret-down') @@ -19,7 +19,7 @@ %li = link_to "#", class: "merge_when_build_succeeds" do = icon('check fw') - Merge When Build Succeeds + Merge When Pipeline Succeeds %li = link_to "#", class: "accept_merge_request" do = icon('warning fw') diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb deleted file mode 100644 index a710ca66f70..00000000000 --- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb +++ /dev/null @@ -1,108 +0,0 @@ -require 'spec_helper' - -feature 'Merge When Build Succeeds', feature: true, js: true do - let(:user) { create(:user) } - let(:project) { create(:project, :public) } - - let(:merge_request) do - create(:merge_request_with_diffs, source_project: project, - author: user, - title: 'Bug NS-04') - end - - let(:pipeline) do - create(:ci_pipeline, project: project, - sha: merge_request.diff_head_sha, - ref: merge_request.source_branch) - end - - before { project.team << [user, :master] } - - context 'when there is active build for merge request' do - background do - create(:ci_build, pipeline: pipeline) - end - - before do - login_as user - visit_merge_request(merge_request) - end - - it 'displays the Merge When Build Succeeds button' do - expect(page).to have_button "Merge When Build Succeeds" - end - - context "Merge When Build succeeds enabled" do - before do - click_button "Merge When Build Succeeds" - end - - it 'activates Merge When Build Succeeds feature' do - expect(page).to have_link "Cancel Automatic Merge" - - expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds." - expect(page).to have_content "The source branch will not be removed." - - visit_merge_request(merge_request) # Needed to refresh the page - expect(page).to have_content /Enabled an automatic merge when the pipeline for [0-9a-f]{8} succeeds/i - end - end - end - - context 'when merge when build succeeds is enabled' do - let(:merge_request) do - create(:merge_request_with_diffs, :simple, source_project: project, - author: user, - merge_user: user, - title: 'MepMep', - merge_when_build_succeeds: true) - end - - let!(:build) do - create(:ci_build, pipeline: pipeline) - end - - before do - login_as user - visit_merge_request(merge_request) - end - - it 'allows to cancel the automatic merge' do - click_link "Cancel Automatic Merge" - - expect(page).to have_button "Merge When Build Succeeds" - - visit_merge_request(merge_request) # refresh the page - expect(page).to have_content "canceled the automatic merge" - end - - it "allows the user to remove the source branch" do - expect(page).to have_link "Remove Source Branch When Merged" - - click_link "Remove Source Branch When Merged" - expect(page).to have_content "The source branch will be removed" - end - - context 'when build succeeds' do - background { build.success } - - it 'merges merge request' do - visit_merge_request(merge_request) # refresh the page - - expect(page).to have_content 'The changes were merged' - expect(merge_request.reload).to be_merged - end - end - end - - context 'when build is not active' do - it "does not allow to enable merge when build succeeds" do - visit_merge_request(merge_request) - expect(page).not_to have_link "Merge When Build Succeeds" - end - end - - def visit_merge_request(merge_request) - visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) - end -end diff --git a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb new file mode 100644 index 00000000000..638b27172ec --- /dev/null +++ b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb @@ -0,0 +1,109 @@ +require 'spec_helper' + +feature 'Merge When Pipeline Succeeds', :feature, :js do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + + let(:merge_request) do + create(:merge_request_with_diffs, source_project: project, + author: user, + title: 'Bug NS-04') + end + + let(:pipeline) do + create(:ci_pipeline, project: project, + sha: merge_request.diff_head_sha, + ref: merge_request.source_branch) + end + + before { project.team << [user, :master] } + + context 'when there is active pipeline for merge request' do + background do + create(:ci_build, pipeline: pipeline) + end + + before do + login_as user + visit_merge_request(merge_request) + end + + it 'displays the Merge When Pipeline Succeeds button' do + expect(page).to have_button "Merge When Pipeline Succeeds" + end + + context "Merge When Pipeline Succeeds enabled" do + before do + click_button "Merge When Pipeline Succeeds" + end + + it 'activates Merge When Pipeline Succeeds feature' do + expect(page).to have_link "Cancel Automatic Merge" + + expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds." + expect(page).to have_content "The source branch will not be removed." + + visit_merge_request(merge_request) # Needed to refresh the page + expect(page).to have_content /Enabled an automatic merge when the pipeline for [0-9a-f]{8} succeeds/i + end + end + end + + context 'when merge when pipeline succeeds is enabled' do + let(:merge_request) do + create(:merge_request_with_diffs, :simple, source_project: project, + author: user, + merge_user: user, + title: 'MepMep', + merge_when_build_succeeds: true) + end + + let!(:build) do + create(:ci_build, pipeline: pipeline) + end + + before do + login_as user + visit_merge_request(merge_request) + end + + it 'allows to cancel the automatic merge' do + click_link "Cancel Automatic Merge" + + expect(page).to have_button "Merge When Pipeline Succeeds" + + visit_merge_request(merge_request) # refresh the page + expect(page).to have_content "canceled the automatic merge" + end + + it "allows the user to remove the source branch" do + expect(page).to have_link "Remove Source Branch When Merged" + + click_link "Remove Source Branch When Merged" + expect(page).to have_content "The source branch will be removed" + end + + context 'when pipeline succeeds' do + background { build.success } + + it 'merges merge request' do + visit_merge_request(merge_request) # refresh the page + + expect(page).to have_content 'The changes were merged' + expect(merge_request.reload).to be_merged + end + end + end + + context 'when pipeline is not active' do + it "does not allow to enable merge when pipeline succeeds" do + visit_merge_request(merge_request) + + expect(page).not_to have_link 'Merge When Pipeline Succeeds' + end + end + + def visit_merge_request(merge_request) + visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) + end +end -- cgit v1.2.1 From c6a4f9fc5b98024d4af872b0009b90c36bc2e595 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 21 Nov 2016 11:27:28 +0100 Subject: Update some docs to reflect MWPS name change --- app/services/system_note_service.rb | 4 ++-- doc/api/merge_requests.md | 2 +- doc/development/code_review.md | 4 ++-- doc/intro/README.md | 2 +- doc/user/project/merge_requests.md | 10 +++++----- .../project/merge_requests/merge_when_build_succeeds.md | 16 ++++++++-------- doc/workflow/README.md | 2 +- lib/api/merge_requests.rb | 4 ++-- spec/models/merge_request_spec.rb | 2 +- spec/requests/api/merge_requests_spec.rb | 2 +- 10 files changed, 24 insertions(+), 24 deletions(-) diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 925e4938784..5bf6e094d68 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -133,14 +133,14 @@ module SystemNoteService create_note(noteable: noteable, project: project, author: author, note: body) end - # Called when 'merge when build succeeds' is executed + # Called when 'merge when pipeline succeeds' is executed def merge_when_build_succeeds(noteable, project, author, last_commit) body = "Enabled an automatic merge when the pipeline for #{last_commit.to_reference(project)} succeeds" create_note(noteable: noteable, project: project, author: author, note: body) end - # Called when 'merge when build succeeds' is canceled + # Called when 'merge when pipeline succeeds' is canceled def cancel_merge_when_build_succeeds(noteable, project, author) body = 'Canceled the automatic merge' diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index f4167403c2c..75b9d5d1a75 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -518,7 +518,7 @@ Parameters: } ``` -## Cancel Merge When Build Succeeds +## Cancel Merge When Pipeline Succeeds If successful you'll get `200 OK`. diff --git a/doc/development/code_review.md b/doc/development/code_review.md index c5c23b5c0b8..4bf0e7197e9 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -70,8 +70,8 @@ experience, refactors the existing code). Then: - After a round of line notes, it can be helpful to post a summary note such as "LGTM :thumbsup:", or "Just a couple things to address." - Avoid accepting a merge request before the build succeeds. Of course, "Merge - When Build Succeeds" (MWBS) is fine. -- If you set the MR to "Merge When Build Succeeds", you should take over + When Pipeline Succeeds" (MWPS) is fine. +- If you set the MR to "Merge When Pipeline Succeeds", you should take over subsequent revisions for anything that would be spotted after that. ## Credits diff --git a/doc/intro/README.md b/doc/intro/README.md index 1790b2b761f..6deed35b7c9 100644 --- a/doc/intro/README.md +++ b/doc/intro/README.md @@ -23,7 +23,7 @@ Create merge requests and review code. - [Fork a project and contribute to it](../workflow/forking_workflow.md) - [Create a new merge request](../gitlab-basics/add-merge-request.md) - [Automatically close issues from merge requests](../user/project/issues/automatic_issue_closing.md) -- [Automatically merge when your builds succeed](../user/project/merge_requests/merge_when_build_succeeds.md) +- [Automatically merge when pipeline succeeds](../user/project/merge_requests/merge_when_build_succeeds.md) - [Revert any commit](../user/project/merge_requests/revert_changes.md) - [Cherry-pick any commit](../user/project/merge_requests/cherry_pick_changes.md) diff --git a/doc/user/project/merge_requests.md b/doc/user/project/merge_requests.md index 5af9a5d049c..e76428d41f3 100644 --- a/doc/user/project/merge_requests.md +++ b/doc/user/project/merge_requests.md @@ -19,14 +19,14 @@ in a merged merge requests or a commit. [Learn more about cherry-picking changes.](merge_requests/cherry_pick_changes.md) -## Merge when build succeeds +## Merge when pipeline succeeds When reviewing a merge request that looks ready to merge but still has one or -more CI builds running, you can set it to be merged automatically when all -builds succeed. This way, you don't have to wait for the builds to finish and -remember to merge the request manually. +more CI builds running, you can set it to be merged automatically when CI +pipeline succeeds. This way, you don't have to wait for the pipeline to finish +and remember to merge the request manually. -[Learn more about merging when build succeeds.](merge_requests/merge_when_build_succeeds.md) +[Learn more about merging when pipeline succeeds.](merge_requests/merge_when_build_succeeds.md) ## Resolve discussion comments in merge requests reviews diff --git a/doc/user/project/merge_requests/merge_when_build_succeeds.md b/doc/user/project/merge_requests/merge_when_build_succeeds.md index d4e5b5de685..75ad18b28cf 100644 --- a/doc/user/project/merge_requests/merge_when_build_succeeds.md +++ b/doc/user/project/merge_requests/merge_when_build_succeeds.md @@ -1,13 +1,13 @@ -# Merge When Build Succeeds +# Merge When Pipeline Succeeds When reviewing a merge request that looks ready to merge but still has one or more CI builds running, you can set it to be merged automatically when the -builds pipeline succeed. This way, you don't have to wait for the builds to +builds pipeline succeeds. This way, you don't have to wait for the builds to finish and remember to merge the request manually. ![Enable](img/merge_when_build_succeeds_enable.png) -When you hit the "Merge When Build Succeeds" button, the status of the merge +When you hit the "Merge When Pipeline Succeeds" button, the status of the merge request will be updated to represent the impending merge. If you cannot wait for the pipeline to succeed and want to merge immediately, this option is available in the dropdown menu on the right of the main button. @@ -27,20 +27,20 @@ will automatically be merged after all. When the merge request is updated with new commits, the automatic merge is automatically canceled to allow the new changes to be reviewed. -## Only allow merge requests to be merged if the build succeeds +## Only allow merge requests to be merged if the pipeline succeeds > **Note:** You need to have builds configured to enable this feature. -You can prevent merge requests from being merged if their build did not succeed. +You can prevent merge requests from being merged if their pipeline did not succeed. Navigate to your project's settings page, select the -**Only allow merge requests to be merged if the build succeeds** check box and +**Only allow merge requests to be merged if the pipeline succeeds** check box and hit **Save** for the changes to take effect. -![Only allow merge if build succeeds settings](img/merge_when_build_succeeds_only_if_succeeds_settings.png) +![Only allow merge if pipeline succeeds settings](img/merge_when_build_succeeds_only_if_succeeds_settings.png) From now on, every time the pipeline fails you will not be able to merge the merge request from the UI, until you make all relevant builds pass. -![Only allow merge if build succeeds message](img/merge_when_build_succeeds_only_if_succeeds_msg.png) +![Only allow merge if pipeline succeeds message](img/merge_when_build_succeeds_only_if_succeeds_msg.png) diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 2d9bfbc0629..57a0d21c7a7 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -25,7 +25,7 @@ - [Merge Requests](../user/project/merge_requests.md) - [Authorization for merge requests](../user/project/merge_requests/authorization_for_merge_requests.md) - [Cherry-pick changes](../user/project/merge_requests/cherry_pick_changes.md) - - [Merge when build succeeds](../user/project/merge_requests/merge_when_build_succeeds.md) + - [Merge when pipeline succeeds](../user/project/merge_requests/merge_when_build_succeeds.md) - [Resolve discussion comments in merge requests reviews](../user/project/merge_requests/merge_request_discussion_resolution.md) - [Resolve merge conflicts in the UI](../user/project/merge_requests/resolve_conflicts.md) - [Revert changes in the UI](../user/project/merge_requests/revert_changes.md) diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 8c55f09a9ce..19f93c1c892 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -181,7 +181,7 @@ module API optional :should_remove_source_branch, type: Boolean, desc: 'When true, the source branch will be deleted if possible' optional :merge_when_build_succeeds, type: Boolean, - desc: 'When true, this merge request will be merged when the build succeeds' + desc: 'When true, this merge request will be merged when the pipeline succeeds' optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch' end put "#{path}/merge" do @@ -217,7 +217,7 @@ module API present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project end - desc 'Cancel merge if "Merge when build succeeds" is enabled' do + desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do success Entities::MergeRequest end post "#{path}/cancel_merge_when_build_succeeds" do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index fb032a89d50..e295a920b45 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -31,7 +31,7 @@ describe MergeRequest, models: true do it { is_expected.to validate_presence_of(:target_branch) } it { is_expected.to validate_presence_of(:source_branch) } - context "Validation of merge user with Merge When Build succeeds" do + context "Validation of merge user with Merge When Pipeline Succeeds" do it "allows user to be nil when the feature is disabled" do expect(subject).to be_valid end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 7b3d1460c90..212e633c9aa 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -463,7 +463,7 @@ describe API::API, api: true do expect(response).to have_http_status(200) end - it "enables merge when build succeeds if the ci is active" do + it "enables merge when pipeline succeeds if the pipeline is active" do allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline) allow(pipeline).to receive(:active?).and_return(true) -- cgit v1.2.1 From 1edb1746a51a19fae24c976c329e80a1dbd6062a Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 21 Nov 2016 17:59:57 +0800 Subject: Prefer a description for it and split the case: Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7508#note_18730091 --- spec/models/ci/pipeline_spec.rb | 22 ++++++++---- spec/models/concerns/has_status_spec.rb | 61 +++++++++++++++++++-------------- 2 files changed, 51 insertions(+), 32 deletions(-) diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index af619a02ed9..29e5693d5ab 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -403,15 +403,15 @@ describe Ci::Pipeline, models: true do end describe '#cancelable?' do - subject { pipeline.cancelable? } - %i[created running pending].each do |status| context "when there is a build #{status}" do before do create(:ci_build, status, pipeline: pipeline) end - it { is_expected.to be_truthy } + it 'is cancelable' do + expect(pipeline.cancelable?).to be_truthy + end end context "when there is an external job #{status}" do @@ -419,7 +419,9 @@ describe Ci::Pipeline, models: true do create(:generic_commit_status, status, pipeline: pipeline) end - it { is_expected.to be_truthy } + it 'is cancelable' do + expect(pipeline.cancelable?).to be_truthy + end end %i[success failed canceled].each do |status2| @@ -430,7 +432,9 @@ describe Ci::Pipeline, models: true do create(build.sample, status2, pipeline: pipeline) end - it { is_expected.to be_truthy } + it 'is cancelable' do + expect(pipeline.cancelable?).to be_truthy + end end end end @@ -441,7 +445,9 @@ describe Ci::Pipeline, models: true do create(:ci_build, status, pipeline: pipeline) end - it { is_expected.to be_falsey } + it 'is not cancelable' do + expect(pipeline.cancelable?).to be_falsey + end end context "when there is an external job #{status}" do @@ -449,7 +455,9 @@ describe Ci::Pipeline, models: true do create(:generic_commit_status, status, pipeline: pipeline) end - it { is_expected.to be_falsey } + it 'is not cancelable' do + expect(pipeline.cancelable?).to be_falsey + end end end end diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb index 24cd435256e..788c84bf5de 100644 --- a/spec/models/concerns/has_status_spec.rb +++ b/spec/models/concerns/has_status_spec.rb @@ -134,20 +134,20 @@ describe HasStatus do let!(:job) { create(type, status) } describe ".#{status}" do - subject { CommitStatus.public_send(status).all } - - it { is_expected.to contain_exactly(job) } + it 'contains the job' do + expect(CommitStatus.public_send(status).all). + to contain_exactly(job) + end end describe '.relevant' do - subject { CommitStatus.relevant.all } - - it do - case status - when :created - is_expected.to be_empty - else - is_expected.to contain_exactly(job) + if status == :created + it 'contains nothing' do + expect(CommitStatus.relevant.all).to be_empty + end + else + it 'contains the job' do + expect(CommitStatus.relevant.all).to contain_exactly(job) end end end @@ -161,17 +161,22 @@ describe HasStatus do end context 'for scope with more statuses' do - shared_examples 'having a job' do |type, status, excluded_status| + shared_examples 'containing the job' do |type, status| context "when it's #{status} #{type} job" do let!(:job) { create(type, status) } - it do - case status - when excluded_status - is_expected.to be_empty - else - is_expected.to contain_exactly(job) - end + it 'contains the job' do + is_expected.to contain_exactly(job) + end + end + end + + shared_examples 'not containing the job' do |type, status| + context "when it's #{status} #{type} job" do + let!(:job) { create(type, status) } + + it 'contains nothing' do + is_expected.to be_empty end end end @@ -179,25 +184,31 @@ describe HasStatus do describe '.running_or_pending' do subject { CommitStatus.running_or_pending } - %i[running pending created].each do |status| - it_behaves_like 'having a job', random_type, status, :created + %i[running pending].each do |status| + it_behaves_like 'containing the job', random_type, status end + + it_behaves_like 'not containing the job', random_type, :created end describe '.finished' do subject { CommitStatus.finished } - %i[success failed canceled created].each do |status| - it_behaves_like 'having a job', random_type, status, :created + %i[success failed canceled].each do |status| + it_behaves_like 'containing the job', random_type, status end + + it_behaves_like 'not containing the job', random_type, :created end describe '.cancelable' do subject { CommitStatus.cancelable } - %i[running pending created failed].each do |status| - it_behaves_like 'having a job', random_type, status, :failed + %i[running pending created].each do |status| + it_behaves_like 'containing the job', random_type, status end + + it_behaves_like 'not containing the job', random_type, :failed end end end -- cgit v1.2.1 From 0a5a65df0c7d08e3ce041e10906549313a9ad156 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 21 Nov 2016 12:42:44 +0100 Subject: Add Changelog entry for Merge When Pipeline Succeeds --- .../unreleased/fix-rename-mwbs-to-merge-when-pipeline-succeeds.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-rename-mwbs-to-merge-when-pipeline-succeeds.yml diff --git a/changelogs/unreleased/fix-rename-mwbs-to-merge-when-pipeline-succeeds.yml b/changelogs/unreleased/fix-rename-mwbs-to-merge-when-pipeline-succeeds.yml new file mode 100644 index 00000000000..f8acc6ef8ad --- /dev/null +++ b/changelogs/unreleased/fix-rename-mwbs-to-merge-when-pipeline-succeeds.yml @@ -0,0 +1,4 @@ +--- +title: Rename Merge When Build Succeeds to Merge When Pipeline Succeeds +merge_request: 7135 +author: -- cgit v1.2.1 From 721f2d3788ae5e8374f357014bd9e20d62de0a81 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 21 Nov 2016 22:19:16 +0800 Subject: Still use compound pipeline status, but group by ref and sha so that it would show latest pipeline if ref and sha are both specified, otherwise still the same as before. --- app/models/ci/pipeline.rb | 16 +++++++++++++--- app/models/commit.rb | 8 +------- app/services/ci/image_for_build_service.rb | 11 ++--------- spec/models/commit_spec.rb | 8 ++++---- 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index cd7d8fd3af7..e566503bb18 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -89,13 +89,23 @@ module Ci end end - scope :latest, -> { order(id: :desc) } + scope :latest, -> do + max_id = unscope(:select).select("max(#{quoted_table_name}.id)") + + where(id: max_id.group(:ref, :sha)) + end # ref can't be HEAD or SHA, can only be branch/tag name - scope :latest_for, ->(ref) { where(ref: ref).latest } + scope :latest_for, ->(ref) do + if ref + where(ref: ref) + else + self + end.latest + end def self.latest_successful_for(ref) - latest_for(ref).success.first + where(ref: ref).order(id: :desc).success.first end def self.truncate_sha(sha) diff --git a/app/models/commit.rb b/app/models/commit.rb index 2134ba2d75f..b588b93b158 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -234,13 +234,7 @@ class Commit return @statuses[ref] if @statuses.key?(ref) - latest_pipeline = if ref - pipelines.latest_for(ref) - else - pipelines.latest - end.first - - @statuses[ref] = latest_pipeline.try(:status) + @statuses[ref] = pipelines.latest_for(ref).status end def revert_branch_name diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb index 026a727a8f9..d5a07ef630b 100644 --- a/app/services/ci/image_for_build_service.rb +++ b/app/services/ci/image_for_build_service.rb @@ -3,18 +3,11 @@ module Ci def execute(project, opts) ref = opts[:ref] sha = opts[:sha] || ref_sha(project, ref) - pipelines = project.pipelines.where(sha: sha) - latest_pipeline = if ref - pipelines.latest_for(ref) - else - pipelines.latest - end.first - - image_name = image_for_status(latest_pipeline.try(:status)) - + image_name = image_for_status(pipelines.latest_for(ref).status) image_path = Rails.root.join('public/ci', image_name) + OpenStruct.new(path: image_path, name: image_name) end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index ca277601970..21590cd4ff1 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -216,8 +216,8 @@ eos end end - it 'gives the status from latest pipeline' do - expect(commit.status).to eq(Ci::Pipeline.latest.first.status) + it 'gives compound status' do + expect(commit.status).to eq(Ci::Pipeline.latest.status) end end @@ -243,8 +243,8 @@ eos expect(commit.status('fix')).to eq(pipeline_from_fix.status) end - it 'gives status from latest pipeline for whatever branch' do - expect(commit.status(nil)).to eq(Ci::Pipeline.latest.first.status) + it 'gives compound status if ref is nil' do + expect(commit.status(nil)).to eq(Ci::Pipeline.latest.status) end end end -- cgit v1.2.1 From dd6b16a111a6e6cce0be322be8ddba17a2a30534 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 21 Nov 2016 22:24:09 +0800 Subject: Use latest_for in latest_successful_for --- app/models/ci/pipeline.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index e566503bb18..95af8c72309 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -105,7 +105,7 @@ module Ci end def self.latest_successful_for(ref) - where(ref: ref).order(id: :desc).success.first + latest_for(ref).success.first end def self.truncate_sha(sha) -- cgit v1.2.1 From d09d6ad01da722b3558565b8b78f0476b529a91c Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 21 Nov 2016 22:39:58 +0800 Subject: Test against all types and more status: Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7508#note_18748231 --- spec/models/concerns/has_status_spec.rb | 82 ++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb index 788c84bf5de..9defb17dc92 100644 --- a/spec/models/concerns/has_status_spec.rb +++ b/spec/models/concerns/has_status_spec.rb @@ -124,30 +124,28 @@ describe HasStatus do end end - def self.random_type - %i[ci_build generic_commit_status].sample - end - context 'for scope with one status' do - shared_examples 'having a job' do |type, status| - context "when it's #{status} #{type} job" do - let!(:job) { create(type, status) } + shared_examples 'having a job' do |status| + %i[ci_build generic_commit_status].each do |type| + context "when it's #{status} #{type} job" do + let!(:job) { create(type, status) } - describe ".#{status}" do - it 'contains the job' do - expect(CommitStatus.public_send(status).all). - to contain_exactly(job) + describe ".#{status}" do + it 'contains the job' do + expect(CommitStatus.public_send(status).all). + to contain_exactly(job) + end end - end - describe '.relevant' do - if status == :created - it 'contains nothing' do - expect(CommitStatus.relevant.all).to be_empty - end - else - it 'contains the job' do - expect(CommitStatus.relevant.all).to contain_exactly(job) + describe '.relevant' do + if status == :created + it 'contains nothing' do + expect(CommitStatus.relevant.all).to be_empty + end + else + it 'contains the job' do + expect(CommitStatus.relevant.all).to contain_exactly(job) + end end end end @@ -156,27 +154,31 @@ describe HasStatus do %i[created running pending success failed canceled skipped].each do |status| - it_behaves_like 'having a job', random_type, status + it_behaves_like 'having a job', status end end context 'for scope with more statuses' do - shared_examples 'containing the job' do |type, status| - context "when it's #{status} #{type} job" do - let!(:job) { create(type, status) } + shared_examples 'containing the job' do |status| + %i[ci_build generic_commit_status].each do |type| + context "when it's #{status} #{type} job" do + let!(:job) { create(type, status) } - it 'contains the job' do - is_expected.to contain_exactly(job) + it 'contains the job' do + is_expected.to contain_exactly(job) + end end end end - shared_examples 'not containing the job' do |type, status| - context "when it's #{status} #{type} job" do - let!(:job) { create(type, status) } + shared_examples 'not containing the job' do |status| + %i[ci_build generic_commit_status].each do |type| + context "when it's #{status} #{type} job" do + let!(:job) { create(type, status) } - it 'contains nothing' do - is_expected.to be_empty + it 'contains nothing' do + is_expected.to be_empty + end end end end @@ -185,30 +187,36 @@ describe HasStatus do subject { CommitStatus.running_or_pending } %i[running pending].each do |status| - it_behaves_like 'containing the job', random_type, status + it_behaves_like 'containing the job', status end - it_behaves_like 'not containing the job', random_type, :created + %i[created failed success].each do |status| + it_behaves_like 'not containing the job', status + end end describe '.finished' do subject { CommitStatus.finished } %i[success failed canceled].each do |status| - it_behaves_like 'containing the job', random_type, status + it_behaves_like 'containing the job', status end - it_behaves_like 'not containing the job', random_type, :created + %i[created running pending].each do |status| + it_behaves_like 'not containing the job', status + end end describe '.cancelable' do subject { CommitStatus.cancelable } %i[running pending created].each do |status| - it_behaves_like 'containing the job', random_type, status + it_behaves_like 'containing the job', status end - it_behaves_like 'not containing the job', random_type, :failed + %i[failed success skipped canceled].each do |status| + it_behaves_like 'not containing the job', status + end end end end -- cgit v1.2.1 From c7c4850d0b9d6751a5f6fdaa1f9c34aee6728676 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 22 Nov 2016 01:21:15 +0800 Subject: Test against all possible cases, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7508#note_18755739 --- spec/models/ci/pipeline_spec.rb | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 29e5693d5ab..cbf25c23b05 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -403,10 +403,10 @@ describe Ci::Pipeline, models: true do end describe '#cancelable?' do - %i[created running pending].each do |status| - context "when there is a build #{status}" do + %i[created running pending].each do |status0| + context "when there is a build #{status0}" do before do - create(:ci_build, status, pipeline: pipeline) + create(:ci_build, status0, pipeline: pipeline) end it 'is cancelable' do @@ -414,9 +414,9 @@ describe Ci::Pipeline, models: true do end end - context "when there is an external job #{status}" do + context "when there is an external job #{status0}" do before do - create(:generic_commit_status, status, pipeline: pipeline) + create(:generic_commit_status, status0, pipeline: pipeline) end it 'is cancelable' do @@ -424,16 +424,19 @@ describe Ci::Pipeline, models: true do end end - %i[success failed canceled].each do |status2| - context "when there are two builds for #{status} and #{status2}" do - before do - build = %i[ci_build generic_commit_status] - create(build.sample, status, pipeline: pipeline) - create(build.sample, status2, pipeline: pipeline) - end + %i[success failed canceled].each do |status1| + %i[ci_build generic_commit_status].each do |type0| + %i[ci_build generic_commit_status].each do |type1| + context "when there are #{type0} and #{type1} for #{status0} and #{status1}" do + before do + create(type0, status0, pipeline: pipeline) + create(type1, status1, pipeline: pipeline) + end - it 'is cancelable' do - expect(pipeline.cancelable?).to be_truthy + it 'is cancelable' do + expect(pipeline.cancelable?).to be_truthy + end + end end end end -- cgit v1.2.1 From 66301ce274b42cefe76e343a3d545c8f12d847b2 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 22 Nov 2016 01:48:41 +0800 Subject: Filter against status first, otherwise we can't find the latest successful one if the last one is failed and we already exclude the others. --- app/models/ci/pipeline.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 95af8c72309..61d9316a5d3 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -105,7 +105,7 @@ module Ci end def self.latest_successful_for(ref) - latest_for(ref).success.first + success.latest_for(ref).first end def self.truncate_sha(sha) -- cgit v1.2.1 From 5c747ded7ecc1d7da418a854d12ad2906b173b7b Mon Sep 17 00:00:00 2001 From: Liz Lam Date: Thu, 17 Nov 2016 23:17:59 -0800 Subject: Edit help text to clarify that tags are annotated. --- app/views/projects/tags/new.html.haml | 2 +- changelogs/unreleased/dev-issue-24554.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/dev-issue-24554.yml diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index 3a097750d6e..c06a413eb2f 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -23,7 +23,7 @@ = label_tag :message, nil, class: 'control-label' .col-sm-10 = text_area_tag :message, nil, required: false, tabindex: 3, class: 'form-control', rows: 5 - .help-block Optionally, enter a message to create an annotated tag. + .help-block Optionally, add a message to the tag. %hr .form-group = label_tag :release_description, 'Release notes', class: 'control-label' diff --git a/changelogs/unreleased/dev-issue-24554.yml b/changelogs/unreleased/dev-issue-24554.yml new file mode 100644 index 00000000000..0bb362b9325 --- /dev/null +++ b/changelogs/unreleased/dev-issue-24554.yml @@ -0,0 +1,4 @@ +--- +title: Edit help text to clarify annotated tag creation. +merge_request: +author: Liz Lam -- cgit v1.2.1 From 855567ac587178de56d7a177c7b5e2243bbb030d Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 21 Nov 2016 17:05:43 -0600 Subject: roll back eslint-plugin-import node module to the version required by eslint-config-airbnb --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e75e070451b..3ebbfc3e59c 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "eslint": "^3.1.1", "eslint-config-airbnb": "^12.0.0", "eslint-plugin-filenames": "^1.1.0", - "eslint-plugin-import": "^2.0.1", + "eslint-plugin-import": "^1.16.0", "eslint-plugin-jasmine": "^1.8.1", "eslint-plugin-jsx-a11y": "^2.2.3", "eslint-plugin-react": "^6.4.1" -- cgit v1.2.1 From de21cfd1417e4e1f3d8076cd5e4bfc8a0f4b39ac Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Sat, 19 Nov 2016 14:44:06 +0000 Subject: Swapped buttons for checkboxes --- app/helpers/application_settings_helper.rb | 8 ++++---- app/views/admin/application_settings/_form.html.haml | 6 ++---- ...or-import-sources-in-administrator-settings-enable-disable.yml | 4 ++++ 3 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 changelogs/unreleased/24161-non-intuitive-buttons-for-import-sources-in-administrator-settings-enable-disable.yml diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index be5e0301a43..93c645d422c 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -67,14 +67,14 @@ module ApplicationSettingsHelper def import_sources_checkboxes(help_block_id) Gitlab::ImportSources.options.map do |name, source| checked = current_application_settings.import_sources.include?(source) - css_class = 'btn' - css_class += ' active' if checked + css_class = checked ? 'active' : '' checkbox_name = 'application_setting[import_sources][]' - label_tag(checkbox_name, class: css_class) do + label_tag(name, class: css_class) do check_box_tag(checkbox_name, source, checked, autocomplete: 'off', - 'aria-describedby' => help_block_id) + name + 'aria-describedby' => help_block_id, + id: name.tr(' ', '_')) + name end end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index a236335131a..de6b2272c43 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -32,10 +32,8 @@ .form-group = f.label :import_sources, class: 'control-label col-sm-2' .col-sm-10 - - data_attrs = { toggle: 'buttons' } - .btn-group{ data: data_attrs } - - import_sources_checkboxes('import-sources-help').each do |source| - = source + - import_sources_checkboxes('import-sources-help').each do |source| + .checkbox= source %span.help-block#import-sources-help Enabled sources for code import during project creation. OmniAuth must be configured for GitHub = link_to "(?)", help_page_path("integration/github") diff --git a/changelogs/unreleased/24161-non-intuitive-buttons-for-import-sources-in-administrator-settings-enable-disable.yml b/changelogs/unreleased/24161-non-intuitive-buttons-for-import-sources-in-administrator-settings-enable-disable.yml new file mode 100644 index 00000000000..1404748e83e --- /dev/null +++ b/changelogs/unreleased/24161-non-intuitive-buttons-for-import-sources-in-administrator-settings-enable-disable.yml @@ -0,0 +1,4 @@ +--- +title: Changed import sources buttons to checkboxes +merge_request: 7598 +author: Luke "Jared" Bennett -- cgit v1.2.1 From 588e21522a3287d80c6fb0bc533f48f57be4c561 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Tue, 22 Nov 2016 00:13:12 +0100 Subject: fixed mini pipeline graph connector lines offset --- app/assets/stylesheets/pages/pipelines.scss | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index a44a496c728..1b1b056f213 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -195,7 +195,7 @@ width: 8px; position: absolute; right: -7px; - bottom: 8px; + bottom: 9px; border-bottom: 2px solid $border-color; } } @@ -691,10 +691,3 @@ } } } - -.ci-status-icon-created { - - svg { - fill: $gray-darkest; - } -} -- cgit v1.2.1 From 9124b5966c50766df6fe623856f325adfb191504 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Tue, 22 Nov 2016 00:14:40 +0100 Subject: Converted all status icons to be managed by scss colors only and deleted any classes or styles within the svg's, plus gave status badges a hover style only if clickable --- app/assets/stylesheets/pages/icons.scss | 53 +++++++++++++-- app/assets/stylesheets/pages/status.scss | 75 ++++++++++++++++------ app/views/projects/builds/_sidebar.html.haml | 3 +- .../projects/ci/builds/_build_pipeline.html.haml | 4 +- app/views/projects/commit/_commit_box.html.haml | 2 +- .../commit/_pipeline_status_group.html.haml | 2 +- .../_generic_commit_status_pipeline.html.haml | 4 +- app/views/projects/pipelines/_info.html.haml | 6 +- app/views/shared/icons/_icon_status_canceled.svg | 7 +- app/views/shared/icons/_icon_status_created.svg | 2 +- app/views/shared/icons/_icon_status_failed.svg | 7 +- app/views/shared/icons/_icon_status_pending.svg | 7 +- app/views/shared/icons/_icon_status_running.svg | 7 +- app/views/shared/icons/_icon_status_skipped.svg | 2 +- app/views/shared/icons/_icon_status_success.svg | 7 +- app/views/shared/icons/_icon_status_warning.svg | 7 +- 16 files changed, 118 insertions(+), 77 deletions(-) diff --git a/app/assets/stylesheets/pages/icons.scss b/app/assets/stylesheets/pages/icons.scss index 407c8db211d..3293dc2029d 100644 --- a/app/assets/stylesheets/pages/icons.scss +++ b/app/assets/stylesheets/pages/icons.scss @@ -1,12 +1,51 @@ -// CI icon colors +.ci-status-icon-success { + color: $gl-success; -.ci-status-icon { - &-created { - fill: $gray-darkest; + svg { + fill: $gl-success; + } +} + +.ci-status-icon-failed { + color: $gl-danger; + + svg { + fill: $gl-danger; + } +} + +.ci-status-icon-pending, +.ci-status-icon-success_with_warning { + color: $gl-warning; + + svg { + fill: $gl-warning; + } +} + +.ci-status-icon-running { + color: $blue-normal; + + svg { + fill: $blue-normal; + } +} + +.ci-status-icon-canceled, +.ci-status-icon-disabled, +.ci-status-icon-not-found { + color: $gl-gray; + + svg { + fill: $gl-gray; } +} - &-skipped, - &-canceled { - fill: $gl-text-color; +.ci-status-icon-created, +.ci-status-icon-skipped { + color: $gray-darkest; + + svg { + fill: $gray-darkest; } } diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 92997eae8b9..e97b610a07b 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -14,40 +14,92 @@ &.ci-failed { color: $gl-danger; border-color: $gl-danger; + + &:not(span):hover { + background-color: rgba( $gl-danger, .07); + } + + svg { + fill: $gl-danger; + } } &.ci-success, &.ci-success_with_warnings { color: $gl-success; border-color: $gl-success; + + &:not(span):hover { + background-color: rgba( $gl-success, .07); + } + + svg { + fill: $gl-success; + } } &.ci-info { color: $gl-info; border-color: $gl-info; + + &:not(span):hover { + background-color: rgba( $gl-info, .07); + } + + svg { + fill: $gl-info; + } } &.ci-canceled, - &.ci-skipped, &.ci-disabled { color: $gl-gray; border-color: $gl-gray; + + &:not(span):hover { + background-color: rgba( $gl-gray, .07); + } + + svg { + fill: $gl-gray; + } } &.ci-pending { color: $gl-warning; border-color: $gl-warning; + + &:not(span):hover { + background-color: rgba( $gl-warning, .07); + } + + svg { + fill: $gl-warning; + } } &.ci-running { color: $blue-normal; border-color: $blue-normal; + + &:not(span):hover { + background-color: rgba( $blue-normal, .07); + } + + svg { + fill: $blue-normal; + } } - &.ci-created { + &.ci-created, + &.ci-skipped { color: $table-text-gray; border-color: $table-text-gray; + &:not(span):hover { + background-color: rgba( $table-text-gray, .07); + } + svg { fill: $table-text-gray; } @@ -63,29 +115,10 @@ } } - .ci-status-icon-success { - color: $gl-success; - } - .ci-status-icon-failed { - color: $gl-danger; - } - .ci-status-icon-pending, - .ci-status-icon-success_with_warning { - color: $gl-warning; - } - .ci-status-icon-running { - color: $blue-normal; - } - .ci-status-icon-canceled, - .ci-status-icon-disabled, - .ci-status-icon-not-found, - .ci-status-icon-skipped { - color: $gl-gray; - } } .visible-xs-inline { diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index 28f519f11b2..f5562046953 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -128,7 +128,8 @@ .build-job{class: sidebar_build_class(build, @build), data: {stage: build.stage}} = link_to namespace_project_build_path(@project.namespace, @project, build) do = icon('arrow-right') - = ci_icon_for_status(build.status) + %span{class: "ci-status-icon-#{build.status}"} + = ci_icon_for_status(build.status) %span - if build.name = build.name diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml index 93dca81e6f9..423a1282eb2 100644 --- a/app/views/projects/ci/builds/_build_pipeline.html.haml +++ b/app/views/projects/ci/builds/_build_pipeline.html.haml @@ -5,9 +5,9 @@ .ci-status-text= subject.name - elsif can?(current_user, :read_build, @project) = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } do - %span.ci-status-icon + %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} = ci_icon_for_status(subject.status) .ci-status-text= subject.name - else - %span.ci-status-icon + %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} = ci_icon_for_status(subject.status) diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 503cbd13b5e..65151ac3a56 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -68,7 +68,7 @@ - if @commit.status .well-segment.pipeline-info - .icon-container + %div{class: "icon-container ci-status-icon-#{@commit.status}"} = ci_icon_for_status(@commit.status) Pipeline = link_to "##{@commit.pipelines.last.id}", pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "monospace" diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml index 18daa2ee693..2b26ad9d6fa 100644 --- a/app/views/projects/commit/_pipeline_status_group.html.haml +++ b/app/views/projects/commit/_pipeline_status_group.html.haml @@ -1,6 +1,6 @@ - group_status = CommitStatus.where(id: subject).status %button.dropdown-menu-toggle.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } } - %span.ci-status-icon + %span{class: "ci-status-icon ci-status-icon-#{group_status}"} = ci_icon_for_status(group_status) %span.ci-status-text = name diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml index 1c457244a7a..7b82d913d29 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml @@ -1,10 +1,10 @@ %a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } } - if subject.target_url = link_to subject.target_url do - %span.ci-status-icon + %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} = ci_icon_for_status(subject.status) %span.ci-status-text= subject.name - else - %span.ci-status-icon + %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} = ci_icon_for_status(subject.status) %span.ci-status-text= subject.name diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index a0de125d765..095bd254d6b 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -1,8 +1,6 @@ .page-content-header .header-main-content - = link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do - = ci_icon_for_status(@pipeline.status) - = ci_label_for_status(@pipeline.status) + = ci_status_with_icon(@pipeline.status) %strong Pipeline ##{@commit.pipelines.last.id} triggered #{time_ago_with_tooltip(@commit.authored_date)} by = author_avatar(@commit, size: 24) @@ -25,7 +23,7 @@ .info-well - if @commit.status .well-segment.pipeline-info - .icon-container + %div{class: "icon-container ci-status-icon-#{@commit.status}"} = ci_icon_for_status(@commit.status) = pluralize @pipeline.statuses.count(:id), "build" - if @pipeline.ref diff --git a/app/views/shared/icons/_icon_status_canceled.svg b/app/views/shared/icons/_icon_status_canceled.svg index 1b2d0891244..41a210a8ed9 100644 --- a/app/views/shared/icons/_icon_status_canceled.svg +++ b/app/views/shared/icons/_icon_status_canceled.svg @@ -1,6 +1 @@ - - - - - - + diff --git a/app/views/shared/icons/_icon_status_created.svg b/app/views/shared/icons/_icon_status_created.svg index dca5d289767..1f5c3b51b03 100644 --- a/app/views/shared/icons/_icon_status_created.svg +++ b/app/views/shared/icons/_icon_status_created.svg @@ -1 +1 @@ - + diff --git a/app/views/shared/icons/_icon_status_failed.svg b/app/views/shared/icons/_icon_status_failed.svg index e56e0887416..af267b8938a 100644 --- a/app/views/shared/icons/_icon_status_failed.svg +++ b/app/views/shared/icons/_icon_status_failed.svg @@ -1,6 +1 @@ - - - - - - + diff --git a/app/views/shared/icons/_icon_status_pending.svg b/app/views/shared/icons/_icon_status_pending.svg index 117f0367161..516231d1b44 100644 --- a/app/views/shared/icons/_icon_status_pending.svg +++ b/app/views/shared/icons/_icon_status_pending.svg @@ -1,6 +1 @@ - - - - - - + diff --git a/app/views/shared/icons/_icon_status_running.svg b/app/views/shared/icons/_icon_status_running.svg index 920d7952eb5..d2618bce200 100644 --- a/app/views/shared/icons/_icon_status_running.svg +++ b/app/views/shared/icons/_icon_status_running.svg @@ -1,6 +1 @@ - - - - - - + diff --git a/app/views/shared/icons/_icon_status_skipped.svg b/app/views/shared/icons/_icon_status_skipped.svg index e0a2d4282f0..701f33bcbea 100644 --- a/app/views/shared/icons/_icon_status_skipped.svg +++ b/app/views/shared/icons/_icon_status_skipped.svg @@ -1 +1 @@ - + diff --git a/app/views/shared/icons/_icon_status_success.svg b/app/views/shared/icons/_icon_status_success.svg index 67b378b3571..b7c21ba6971 100644 --- a/app/views/shared/icons/_icon_status_success.svg +++ b/app/views/shared/icons/_icon_status_success.svg @@ -1,6 +1 @@ - - - - - - + diff --git a/app/views/shared/icons/_icon_status_warning.svg b/app/views/shared/icons/_icon_status_warning.svg index d0ad4bd65b1..9191e0050a6 100644 --- a/app/views/shared/icons/_icon_status_warning.svg +++ b/app/views/shared/icons/_icon_status_warning.svg @@ -1,6 +1 @@ - - - - - - + -- cgit v1.2.1 From ccddde50879ba8fe694a461f63b01b9f0de6a7df Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Tue, 22 Nov 2016 00:58:16 +0100 Subject: additional fixes --- app/assets/stylesheets/pages/icons.scss | 2 +- app/assets/stylesheets/pages/status.scss | 5 ----- app/views/projects/ci/pipelines/_pipeline.html.haml | 5 +++-- app/views/projects/merge_requests/widget/_heading.html.haml | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/app/assets/stylesheets/pages/icons.scss b/app/assets/stylesheets/pages/icons.scss index 3293dc2029d..226bd2ead31 100644 --- a/app/assets/stylesheets/pages/icons.scss +++ b/app/assets/stylesheets/pages/icons.scss @@ -15,7 +15,7 @@ } .ci-status-icon-pending, -.ci-status-icon-success_with_warning { +.ci-status-icon-success_with_warnings { color: $gl-warning; svg { diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index e97b610a07b..1de4674f3e7 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -114,11 +114,6 @@ overflow: visible; } } - - - - - } .visible-xs-inline { diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 2a2d24be736..4c7b14a04db 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -4,8 +4,9 @@ %tr.commit %td.commit-link - = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do - = ci_status_with_icon(status) + = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "ci-status ci-#{status}" do + = ci_icon_for_status(status) + = ci_label_for_status(status) %td = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index a82c846baa7..09c41f947b0 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,7 +1,7 @@ - if @pipeline .mr-widget-heading - %w[success success_with_warnings skipped canceled failed running pending].each do |status| - .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) } + .ci_widget{ class: "ci-status-icon-#{status}", style: ("display:none" unless @pipeline.status == status) } = ci_icon_for_status(status) %span Pipeline -- cgit v1.2.1 From 5c71a053bcb97064e6e7756ca78dccc2e9e852a4 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Tue, 22 Nov 2016 01:01:59 +0100 Subject: added changelog entry --- changelogs/unreleased/Last-minute-CI-Style-tweaks-for-8-14.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/Last-minute-CI-Style-tweaks-for-8-14.yml diff --git a/changelogs/unreleased/Last-minute-CI-Style-tweaks-for-8-14.yml b/changelogs/unreleased/Last-minute-CI-Style-tweaks-for-8-14.yml new file mode 100644 index 00000000000..7d49c639a43 --- /dev/null +++ b/changelogs/unreleased/Last-minute-CI-Style-tweaks-for-8-14.yml @@ -0,0 +1,4 @@ +--- +title: Last minute CI Style tweaks for 8.14 +merge_request: 7643 +author: -- cgit v1.2.1 From 69aaa972038c7b285c06804e04ff5932e468e523 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Fri, 19 Aug 2016 17:59:58 -0300 Subject: Remove omniauth-bitbucket gem --- Gemfile | 1 - Gemfile.lock | 5 ----- 2 files changed, 6 deletions(-) diff --git a/Gemfile b/Gemfile index 9e815925a1f..3576a670f93 100644 --- a/Gemfile +++ b/Gemfile @@ -22,7 +22,6 @@ gem 'doorkeeper', '~> 4.2.0' gem 'omniauth', '~> 1.3.1' gem 'omniauth-auth0', '~> 1.4.1' gem 'omniauth-azure-oauth2', '~> 0.0.6' -gem 'omniauth-bitbucket', '~> 0.0.2' gem 'omniauth-cas3', '~> 1.1.2' gem 'omniauth-facebook', '~> 4.0.0' gem 'omniauth-github', '~> 1.1.1' diff --git a/Gemfile.lock b/Gemfile.lock index bdc60552480..af1facf255a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -438,10 +438,6 @@ GEM jwt (~> 1.0) omniauth (~> 1.0) omniauth-oauth2 (~> 1.1) - omniauth-bitbucket (0.0.2) - multi_json (~> 1.7) - omniauth (~> 1.1) - omniauth-oauth (~> 1.0) omniauth-cas3 (1.1.3) addressable (~> 2.3) nokogiri (~> 1.6.6) @@ -905,7 +901,6 @@ DEPENDENCIES omniauth (~> 1.3.1) omniauth-auth0 (~> 1.4.1) omniauth-azure-oauth2 (~> 0.0.6) - omniauth-bitbucket (~> 0.0.2) omniauth-cas3 (~> 1.1.2) omniauth-facebook (~> 4.0.0) omniauth-github (~> 1.1.1) -- cgit v1.2.1 From a2ef52b32bd3d1ee70ea333566e330c44e6c9170 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 22 Aug 2016 15:36:41 -0300 Subject: Add custom OmniAuth strategy for Bitbucket OAuth2 --- config/initializers/omniauth.rb | 6 +++++ lib/omniauth/strategies/bitbucket.rb | 45 ++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 lib/omniauth/strategies/bitbucket.rb diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index 26c30e523a7..ab5a0561b8c 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -26,3 +26,9 @@ if Gitlab.config.omniauth.enabled end end end + +module OmniAuth + module Strategies + autoload :Bitbucket, Rails.root.join('lib', 'omniauth', 'strategies', 'bitbucket') + end +end diff --git a/lib/omniauth/strategies/bitbucket.rb b/lib/omniauth/strategies/bitbucket.rb new file mode 100644 index 00000000000..2006e7582ce --- /dev/null +++ b/lib/omniauth/strategies/bitbucket.rb @@ -0,0 +1,45 @@ +require 'omniauth-oauth2' + +module OmniAuth + module Strategies + class Bitbucket < OmniAuth::Strategies::OAuth2 + option :name, 'bitbucket' + + option :client_options, { + site: 'https://bitbucket.org', + authorize_url: 'https://bitbucket.org/site/oauth2/authorize', + token_url: 'https://bitbucket.org/site/oauth2/access_token' + } + + def callback_url + full_host + script_name + callback_path + end + + uid do + raw['username'] + end + + info do + { + name: raw_info['display_name'], + avatar: raw_info['links']['avatar']['href'], + email: primary_email + } + end + + def raw_info + @raw_info ||= access_token.get('user').parsed + end + + def primary_email + primary = emails.find{ |i| i['is_primary'] && i['is_confirmed'] } + primary && primary['email'] || nil + end + + def emails + email_response = access_token.get('user/emails').parsed + @emails ||= email_response && email_response['values'] || nil + end + end + end +end -- cgit v1.2.1 From fc34c9560324b7db5bdaf205cbdf46a339102af2 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 22 Aug 2016 15:42:26 -0300 Subject: Add a Bitbucket client for the OAuth2 API --- lib/bitbucket/client.rb | 11 ++++++++ lib/bitbucket/connection.rb | 69 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 lib/bitbucket/client.rb create mode 100644 lib/bitbucket/connection.rb diff --git a/lib/bitbucket/client.rb b/lib/bitbucket/client.rb new file mode 100644 index 00000000000..5f48e52da98 --- /dev/null +++ b/lib/bitbucket/client.rb @@ -0,0 +1,11 @@ +module Bitbucket + class Client + def initialize(options = {}) + @connection = options.fetch(:connection, Connection.new(options)) + end + + private + + attr_reader :connection + end +end diff --git a/lib/bitbucket/connection.rb b/lib/bitbucket/connection.rb new file mode 100644 index 00000000000..00f127f9507 --- /dev/null +++ b/lib/bitbucket/connection.rb @@ -0,0 +1,69 @@ +module Bitbucket + class Connection + DEFAULT_API_VERSION = '2.0' + DEFAULT_BASE_URI = 'https://api.bitbucket.org/' + DEFAULT_QUERY = {} + + def initialize(options = {}) + @api_version = options.fetch(:api_version, DEFAULT_API_VERSION) + @base_uri = options.fetch(:base_uri, DEFAULT_BASE_URI) + @query = options.fetch(:query, DEFAULT_QUERY) + + @token = options.fetch(:token) + @expires_at = options.fetch(:expires_at) + @expires_in = options.fetch(:expires_in) + @refresh_token = options.fetch(:refresh_token) + + @client = OAuth2::Client.new(provider.app_id, provider.app_secret, options) + @connection = OAuth2::AccessToken.new(@client, @token, refresh_token: @refresh_token, expires_at: @expires_at, expires_in: @expires_in) + end + + def query(params = {}) + @query.update(params) + end + + def get(path, query = {}) + refresh! if expired? + + response = connection.get(build_url(path), params: @query.merge(query)) + response.parsed + end + + def expired? + connection.expired? + end + + def refresh! + response = connection.refresh! + + @token = response.token + @expires_at = response.expires_at + @expires_in = response.expires_in + @refresh_token = response.refresh_token + + @connection = OAuth2::AccessToken.new(@client, @token, refresh_token: @refresh_token, expires_at: @expires_at, expires_in: @expires_in) + end + + private + + attr_reader :connection, :expires_at, :expires_in, :refresh_token, :token + + def build_url(path) + return path if path.starts_with?(root_url) + + "#{root_url}#{path}" + end + + def root_url + @root_url ||= "#{@base_uri}#{@api_version}" + end + + def provider + Gitlab.config.omniauth.providers.find { |provider| provider.name == 'bitbucket' } + end + + def options + OmniAuth::Strategies::Bitbucket.default_options[:client_options].deep_symbolize_keys + end + end +end -- cgit v1.2.1 From f1f5863bfc5727dba4767a54a299b0cf526b025a Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 22 Aug 2016 15:45:29 -0300 Subject: Add an endpoint to get the Bitbucket user profile --- lib/bitbucket/client.rb | 5 +++++ lib/bitbucket/representation/base.rb | 13 +++++++++++++ lib/bitbucket/representation/user.rb | 9 +++++++++ 3 files changed, 27 insertions(+) create mode 100644 lib/bitbucket/representation/base.rb create mode 100644 lib/bitbucket/representation/user.rb diff --git a/lib/bitbucket/client.rb b/lib/bitbucket/client.rb index 5f48e52da98..c05fc35f36e 100644 --- a/lib/bitbucket/client.rb +++ b/lib/bitbucket/client.rb @@ -4,6 +4,11 @@ module Bitbucket @connection = options.fetch(:connection, Connection.new(options)) end + def user + parsed_response = connection.get('/user') + Representation::User.new(parsed_response) + end + private attr_reader :connection diff --git a/lib/bitbucket/representation/base.rb b/lib/bitbucket/representation/base.rb new file mode 100644 index 00000000000..7b639492d38 --- /dev/null +++ b/lib/bitbucket/representation/base.rb @@ -0,0 +1,13 @@ +module Bitbucket + module Representation + class Base + def initialize(raw) + @raw = raw + end + + private + + attr_reader :raw + end + end +end diff --git a/lib/bitbucket/representation/user.rb b/lib/bitbucket/representation/user.rb new file mode 100644 index 00000000000..ba6b7667b49 --- /dev/null +++ b/lib/bitbucket/representation/user.rb @@ -0,0 +1,9 @@ +module Bitbucket + module Representation + class User < Representation::Base + def username + raw['username'] + end + end + end +end -- cgit v1.2.1 From e2f7f32a60a7663d12b5dae0320f640150f354e7 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 22 Aug 2016 15:48:31 -0300 Subject: Add support for Bitbucket pagination when fetching collections --- lib/bitbucket/collection.rb | 21 +++++++++++++++++++++ lib/bitbucket/page.rb | 36 ++++++++++++++++++++++++++++++++++++ lib/bitbucket/paginator.rb | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 lib/bitbucket/collection.rb create mode 100644 lib/bitbucket/page.rb create mode 100644 lib/bitbucket/paginator.rb diff --git a/lib/bitbucket/collection.rb b/lib/bitbucket/collection.rb new file mode 100644 index 00000000000..9cc8947417c --- /dev/null +++ b/lib/bitbucket/collection.rb @@ -0,0 +1,21 @@ +module Bitbucket + class Collection < Enumerator + def initialize(paginator) + super() do |yielder| + loop do + paginator.next.each { |item| yielder << item } + end + end + + lazy + end + + def method_missing(method, *args) + return super unless self.respond_to?(method) + + self.send(method, *args) do |item| + block_given? ? yield(item) : item + end + end + end +end diff --git a/lib/bitbucket/page.rb b/lib/bitbucket/page.rb new file mode 100644 index 00000000000..ad9a2baba36 --- /dev/null +++ b/lib/bitbucket/page.rb @@ -0,0 +1,36 @@ +module Bitbucket + class Page + attr_reader :attrs, :items + + def initialize(raw, type) + @attrs = parse_attrs(raw) + @items = parse_values(raw, representation_class(type)) + end + + def next? + attrs.fetch(:next, false) + end + + def next + attrs.fetch(:next) + end + + private + + def parse_attrs(raw) + attrs = %w(size page pagelen next previous) + attrs.map { |attr| { attr.to_sym => raw[attr] } }.reduce(&:merge) + end + + def parse_values(raw, representation_class) + return [] if raw['values'].nil? || !raw['values'].is_a?(Array) + + raw['values'].map { |hash| representation_class.new(hash) } + end + + def representation_class(type) + class_name = "Bitbucket::Representation::#{type.to_s.camelize}" + class_name.constantize + end + end +end diff --git a/lib/bitbucket/paginator.rb b/lib/bitbucket/paginator.rb new file mode 100644 index 00000000000..a1672d9eaa1 --- /dev/null +++ b/lib/bitbucket/paginator.rb @@ -0,0 +1,38 @@ +module Bitbucket + class Paginator + PAGE_LENGTH = 50 # The minimum length is 10 and the maximum is 100. + + def initialize(connection, url, type) + @connection = connection + @type = type + @url = url + @page = nil + + connection.query(pagelen: PAGE_LENGTH, sort: :created_on) + end + + def next + raise StopIteration unless has_next_page? + + @page = fetch_next_page + @page.items + end + + private + + attr_reader :connection, :page, :url, :type + + def has_next_page? + page.nil? || page.next? + end + + def page_url + page.nil? ? url : page.next + end + + def fetch_next_page + parsed_response = connection.get(page_url) + Page.new(parsed_response, type) + end + end +end -- cgit v1.2.1 From 6418c6f88efe9015c8bc2ebd4f7db1a7277a4dc9 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 22 Aug 2016 15:53:46 -0300 Subject: Add an endpoint to get the user's repositories --- lib/bitbucket/client.rb | 14 +++++++-- lib/bitbucket/representation/repo.rb | 57 ++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 lib/bitbucket/representation/repo.rb diff --git a/lib/bitbucket/client.rb b/lib/bitbucket/client.rb index c05fc35f36e..39b52ae25a6 100644 --- a/lib/bitbucket/client.rb +++ b/lib/bitbucket/client.rb @@ -4,9 +4,19 @@ module Bitbucket @connection = options.fetch(:connection, Connection.new(options)) end + + def repos + relative_path = "/repositories/#{user.username}" + paginator = Paginator.new(connection, relative_path, :repo) + + Collection.new(paginator) + end + def user - parsed_response = connection.get('/user') - Representation::User.new(parsed_response) + @user ||= begin + parsed_response = connection.get('/user') + Representation::User.new(parsed_response) + end end private diff --git a/lib/bitbucket/representation/repo.rb b/lib/bitbucket/representation/repo.rb new file mode 100644 index 00000000000..fe5cda66ab9 --- /dev/null +++ b/lib/bitbucket/representation/repo.rb @@ -0,0 +1,57 @@ +module Bitbucket + module Representation + class Repo < Representation::Base + attr_reader :owner, :slug + + def initialize(raw) + super(raw) + + if full_name && full_name.split('/').size == 2 + @owner, @slug = full_name.split('/') + end + end + + def clone_url(token = nil) + url = raw['links']['clone'].find { |link| link['name'] == 'https' }.fetch('href') + + if token.present? + url.sub(/^[^\@]*/, "https://x-token-auth:#{token}") + else + url + end + end + + def description + raw['description'] + end + + def full_name + raw['full_name'] + end + + def has_issues? + raw['has_issues'] + end + + def name + raw['name'] + end + + def valid? + raw['scm'] == 'git' + end + + def visibility_level + if raw['is_private'] + Gitlab::VisibilityLevel::PRIVATE + else + Gitlab::VisibilityLevel::PUBLIC + end + end + + def to_s + full_name + end + end + end +end -- cgit v1.2.1 From 411d0a93723467eb355155fe52f1d159d4527556 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 22 Aug 2016 15:55:08 -0300 Subject: Add an endpoint to get a single user repository --- lib/bitbucket/client.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/bitbucket/client.rb b/lib/bitbucket/client.rb index 39b52ae25a6..24984ca0793 100644 --- a/lib/bitbucket/client.rb +++ b/lib/bitbucket/client.rb @@ -5,6 +5,11 @@ module Bitbucket end + def repo(name) + parsed_response = connection.get("/repositories/#{name}") + Representation::Repo.new(parsed_response) + end + def repos relative_path = "/repositories/#{user.username}" paginator = Paginator.new(connection, relative_path, :repo) -- cgit v1.2.1 From 56cb4762d42f758ad6e4ec1874b7eed8e1c1f687 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 22 Aug 2016 16:02:48 -0300 Subject: Refactoring Bitbucket import controller to use the new OAuth2 client --- app/controllers/import/bitbucket_controller.rb | 77 ++++++++++++++------------ app/views/import/bitbucket/status.html.haml | 35 ++++++------ lib/bitbucket/error/unauthorized.rb | 6 ++ 3 files changed, 67 insertions(+), 51 deletions(-) create mode 100644 lib/bitbucket/error/unauthorized.rb diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 6ea54744da8..ee30a24ab77 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -3,49 +3,54 @@ class Import::BitbucketController < Import::BaseController before_action :bitbucket_auth, except: :callback rescue_from OAuth::Error, with: :bitbucket_unauthorized - rescue_from Gitlab::BitbucketImport::Client::Unauthorized, with: :bitbucket_unauthorized + rescue_from Bitbucket::Error::Unauthorized, with: :bitbucket_unauthorized def callback - request_token = session.delete(:oauth_request_token) - raise "Session expired!" if request_token.nil? + response = client.auth_code.get_token(params[:code], redirect_uri: callback_import_bitbucket_url) - request_token.symbolize_keys! - - access_token = client.get_token(request_token, params[:oauth_verifier], callback_import_bitbucket_url) - - session[:bitbucket_access_token] = access_token.token - session[:bitbucket_access_token_secret] = access_token.secret + session[:bitbucket_token] = response.token + session[:bitbucket_expires_at] = response.expires_at + session[:bitbucket_expires_in] = response.expires_in + session[:bitbucket_refresh_token] = response.refresh_token redirect_to status_import_bitbucket_url end def status - @repos = client.projects - @incompatible_repos = client.incompatible_projects + client = Bitbucket::Client.new(credentials) + repos = client.repos + + @repos = repos.select(&:valid?) + @incompatible_repos = repos.reject(&:valid?) - @already_added_projects = current_user.created_projects.where(import_type: "bitbucket") + @already_added_projects = current_user.created_projects.where(import_type: 'bitbucket') already_added_projects_names = @already_added_projects.pluck(:import_source) - @repos.to_a.reject!{ |repo| already_added_projects_names.include? "#{repo["owner"]}/#{repo["slug"]}" } + @repos.to_a.reject! { |repo| already_added_projects_names.include?(repo.full_name) } end def jobs - jobs = current_user.created_projects.where(import_type: "bitbucket").to_json(only: [:id, :import_status]) - render json: jobs + render json: current_user.created_projects + .where(import_type: 'bitbucket') + .to_json(only: [:id, :import_status]) end def create + client = Bitbucket::Client.new(credentials) + @repo_id = params[:repo_id].to_s - repo = client.project(@repo_id.gsub('___', '/')) - @project_name = repo['slug'] - @target_namespace = find_or_create_namespace(repo['owner'], client.user['user']['username']) + name = @repo_id.to_s.gsub('___', '/') + repo = client.repo(name) + @project_name = repo.name - unless Gitlab::BitbucketImport::KeyAdder.new(repo, current_user, access_params).execute - render 'deploy_key' and return - end + repo_owner = repo.owner + repo_owner = current_user.username if repo_owner == client.user.username + @target_namespace = params[:new_namespace].presence || repo_owner + + namespace = find_or_create_namespace(target_namespace_name, repo_owner) if current_user.can?(:create_projects, @target_namespace) - @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute + @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, current_user, credentials).execute else render 'unauthorized' end @@ -54,8 +59,15 @@ class Import::BitbucketController < Import::BaseController private def client - @client ||= Gitlab::BitbucketImport::Client.new(session[:bitbucket_access_token], - session[:bitbucket_access_token_secret]) + @client ||= OAuth2::Client.new(provider.app_id, provider.app_secret, options) + end + + def provider + Gitlab.config.omniauth.providers.find { |provider| provider.name == 'bitbucket' } + end + + def options + OmniAuth::Strategies::Bitbucket.default_options[:client_options].deep_symbolize_keys end def verify_bitbucket_import_enabled @@ -63,26 +75,23 @@ class Import::BitbucketController < Import::BaseController end def bitbucket_auth - if session[:bitbucket_access_token].blank? - go_to_bitbucket_for_permissions - end + go_to_bitbucket_for_permissions if session[:bitbucket_token].blank? end def go_to_bitbucket_for_permissions - request_token = client.request_token(callback_import_bitbucket_url) - session[:oauth_request_token] = request_token - - redirect_to client.authorize_url(request_token, callback_import_bitbucket_url) + redirect_to client.auth_code.authorize_url(redirect_uri: callback_import_bitbucket_url) end def bitbucket_unauthorized go_to_bitbucket_for_permissions end - def access_params + def credentials { - bitbucket_access_token: session[:bitbucket_access_token], - bitbucket_access_token_secret: session[:bitbucket_access_token_secret] + token: session[:bitbucket_token], + expires_at: session[:bitbucket_expires_at], + expires_in: session[:bitbucket_expires_in], + refresh_token: session[:bitbucket_refresh_token] } end end diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index f8b4b107513..e62ff5f61f6 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -1,5 +1,6 @@ -- page_title "Bitbucket import" -- header_title "Projects", root_path +- page_title 'Bitbucket import' +- header_title 'Projects', root_path + %h3.page-title %i.fa.fa-bitbucket Import projects from Bitbucket @@ -10,13 +11,13 @@ %hr %p - if @incompatible_repos.any? - = button_tag class: "btn btn-import btn-success js-import-all" do + = button_tag class: 'btn btn-import btn-success js-import-all' do Import all compatible projects - = icon("spinner spin", class: "loading-icon") + = icon('spinner spin', class: 'loading-icon') - else - = button_tag class: "btn btn-success js-import-all" do + = button_tag class: 'btn btn-success js-import-all' do Import all projects - = icon("spinner spin", class: "loading-icon") + = icon('spinner spin', class: 'loading-icon') .table-responsive %table.table.import-jobs @@ -32,7 +33,7 @@ - @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" + = link_to project.import_source, 'https://bitbucket.org/#{project.import_source}', target: '_blank' %td = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status @@ -47,31 +48,31 @@ = project.human_import_status_name - @repos.each do |repo| - %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"} + %tr{id: "repo_#{repo.owner}___#{repo.slug}"} %td - = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank" + = link_to "#{repo.full_name}", "https://bitbucket.org/#{repo.full_name}", target: "_blank" %td.import-target - = import_project_target(repo['owner'], repo['slug']) + = "#{repo.full_name}" %td.import-actions.job-status - = button_tag class: "btn btn-import js-add-to-import" do + = button_tag class: 'btn btn-import js-add-to-import' do Import - = icon("spinner spin", class: "loading-icon") + = icon('spinner spin', class: 'loading-icon') - @incompatible_repos.each do |repo| - %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"} + %tr{id: "repo_#{repo.owner}___#{repo.slug}"} %td - = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank" + = link_to "#{repo.full_name}", "https://bitbucket.org/#{repo.full_name}", target: '_blank' %td.import-target %td.import-actions-job-status - = label_tag "Incompatible Project", nil, class: "label label-danger" + = label_tag 'Incompatible Project', nil, class: 'label label-danger' - if @incompatible_repos.any? %p One or more of your Bitbucket projects cannot be imported into GitLab directly because they use Subversion or Mercurial for version control, rather than Git. Please convert - = link_to "them to Git,", "https://www.atlassian.com/git/tutorials/migrating-overview" + = link_to 'them to Git,', 'https://www.atlassian.com/git/tutorials/migrating-overview' and go through the - = link_to "import flow", status_import_bitbucket_path, "data-no-turbolink" => "true" + = link_to 'import flow', status_import_bitbucket_path, 'data-no-turbolink' => 'true' again. .js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_path}", import_path: "#{import_bitbucket_path}" } } diff --git a/lib/bitbucket/error/unauthorized.rb b/lib/bitbucket/error/unauthorized.rb new file mode 100644 index 00000000000..5e2eb57bb0e --- /dev/null +++ b/lib/bitbucket/error/unauthorized.rb @@ -0,0 +1,6 @@ +module Bitbucket + module Error + class Unauthorized < StandardError + end + end +end -- cgit v1.2.1 From ceac7878e9b5ba56f31f605c40095e8b83d83b6f Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 22 Aug 2016 16:04:11 -0300 Subject: Clone Bitbucket repositories over HTTPS --- lib/gitlab/bitbucket_import/project_creator.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb index b90ef0b0fba..9d80c5d4f2b 100644 --- a/lib/gitlab/bitbucket_import/project_creator.rb +++ b/lib/gitlab/bitbucket_import/project_creator.rb @@ -13,15 +13,15 @@ module Gitlab def execute ::Projects::CreateService.new( current_user, - name: repo["name"], - path: repo["slug"], - description: repo["description"], + name: repo.name, + path: repo.slug, + description: repo.description, namespace_id: namespace.id, - visibility_level: repo["is_private"] ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC, - import_type: "bitbucket", - import_source: "#{repo["owner"]}/#{repo["slug"]}", - import_url: "ssh://git@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}.git", - import_data: { credentials: { bb_session: session_data } } + visibility_level: repo.visibility_level, + import_type: 'bitbucket', + import_source: repo.full_name, + import_url: repo.clone_url(@session_data[:token]), + import_data: { credentials: session_data } ).execute end end -- cgit v1.2.1 From 267e27b0cd543e8eeaa04686ad4678c4f553c479 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 22 Aug 2016 16:05:44 -0300 Subject: Remove code to clone Bitbucket repositories using SSH --- app/controllers/application_controller.rb | 2 +- config/initializers/public_key.rb | 2 -- lib/gitlab/bitbucket_import/key_adder.rb | 24 ------------------------ lib/gitlab/bitbucket_import/key_deleter.rb | 23 ----------------------- 4 files changed, 1 insertion(+), 50 deletions(-) delete mode 100644 config/initializers/public_key.rb delete mode 100644 lib/gitlab/bitbucket_import/key_adder.rb delete mode 100644 lib/gitlab/bitbucket_import/key_deleter.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 517ad4f03f3..a6b0a4af503 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -254,7 +254,7 @@ class ApplicationController < ActionController::Base end def bitbucket_import_configured? - Gitlab::OAuth::Provider.enabled?(:bitbucket) && Gitlab::BitbucketImport.public_key.present? + Gitlab::OAuth::Provider.enabled?(:bitbucket) end def google_code_import_enabled? diff --git a/config/initializers/public_key.rb b/config/initializers/public_key.rb deleted file mode 100644 index e4f09a2d020..00000000000 --- a/config/initializers/public_key.rb +++ /dev/null @@ -1,2 +0,0 @@ -path = File.expand_path("~/.ssh/bitbucket_rsa.pub") -Gitlab::BitbucketImport.public_key = File.read(path) if File.exist?(path) diff --git a/lib/gitlab/bitbucket_import/key_adder.rb b/lib/gitlab/bitbucket_import/key_adder.rb deleted file mode 100644 index 0b63f025d0a..00000000000 --- a/lib/gitlab/bitbucket_import/key_adder.rb +++ /dev/null @@ -1,24 +0,0 @@ -module Gitlab - module BitbucketImport - class KeyAdder - attr_reader :repo, :current_user, :client - - def initialize(repo, current_user, access_params) - @repo, @current_user = repo, current_user - @client = Client.new(access_params[:bitbucket_access_token], - access_params[:bitbucket_access_token_secret]) - end - - def execute - return false unless BitbucketImport.public_key.present? - - project_identifier = "#{repo["owner"]}/#{repo["slug"]}" - client.add_deploy_key(project_identifier, BitbucketImport.public_key) - - true - rescue - false - end - end - end -end diff --git a/lib/gitlab/bitbucket_import/key_deleter.rb b/lib/gitlab/bitbucket_import/key_deleter.rb deleted file mode 100644 index e03c3155b3e..00000000000 --- a/lib/gitlab/bitbucket_import/key_deleter.rb +++ /dev/null @@ -1,23 +0,0 @@ -module Gitlab - module BitbucketImport - class KeyDeleter - attr_reader :project, :current_user, :client - - def initialize(project) - @project = project - @current_user = project.creator - @client = Client.from_project(@project) - end - - def execute - return false unless BitbucketImport.public_key.present? - - client.delete_deploy_key(project.import_source, BitbucketImport.public_key) - - true - rescue - false - end - end - end -end -- cgit v1.2.1 From 3dd15d3f753a5a71522275a37393bfa56d6e3517 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 22 Aug 2016 16:09:25 -0300 Subject: Add an endpoint to get a list of issues for a repo --- lib/bitbucket/client.rb | 7 +++++ lib/bitbucket/representation/issue.rb | 49 +++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 lib/bitbucket/representation/issue.rb diff --git a/lib/bitbucket/client.rb b/lib/bitbucket/client.rb index 24984ca0793..ac6e91bb526 100644 --- a/lib/bitbucket/client.rb +++ b/lib/bitbucket/client.rb @@ -4,6 +4,13 @@ module Bitbucket @connection = options.fetch(:connection, Connection.new(options)) end + def issues(repo) + relative_path = "/repositories/#{repo}/issues" + paginator = Paginator.new(connection, relative_path, :issue) + + Collection.new(paginator) + end + def repo(name) parsed_response = connection.get("/repositories/#{name}") diff --git a/lib/bitbucket/representation/issue.rb b/lib/bitbucket/representation/issue.rb new file mode 100644 index 00000000000..48647ad51f6 --- /dev/null +++ b/lib/bitbucket/representation/issue.rb @@ -0,0 +1,49 @@ +module Bitbucket + module Representation + class Issue < Representation::Base + CLOSED_STATUS = %w(resolved invalid duplicate wontfix closed).freeze + + def iid + raw['id'] + end + + def author + reporter.fetch('username', 'Anonymous') + end + + def description + raw.dig('content', 'raw') + end + + def state + closed? ? 'closed' : 'opened' + end + + def title + raw['title'] + end + + def created_at + raw['created_on'] + end + + def updated_at + raw['edited_on'] + end + + def to_s + iid + end + + private + + def closed? + CLOSED_STATUS.include?(raw['state']) + end + + def reporter + raw.fetch('reporter', {}) + end + end + end +end -- cgit v1.2.1 From 3f59d25d263d1ac9db76cd2d3d4d025fb6d6dff4 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 22 Aug 2016 16:10:29 -0300 Subject: Add an endpoint to get a list of issue comments --- lib/bitbucket/client.rb | 10 ++++++++++ lib/bitbucket/representation/comment.rb | 27 +++++++++++++++++++++++++++ lib/bitbucket/representation/url.rb | 9 +++++++++ 3 files changed, 46 insertions(+) create mode 100644 lib/bitbucket/representation/comment.rb create mode 100644 lib/bitbucket/representation/url.rb diff --git a/lib/bitbucket/client.rb b/lib/bitbucket/client.rb index ac6e91bb526..3d22347603d 100644 --- a/lib/bitbucket/client.rb +++ b/lib/bitbucket/client.rb @@ -11,6 +11,16 @@ module Bitbucket Collection.new(paginator) end + def issue_comments(repo, number) + relative_path = "/repositories/#{repo}/issues/#{number}/comments" + paginator = Paginator.new(connection, relative_path, :url) + + Collection.new(paginator).map do |comment_url| + parsed_response = connection.get(comment_url.to_s) + Representation::Comment.new(parsed_response) + end + end + def repo(name) parsed_response = connection.get("/repositories/#{name}") diff --git a/lib/bitbucket/representation/comment.rb b/lib/bitbucket/representation/comment.rb new file mode 100644 index 00000000000..94bc18cbfab --- /dev/null +++ b/lib/bitbucket/representation/comment.rb @@ -0,0 +1,27 @@ +module Bitbucket + module Representation + class Comment < Representation::Base + def author + user.fetch('username', 'Anonymous') + end + + def note + raw.dig('content', 'raw') + end + + def created_at + raw['created_on'] + end + + def updated_at + raw['updated_on'] || raw['created_on'] + end + + private + + def user + raw.fetch('user', {}) + end + end + end +end diff --git a/lib/bitbucket/representation/url.rb b/lib/bitbucket/representation/url.rb new file mode 100644 index 00000000000..24ae1048013 --- /dev/null +++ b/lib/bitbucket/representation/url.rb @@ -0,0 +1,9 @@ +module Bitbucket + module Representation + class Url < Representation::Base + def to_s + raw.dig('links', 'self', 'href') + end + end + end +end -- cgit v1.2.1 From 3c756b83ef04dbbb2a82a53cf785a87da0772255 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 22 Aug 2016 16:11:45 -0300 Subject: Remove client for the Bitbucket API version 1.0 --- lib/gitlab/bitbucket_import/client.rb | 142 ---------------------------------- 1 file changed, 142 deletions(-) delete mode 100644 lib/gitlab/bitbucket_import/client.rb diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb deleted file mode 100644 index 8d1ad62fae0..00000000000 --- a/lib/gitlab/bitbucket_import/client.rb +++ /dev/null @@ -1,142 +0,0 @@ -module Gitlab - module BitbucketImport - class Client - class Unauthorized < StandardError; end - - attr_reader :consumer, :api - - def self.from_project(project) - import_data_credentials = project.import_data.credentials if project.import_data - if import_data_credentials && import_data_credentials[:bb_session] - token = import_data_credentials[:bb_session][:bitbucket_access_token] - token_secret = import_data_credentials[:bb_session][:bitbucket_access_token_secret] - new(token, token_secret) - else - raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{project.id}" - end - end - - def initialize(access_token = nil, access_token_secret = nil) - @consumer = ::OAuth::Consumer.new( - config.app_id, - config.app_secret, - bitbucket_options - ) - - if access_token && access_token_secret - @api = ::OAuth::AccessToken.new(@consumer, access_token, access_token_secret) - end - end - - def request_token(redirect_uri) - request_token = consumer.get_request_token(oauth_callback: redirect_uri) - - { - oauth_token: request_token.token, - oauth_token_secret: request_token.secret, - oauth_callback_confirmed: request_token.callback_confirmed?.to_s - } - end - - def authorize_url(request_token, redirect_uri) - request_token = ::OAuth::RequestToken.from_hash(consumer, request_token) if request_token.is_a?(Hash) - - if request_token.callback_confirmed? - request_token.authorize_url - else - request_token.authorize_url(oauth_callback: redirect_uri) - end - end - - def get_token(request_token, oauth_verifier, redirect_uri) - request_token = ::OAuth::RequestToken.from_hash(consumer, request_token) if request_token.is_a?(Hash) - - if request_token.callback_confirmed? - request_token.get_access_token(oauth_verifier: oauth_verifier) - else - request_token.get_access_token(oauth_callback: redirect_uri) - end - end - - def user - JSON.parse(get("/api/1.0/user").body) - end - - def issues(project_identifier) - all_issues = [] - offset = 0 - per_page = 50 # Maximum number allowed by Bitbucket - index = 0 - - begin - issues = JSON.parse(get(issue_api_endpoint(project_identifier, per_page, offset)).body) - # Find out how many total issues are present - total = issues["count"] if index == 0 - all_issues.concat(issues["issues"]) - offset += issues["issues"].count - index += 1 - end while all_issues.count < total - - all_issues - end - - def issue_comments(project_identifier, issue_id) - comments = JSON.parse(get("/api/1.0/repositories/#{project_identifier}/issues/#{issue_id}/comments").body) - comments.sort_by { |comment| comment["utc_created_on"] } - end - - def project(project_identifier) - JSON.parse(get("/api/1.0/repositories/#{project_identifier}").body) - end - - def find_deploy_key(project_identifier, key) - JSON.parse(get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find do |deploy_key| - deploy_key["key"].chomp == key.chomp - end - end - - def add_deploy_key(project_identifier, key) - deploy_key = find_deploy_key(project_identifier, key) - return if deploy_key - - JSON.parse(api.post("/api/1.0/repositories/#{project_identifier}/deploy-keys", key: key, label: "GitLab import key").body) - end - - def delete_deploy_key(project_identifier, key) - deploy_key = find_deploy_key(project_identifier, key) - return unless deploy_key - - api.delete("/api/1.0/repositories/#{project_identifier}/deploy-keys/#{deploy_key["pk"]}").code == "204" - end - - def projects - JSON.parse(get("/api/1.0/user/repositories").body).select { |repo| repo["scm"] == "git" } - end - - def incompatible_projects - JSON.parse(get("/api/1.0/user/repositories").body).reject { |repo| repo["scm"] == "git" } - end - - private - - def get(url) - response = api.get(url) - raise Unauthorized if (400..499).cover?(response.code.to_i) - - response - end - - def issue_api_endpoint(project_identifier, per_page, offset) - "/api/1.0/repositories/#{project_identifier}/issues?sort=utc_created_on&limit=#{per_page}&start=#{offset}" - end - - def config - Gitlab.config.omniauth.providers.find { |provider| provider.name == "bitbucket" } - end - - def bitbucket_options - OmniAuth::Strategies::Bitbucket.default_options[:client_options].symbolize_keys - end - end - end -end -- cgit v1.2.1 From 317b020932736d2cd629542e3a8b3aef2219e033 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 22 Aug 2016 16:13:56 -0300 Subject: Refactoring Bitbucket importer to use the new OAuth2 client --- lib/gitlab/bitbucket_import/importer.rb | 74 +++++++++++++++------------------ 1 file changed, 33 insertions(+), 41 deletions(-) diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index f4b5097adb1..67e906431f0 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -5,18 +5,14 @@ module Gitlab def initialize(project) @project = project - @client = Client.from_project(@project) + @client = Bitbucket::Client.new(project.import_data.credentials) @formatter = Gitlab::ImportFormatter.new end def execute - import_issues if has_issues? + import_issues true - rescue ActiveRecord::RecordInvalid => e - raise Projects::ImportService::Error.new, e.message - ensure - Gitlab::BitbucketImport::KeyDeleter.new(project).execute end private @@ -30,44 +26,40 @@ module Gitlab end end - def identifier - project.import_source - end - - def has_issues? - client.project(identifier)["has_issues"] + def repo + @repo ||= client.repo(project.import_source) end def import_issues - issues = client.issues(identifier) - - issues.each do |issue| - body = '' - reporter = nil - author = 'Anonymous' - - if issue["reported_by"] && issue["reported_by"]["username"] - reporter = issue["reported_by"]["username"] - author = reporter - end - - body = @formatter.author_line(author) - body += issue["content"] - - comments = client.issue_comments(identifier, issue["local_id"]) - - if comments.any? - body += @formatter.comments_header - end - - comments.each do |comment| - author = 'Anonymous' + return unless repo.has_issues? + + client.issues(repo).each do |issue| + description = @formatter.author_line(issue.author) + description += issue.description + + issue = project.issues.create( + iid: issue.iid, + title: issue.title, + description: description, + state: issue.state, + author_id: gl_user_id(project, issue.author), + created_at: issue.created_at, + updated_at: issue.updated_at + ) - if comment["author_info"] && comment["author_info"]["username"] - author = comment["author_info"]["username"] + if issue.persisted? + client.issue_comments(repo, issue.iid).each do |comment| + note = @formatter.author_line(comment.author) + note += comment.note + + issue.notes.create!( + project: project, + note: note, + author_id: gl_user_id(project, comment.author), + created_at: comment.created_at, + updated_at: comment.updated_at + ) end - - body += @formatter.comment(author, comment["utc_created_on"], comment["content"]) end project.issues.create!( @@ -77,8 +69,8 @@ module Gitlab author_id: gitlab_user_id(project, reporter) ) end - rescue ActiveRecord::RecordInvalid => e - raise Projects::ImportService::Error, e.message + rescue ActiveRecord::RecordInvalid + nil end end end -- cgit v1.2.1 From 64722a15e39436820a0636804179cf8c8957197e Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 22 Aug 2016 16:15:15 -0300 Subject: Add an endpoint to get a list of pull requests for a repo --- lib/bitbucket/client.rb | 6 ++++++ lib/bitbucket/representation/pull_request.rb | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 lib/bitbucket/representation/pull_request.rb diff --git a/lib/bitbucket/client.rb b/lib/bitbucket/client.rb index 3d22347603d..0368f388ea4 100644 --- a/lib/bitbucket/client.rb +++ b/lib/bitbucket/client.rb @@ -21,6 +21,12 @@ module Bitbucket end end + def pull_requests(repo) + relative_path = "/repositories/#{repo}/pullrequests" + paginator = Paginator.new(connection, relative_path, :pull_request) + + Collection.new(paginator) + end def repo(name) parsed_response = connection.get("/repositories/#{name}") diff --git a/lib/bitbucket/representation/pull_request.rb b/lib/bitbucket/representation/pull_request.rb new file mode 100644 index 00000000000..7cbad91e9c8 --- /dev/null +++ b/lib/bitbucket/representation/pull_request.rb @@ -0,0 +1,6 @@ +module Bitbucket + module Representation + class PullRequest < Representation::Base + end + end +end -- cgit v1.2.1 From 704115c726999d6f0467bbf70087db3ae690d3ab Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 22 Aug 2016 16:15:39 -0300 Subject: Import opened pull request from Bitbucket --- lib/bitbucket/representation/pull_request.rb | 59 ++++++++++++++++++++++++++++ lib/gitlab/bitbucket_import/importer.rb | 31 +++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/lib/bitbucket/representation/pull_request.rb b/lib/bitbucket/representation/pull_request.rb index 7cbad91e9c8..e7b1f99e9a6 100644 --- a/lib/bitbucket/representation/pull_request.rb +++ b/lib/bitbucket/representation/pull_request.rb @@ -1,6 +1,65 @@ module Bitbucket module Representation class PullRequest < Representation::Base + def author + raw.fetch('author', {}).fetch('username', 'Anonymous') + end + + def description + raw['description'] + end + + def iid + raw['id'] + end + + def state + if raw['state'] == 'MERGED' + 'merged' + elsif raw['state'] == 'DECLINED' + 'closed' + else + 'opened' + end + end + + def created_at + raw['created_on'] + end + + def updated_at + raw['updated_on'] + end + + def title + raw['title'] + end + + def source_branch_name + source_branch.dig('branch', 'name') + end + + def source_branch_sha + source_branch.dig('commit', 'hash') + end + + def target_branch_name + target_branch.dig('branch', 'name') + end + + def target_branch_sha + target_branch.dig('commit', 'hash') + end + + private + + def source_branch + raw['source'] + end + + def target_branch + raw['destination'] + end end end end diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 67e906431f0..0060e350249 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -11,6 +11,7 @@ module Gitlab def execute import_issues + import_pull_requests true end @@ -72,6 +73,36 @@ module Gitlab rescue ActiveRecord::RecordInvalid nil end + + def import_pull_requests + pull_requests = client.pull_requests(repo) + + pull_requests.each do |pull_request| + begin + description = @formatter.author_line(pull_request.author) + description += pull_request.description + + project.merge_requests.create( + iid: pull_request.iid, + title: pull_request.title, + description: description, + source_project: project, + source_branch: pull_request.source_branch_name, + source_branch_sha: pull_request.source_branch_sha, + target_project: project, + target_branch: pull_request.target_branch_name, + target_branch_sha: pull_request.target_branch_sha, + state: pull_request.state, + author_id: gl_user_id(project, pull_request.author), + assignee_id: nil, + created_at: pull_request.created_at, + updated_at: pull_request.updated_at + ) + rescue ActiveRecord::RecordInvalid + nil + end + end + end end end end -- cgit v1.2.1 From a0959430516f57ad27df21447777ebb226890647 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 11 Nov 2016 14:00:33 -0800 Subject: Fix rebase failures with Bitbucket changes --- app/controllers/import/bitbucket_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index ee30a24ab77..5326dce4ebb 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -47,9 +47,9 @@ class Import::BitbucketController < Import::BaseController repo_owner = current_user.username if repo_owner == client.user.username @target_namespace = params[:new_namespace].presence || repo_owner - namespace = find_or_create_namespace(target_namespace_name, repo_owner) + namespace = find_or_create_namespace(@target_namespace, repo_owner) - if current_user.can?(:create_projects, @target_namespace) + if current_user.can?(:create_projects, namespace) @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, current_user, credentials).execute else render 'unauthorized' -- cgit v1.2.1 From 478730bebd5c8a9505490d2b396ac3c866da1b09 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 11 Nov 2016 14:44:31 -0800 Subject: Support selection of different namespace and project destination --- app/controllers/import/bitbucket_controller.rb | 6 +++--- app/views/import/bitbucket/status.html.haml | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 5326dce4ebb..e7150cb8e95 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -39,9 +39,9 @@ class Import::BitbucketController < Import::BaseController client = Bitbucket::Client.new(credentials) @repo_id = params[:repo_id].to_s - name = @repo_id.to_s.gsub('___', '/') + name = @repo_id.gsub('___', '/') repo = client.repo(name) - @project_name = repo.name + @project_name = params[:new_name].presence || repo.name repo_owner = repo.owner repo_owner = current_user.username if repo_owner == client.user.username @@ -50,7 +50,7 @@ class Import::BitbucketController < Import::BaseController namespace = find_or_create_namespace(@target_namespace, repo_owner) if current_user.can?(:create_projects, namespace) - @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, current_user, credentials).execute + @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @project_name, namespace, current_user, credentials).execute else render 'unauthorized' end diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index e62ff5f61f6..cc262e97ceb 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -15,7 +15,7 @@ Import all compatible projects = icon('spinner spin', class: 'loading-icon') - else - = button_tag class: 'btn btn-success js-import-all' do + = button_tag class: 'btn btn-import btn-success js-import-all' do Import all projects = icon('spinner spin', class: 'loading-icon') @@ -52,7 +52,17 @@ %td = link_to "#{repo.full_name}", "https://bitbucket.org/#{repo.full_name}", target: "_blank" %td.import-target - = "#{repo.full_name}" + %fieldset.row + .input-group + .project-path.input-group-btn + - if current_user.can_select_namespace? + - selected = params[:namespace_id] || :current_user + - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner, path: repo.owner) } : {} + = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } + - else + = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true + %span.input-group-addon / + = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true %td.import-actions.job-status = button_tag class: 'btn btn-import js-add-to-import' do Import -- cgit v1.2.1 From e2688feeb3075265fb926bbd68560b2046afa0c5 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 11 Nov 2016 15:44:52 -0800 Subject: Address initial review comments --- app/controllers/import/bitbucket_controller.rb | 13 ++++++------- app/views/import/bitbucket/status.html.haml | 6 +++--- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index e7150cb8e95..72c90f9daf2 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -17,11 +17,10 @@ class Import::BitbucketController < Import::BaseController end def status - client = Bitbucket::Client.new(credentials) - repos = client.repos + bitbucket_client = Bitbucket::Client.new(credentials) + repos = bitbucket_client.repos - @repos = repos.select(&:valid?) - @incompatible_repos = repos.reject(&:valid?) + @repos, @incompatible_repos = repos.partition { |repo| repo.valid? } @already_added_projects = current_user.created_projects.where(import_type: 'bitbucket') already_added_projects_names = @already_added_projects.pluck(:import_source) @@ -36,15 +35,15 @@ class Import::BitbucketController < Import::BaseController end def create - client = Bitbucket::Client.new(credentials) + bitbucket_client = Bitbucket::Client.new(credentials) @repo_id = params[:repo_id].to_s name = @repo_id.gsub('___', '/') - repo = client.repo(name) + repo = bitbucket_client.repo(name) @project_name = params[:new_name].presence || repo.name repo_owner = repo.owner - repo_owner = current_user.username if repo_owner == client.user.username + repo_owner = current_user.username if repo_owner == bitbucket_client.user.username @target_namespace = params[:new_namespace].presence || repo_owner namespace = find_or_create_namespace(@target_namespace, repo_owner) diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index cc262e97ceb..ac09b71ae89 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -33,7 +33,7 @@ - @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' + = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank' %td = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status @@ -50,7 +50,7 @@ - @repos.each do |repo| %tr{id: "repo_#{repo.owner}___#{repo.slug}"} %td - = link_to "#{repo.full_name}", "https://bitbucket.org/#{repo.full_name}", target: "_blank" + = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: "_blank" %td.import-target %fieldset.row .input-group @@ -70,7 +70,7 @@ - @incompatible_repos.each do |repo| %tr{id: "repo_#{repo.owner}___#{repo.slug}"} %td - = link_to "#{repo.full_name}", "https://bitbucket.org/#{repo.full_name}", target: '_blank' + = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank' %td.import-target %td.import-actions-job-status = label_tag 'Incompatible Project', nil, class: 'label label-danger' -- cgit v1.2.1 From 9860488360681a3b10c3de04606ef931c3639601 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 11 Nov 2016 16:07:16 -0800 Subject: Fix missing Bitbucket ProjectController changes for specifying namespace/project --- lib/gitlab/bitbucket_import/project_creator.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb index 9d80c5d4f2b..b34be272af3 100644 --- a/lib/gitlab/bitbucket_import/project_creator.rb +++ b/lib/gitlab/bitbucket_import/project_creator.rb @@ -1,10 +1,11 @@ module Gitlab module BitbucketImport class ProjectCreator - attr_reader :repo, :namespace, :current_user, :session_data + attr_reader :repo, :name, :namespace, :current_user, :session_data - def initialize(repo, namespace, current_user, session_data) + def initialize(repo, name, namespace, current_user, session_data) @repo = repo + @name = name @namespace = namespace @current_user = current_user @session_data = session_data @@ -13,8 +14,8 @@ module Gitlab def execute ::Projects::CreateService.new( current_user, - name: repo.name, - path: repo.slug, + name: name, + path: name, description: repo.description, namespace_id: namespace.id, visibility_level: repo.visibility_level, -- cgit v1.2.1 From b25ebfe67b0e5448e9625e7a5c469ab41a4b7059 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 11 Nov 2016 16:08:03 -0800 Subject: Lazily load Bitbucket connection --- lib/bitbucket/connection.rb | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/bitbucket/connection.rb b/lib/bitbucket/connection.rb index 00f127f9507..e9cbf36a44b 100644 --- a/lib/bitbucket/connection.rb +++ b/lib/bitbucket/connection.rb @@ -13,13 +13,18 @@ module Bitbucket @expires_at = options.fetch(:expires_at) @expires_in = options.fetch(:expires_in) @refresh_token = options.fetch(:refresh_token) + end - @client = OAuth2::Client.new(provider.app_id, provider.app_secret, options) - @connection = OAuth2::AccessToken.new(@client, @token, refresh_token: @refresh_token, expires_at: @expires_at, expires_in: @expires_in) + def client + @client ||= OAuth2::Client.new(provider.app_id, provider.app_secret, options) + end + + def connection + @connection ||= OAuth2::AccessToken.new(client, @token, refresh_token: @refresh_token, expires_at: @expires_at, expires_in: @expires_in) end def query(params = {}) - @query.update(params) + @query.merge!(params) end def get(path, query = {}) @@ -46,7 +51,7 @@ module Bitbucket private - attr_reader :connection, :expires_at, :expires_in, :refresh_token, :token + attr_reader :expires_at, :expires_in, :refresh_token, :token def build_url(path) return path if path.starts_with?(root_url) -- cgit v1.2.1 From 3b9d1c0f5d2996b946ab71704995b752c0ff8f60 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 11 Nov 2016 16:10:12 -0800 Subject: Fix refresh to lazily load connection --- lib/bitbucket/connection.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/bitbucket/connection.rb b/lib/bitbucket/connection.rb index e9cbf36a44b..201e7e3b808 100644 --- a/lib/bitbucket/connection.rb +++ b/lib/bitbucket/connection.rb @@ -45,8 +45,7 @@ module Bitbucket @expires_at = response.expires_at @expires_in = response.expires_in @refresh_token = response.refresh_token - - @connection = OAuth2::AccessToken.new(@client, @token, refresh_token: @refresh_token, expires_at: @expires_at, expires_in: @expires_in) + @connection = nil end private -- cgit v1.2.1 From 82d7a3a3dd61c70c87a8a4a116b2ce6c0612de59 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 11 Nov 2016 16:33:28 -0800 Subject: Fix typos in pull requests failing to import --- lib/gitlab/bitbucket_import/importer.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 0060e350249..15cc64dd7f4 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -43,7 +43,7 @@ module Gitlab title: issue.title, description: description, state: issue.state, - author_id: gl_user_id(project, issue.author), + author_id: gitlab_user_id(project, issue.author), created_at: issue.created_at, updated_at: issue.updated_at ) @@ -56,7 +56,7 @@ module Gitlab issue.notes.create!( project: project, note: note, - author_id: gl_user_id(project, comment.author), + author_id: gitlab_user_id(project, comment.author), created_at: comment.created_at, updated_at: comment.updated_at ) @@ -93,7 +93,7 @@ module Gitlab target_branch: pull_request.target_branch_name, target_branch_sha: pull_request.target_branch_sha, state: pull_request.state, - author_id: gl_user_id(project, pull_request.author), + author_id: gitlab_user_id(project, pull_request.author), assignee_id: nil, created_at: pull_request.created_at, updated_at: pull_request.updated_at -- cgit v1.2.1 From 489d241c8d68ed527fccb73a1f7e46e9a567c971 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 11 Nov 2016 16:48:04 -0800 Subject: Incorporate review comments --- lib/bitbucket/page.rb | 3 +-- lib/omniauth/strategies/bitbucket.rb | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/bitbucket/page.rb b/lib/bitbucket/page.rb index ad9a2baba36..0e8ce11aa1d 100644 --- a/lib/bitbucket/page.rb +++ b/lib/bitbucket/page.rb @@ -29,8 +29,7 @@ module Bitbucket end def representation_class(type) - class_name = "Bitbucket::Representation::#{type.to_s.camelize}" - class_name.constantize + class_name = Bitbucket::Representation.const_get(type.to_s.camelize) end end end diff --git a/lib/omniauth/strategies/bitbucket.rb b/lib/omniauth/strategies/bitbucket.rb index 2006e7582ce..c5484c59c47 100644 --- a/lib/omniauth/strategies/bitbucket.rb +++ b/lib/omniauth/strategies/bitbucket.rb @@ -32,7 +32,7 @@ module OmniAuth end def primary_email - primary = emails.find{ |i| i['is_primary'] && i['is_confirmed'] } + primary = emails.find { |i| i['is_primary'] && i['is_confirmed'] } primary && primary['email'] || nil end -- cgit v1.2.1 From ea393e6f308e5dcdd5c48433285594db0539b203 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 16 Nov 2016 00:13:17 -0800 Subject: Import pull request comments --- lib/bitbucket/client.rb | 7 +++ .../representation/pull_request_comment.rb | 39 ++++++++++++ lib/gitlab/bitbucket_import/importer.rb | 70 +++++++++++++++++++++- 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 lib/bitbucket/representation/pull_request_comment.rb diff --git a/lib/bitbucket/client.rb b/lib/bitbucket/client.rb index 0368f388ea4..fce1c898030 100644 --- a/lib/bitbucket/client.rb +++ b/lib/bitbucket/client.rb @@ -28,6 +28,13 @@ module Bitbucket Collection.new(paginator) end + def pull_request_comments(repo, pull_request) + relative_path = "/repositories/#{repo}/pullrequests/#{pull_request}/comments" + paginator = Paginator.new(connection, relative_path, :pull_request_comment) + + Collection.new(paginator) + end + def repo(name) parsed_response = connection.get("/repositories/#{name}") Representation::Repo.new(parsed_response) diff --git a/lib/bitbucket/representation/pull_request_comment.rb b/lib/bitbucket/representation/pull_request_comment.rb new file mode 100644 index 00000000000..94719edbf38 --- /dev/null +++ b/lib/bitbucket/representation/pull_request_comment.rb @@ -0,0 +1,39 @@ +module Bitbucket + module Representation + class PullRequestComment < Comment + def iid + raw['id'] + end + + def file_path + inline.fetch('path', nil) + end + + def old_pos + inline.fetch('from', nil) || 1 + end + + def new_pos + inline.fetch('to', nil) || 1 + end + + def parent_id + raw.fetch('parent', {}).fetch('id', nil) + end + + def inline? + raw.has_key?('inline') + end + + def has_parent? + raw.has_key?('parent') + end + + private + + def inline + raw.fetch('inline', {}) + end + end + end +end diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 15cc64dd7f4..94e8062e447 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -82,7 +82,7 @@ module Gitlab description = @formatter.author_line(pull_request.author) description += pull_request.description - project.merge_requests.create( + merge_request = project.merge_requests.create( iid: pull_request.iid, title: pull_request.title, description: description, @@ -98,11 +98,79 @@ module Gitlab created_at: pull_request.created_at, updated_at: pull_request.updated_at ) + + import_pull_request_comments(pull_request, merge_request) if merge_request.persisted? rescue ActiveRecord::RecordInvalid nil end end end + + def import_pull_request_comments(pull_request, merge_request) + comments = client.pull_request_comments(repo, pull_request.iid) + + inline_comments, pr_comments = comments.partition(&:inline?) + + import_inline_comments(inline_comments, pull_request, merge_request) + import_standalone_pr_comments(pr_comments, merge_request) + end + + def import_inline_comments(inline_comments, pull_request, merge_request) + line_code_map = {} + + children, parents = inline_comments.partition(&:has_parent?) + + # The Bitbucket API returns threaded replies as parent-child + # relationships. We assume that the child can appear in any order in + # the JSON. + parents.each do |comment| + line_code_map[comment.iid] = generate_line_code(comment) + end + + children.each do |comment| + line_code_map[comment.iid] = line_code_map.fetch(comment.parent_id, nil) + end + + inline_comments.each do |comment| + begin + attributes = pull_request_comment_attributes(comment) + attributes.merge!( + commit_id: pull_request.source_branch_sha, + line_code: line_code_map.fetch(comment.iid), + type: 'LegacyDiffNote') + + note = merge_request.notes.create!(attributes) + rescue ActiveRecord::RecordInvalid => e + Rails.log.error("Bitbucket importer ERROR: Invalid pull request comment #{e.message}") + nil + end + end + end + + def import_standalone_pr_comments(pr_comments, merge_request) + pr_comments.each do |comment| + begin + merge_request.notes.create!(pull_request_comment_attributes(comment)) + rescue ActiveRecord::RecordInvalid => e + Rails.log.error("Bitbucket importer ERROR: Invalid standalone pull request comment #{e.message}") + nil + end + end + end + + def generate_line_code(pr_comment) + Gitlab::Diff::LineCode.generate(pr_comment.file_path, pr_comment.new_pos, pr_comment.old_pos) + end + + def pull_request_comment_attributes(comment) + { + project: project, + note: comment.note, + author_id: gitlab_user_id(project, comment.author), + created_at: comment.created_at, + updated_at: comment.updated_at + } + end end end end -- cgit v1.2.1 From f25d64d41ae16b96c1381068112717be5b4a1552 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 16 Nov 2016 22:59:59 -0800 Subject: Remove no longer used BitbucketImport::Client spec --- spec/lib/gitlab/bitbucket_import/client_spec.rb | 67 ------------------------- 1 file changed, 67 deletions(-) delete mode 100644 spec/lib/gitlab/bitbucket_import/client_spec.rb diff --git a/spec/lib/gitlab/bitbucket_import/client_spec.rb b/spec/lib/gitlab/bitbucket_import/client_spec.rb deleted file mode 100644 index 7543c29bcc4..00000000000 --- a/spec/lib/gitlab/bitbucket_import/client_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'spec_helper' - -describe Gitlab::BitbucketImport::Client, lib: true do - include ImportSpecHelper - - let(:token) { '123456' } - let(:secret) { 'secret' } - let(:client) { Gitlab::BitbucketImport::Client.new(token, secret) } - - before do - stub_omniauth_provider('bitbucket') - end - - it 'all OAuth client options are symbols' do - client.consumer.options.keys.each do |key| - expect(key).to be_kind_of(Symbol) - end - end - - context 'issues' do - let(:per_page) { 50 } - let(:count) { 95 } - let(:sample_issues) do - issues = [] - - count.times do |i| - issues << { local_id: i } - end - - issues - end - let(:first_sample_data) { { count: count, issues: sample_issues[0..per_page - 1] } } - let(:second_sample_data) { { count: count, issues: sample_issues[per_page..count] } } - let(:project_id) { 'namespace/repo' } - - it 'retrieves issues over a number of pages' do - stub_request(:get, - "https://bitbucket.org/api/1.0/repositories/#{project_id}/issues?limit=50&sort=utc_created_on&start=0"). - to_return(status: 200, - body: first_sample_data.to_json, - headers: {}) - - stub_request(:get, - "https://bitbucket.org/api/1.0/repositories/#{project_id}/issues?limit=50&sort=utc_created_on&start=50"). - to_return(status: 200, - body: second_sample_data.to_json, - headers: {}) - - issues = client.issues(project_id) - expect(issues.count).to eq(95) - end - end - - context 'project import' do - it 'calls .from_project with no errors' do - project = create(:empty_project) - project.import_url = "ssh://git@bitbucket.org/test/test.git" - project.create_or_update_import_data(credentials: - { user: "git", - password: nil, - bb_session: { bitbucket_access_token: "test", - bitbucket_access_token_secret: "test" } }) - - expect { described_class.from_project(project) }.not_to raise_error - end - end -end -- cgit v1.2.1 From b8bf28348fb903c62e084353896873438f4f0845 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 17 Nov 2016 20:16:22 -0800 Subject: Rubocop fixes --- lib/bitbucket/page.rb | 2 +- lib/gitlab/bitbucket_import/importer.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/bitbucket/page.rb b/lib/bitbucket/page.rb index 0e8ce11aa1d..bc51ce7dce2 100644 --- a/lib/bitbucket/page.rb +++ b/lib/bitbucket/page.rb @@ -29,7 +29,7 @@ module Bitbucket end def representation_class(type) - class_name = Bitbucket::Representation.const_get(type.to_s.camelize) + Bitbucket::Representation.const_get(type.to_s.camelize) end end end diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 94e8062e447..1f7a691e6dd 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -139,7 +139,7 @@ module Gitlab line_code: line_code_map.fetch(comment.iid), type: 'LegacyDiffNote') - note = merge_request.notes.create!(attributes) + merge_request.notes.create!(attributes) rescue ActiveRecord::RecordInvalid => e Rails.log.error("Bitbucket importer ERROR: Invalid pull request comment #{e.message}") nil -- cgit v1.2.1 From 9e6b25d0bc5c2f88330bb074db242017ea45f90d Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 17 Nov 2016 21:30:35 -0800 Subject: Add support for extracting all pull requests and their raw diffs --- lib/bitbucket/client.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/bitbucket/client.rb b/lib/bitbucket/client.rb index fce1c898030..0d4cfd600b8 100644 --- a/lib/bitbucket/client.rb +++ b/lib/bitbucket/client.rb @@ -22,7 +22,7 @@ module Bitbucket end def pull_requests(repo) - relative_path = "/repositories/#{repo}/pullrequests" + relative_path = "/repositories/#{repo}/pullrequests?state=ALL" paginator = Paginator.new(connection, relative_path, :pull_request) Collection.new(paginator) @@ -35,6 +35,12 @@ module Bitbucket Collection.new(paginator) end + def pull_request_diff(repo, pull_request) + relative_path = "/repositories/#{repo}/pullrequests/#{pull_request}/diff" + + connection.get(relative_path) + end + def repo(name) parsed_response = connection.get("/repositories/#{name}") Representation::Repo.new(parsed_response) -- cgit v1.2.1 From 4d7303a98e970c29079cc03a449c71f3cdaa1214 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 18 Nov 2016 21:35:03 -0800 Subject: Clean up owner and slug representation --- lib/bitbucket/representation/repo.rb | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/bitbucket/representation/repo.rb b/lib/bitbucket/representation/repo.rb index fe5cda66ab9..b291dfe0441 100644 --- a/lib/bitbucket/representation/repo.rb +++ b/lib/bitbucket/representation/repo.rb @@ -5,10 +5,18 @@ module Bitbucket def initialize(raw) super(raw) + end - if full_name && full_name.split('/').size == 2 - @owner, @slug = full_name.split('/') - end + def owner_and_slug + @owner_and_slug ||= full_name.split('/', 2) + end + + def owner + owner_and_slug.first + end + + def slug + owner_and_slug.last end def clone_url(token = nil) -- cgit v1.2.1 From c7c4d657b427c6fa146319ccc5aa17e87d3d0e0b Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 19 Nov 2016 21:44:19 -0800 Subject: Clean up Bitbucket connection based on review comments --- lib/bitbucket/client.rb | 2 +- lib/bitbucket/connection.rb | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/bitbucket/client.rb b/lib/bitbucket/client.rb index 0d4cfd600b8..33e977d655d 100644 --- a/lib/bitbucket/client.rb +++ b/lib/bitbucket/client.rb @@ -1,7 +1,7 @@ module Bitbucket class Client def initialize(options = {}) - @connection = options.fetch(:connection, Connection.new(options)) + @connection = Connection.new(options) end def issues(repo) diff --git a/lib/bitbucket/connection.rb b/lib/bitbucket/connection.rb index 201e7e3b808..c375fe16aee 100644 --- a/lib/bitbucket/connection.rb +++ b/lib/bitbucket/connection.rb @@ -7,12 +7,12 @@ module Bitbucket def initialize(options = {}) @api_version = options.fetch(:api_version, DEFAULT_API_VERSION) @base_uri = options.fetch(:base_uri, DEFAULT_BASE_URI) - @query = options.fetch(:query, DEFAULT_QUERY) + @default_query = options.fetch(:query, DEFAULT_QUERY) - @token = options.fetch(:token) - @expires_at = options.fetch(:expires_at) - @expires_in = options.fetch(:expires_in) - @refresh_token = options.fetch(:refresh_token) + @token = options[:token] + @expires_at = options[:expires_at] + @expires_in = options[:expires_in] + @refresh_token = options[:refresh_token] end def client @@ -24,13 +24,13 @@ module Bitbucket end def query(params = {}) - @query.merge!(params) + @default_query.merge!(params) end - def get(path, query = {}) + def get(path, extra_query = {}) refresh! if expired? - response = connection.get(build_url(path), params: @query.merge(query)) + response = connection.get(build_url(path), params: @default_query.merge(extra_query)) response.parsed end -- cgit v1.2.1 From 2747e515c6b06a905512c00af428b1a0aa018569 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 19 Nov 2016 22:39:12 -0800 Subject: Fix BitbucketImport::ProjectCreator spec --- spec/lib/gitlab/bitbucket_import/project_creator_spec.rb | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb index e1c60e07b4d..bb007949557 100644 --- a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb @@ -3,12 +3,14 @@ require 'spec_helper' describe Gitlab::BitbucketImport::ProjectCreator, lib: true do let(:user) { create(:user) } let(:repo) do - { - name: 'Vim', - slug: 'vim', - is_private: true, - owner: "asd" - }.with_indifferent_access + double(name: 'Vim', + slug: 'vim', + description: 'Test repo', + is_private: true, + owner: "asd", + full_name: 'Vim repo', + visibility_level: Gitlab::VisibilityLevel::PRIVATE, + clone_url: 'ssh://git@bitbucket.org/asd/vim.git') end let(:namespace){ create(:group, owner: user) } let(:token) { "asdasd12345" } @@ -22,7 +24,7 @@ describe Gitlab::BitbucketImport::ProjectCreator, lib: true do it 'creates project' do allow_any_instance_of(Project).to receive(:add_import_job) - project_creator = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, user, access_params) + project_creator = Gitlab::BitbucketImport::ProjectCreator.new(repo, 'vim', namespace, user, access_params) project = project_creator.execute expect(project.import_url).to eq("ssh://git@bitbucket.org/asd/vim.git") -- cgit v1.2.1 From 7ba65d05af190a0aba05bd78463eb9e7d70ca6f7 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sun, 20 Nov 2016 21:00:02 -0800 Subject: Fix Bitbucket callback spec --- spec/controllers/import/bitbucket_controller_spec.rb | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index 1d3c9fbbe2f..11bb190af7e 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -6,11 +6,11 @@ describe Import::BitbucketController do let(:user) { create(:user) } let(:token) { "asdasd12345" } let(:secret) { "sekrettt" } + let(:refresh_token) { SecureRandom.hex(15) } let(:access_params) { { bitbucket_access_token: token, bitbucket_access_token_secret: secret } } def assign_session_tokens - session[:bitbucket_access_token] = token - session[:bitbucket_access_token_secret] = secret + session[:bitbucket_token] = token end before do @@ -24,15 +24,23 @@ describe Import::BitbucketController do end it "updates access token" do - access_token = double(token: token, secret: secret) - allow_any_instance_of(Gitlab::BitbucketImport::Client). + expires_at = Time.now + 1.day + expires_in = 1.day + access_token = double(token: token, + secret: secret, + expires_at: expires_at, + expires_in: expires_in, + refresh_token: refresh_token) + allow_any_instance_of(OAuth2::Client). to receive(:get_token).and_return(access_token) stub_omniauth_provider('bitbucket') get :callback - expect(session[:bitbucket_access_token]).to eq(token) - expect(session[:bitbucket_access_token_secret]).to eq(secret) + expect(session[:bitbucket_token]).to eq(token) + expect(session[:bitbucket_refresh_token]).to eq(refresh_token) + expect(session[:bitbucket_expires_at]).to eq(expires_at) + expect(session[:bitbucket_expires_in]).to eq(expires_in) expect(controller).to redirect_to(status_import_bitbucket_url) end end -- cgit v1.2.1 From af6926283b8c4d8cd9668a8df6a28f2ce35001f6 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sun, 20 Nov 2016 21:33:06 -0800 Subject: Fix Bitbucket status controller spec --- spec/controllers/import/bitbucket_controller_spec.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index 11bb190af7e..2fd01c865fb 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -47,14 +47,13 @@ describe Import::BitbucketController do describe "GET status" do before do - @repo = OpenStruct.new(slug: 'vim', owner: 'asd') + @repo = double(slug: 'vim', owner: 'asd', full_name: 'asd/vim', "valid?" => true) assign_session_tokens end it "assigns variables" do @project = create(:project, import_type: 'bitbucket', creator_id: user.id) - client = stub_client(projects: [@repo]) - allow(client).to receive(:incompatible_projects).and_return([]) + allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo]) get :status @@ -65,7 +64,7 @@ describe Import::BitbucketController do it "does not show already added project" do @project = create(:project, import_type: 'bitbucket', creator_id: user.id, import_source: 'asd/vim') - stub_client(projects: [@repo]) + allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo]) get :status -- cgit v1.2.1 From 7953480646b5b129868e4323502a28ce27328d8c Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sun, 20 Nov 2016 22:29:45 -0800 Subject: Fix remaining Bitbucket controller specs --- app/controllers/import/bitbucket_controller.rb | 2 +- .../import/bitbucket_controller_spec.rb | 25 ++++++++++------------ 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 72c90f9daf2..9c97a97a5dd 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -46,7 +46,7 @@ class Import::BitbucketController < Import::BaseController repo_owner = current_user.username if repo_owner == bitbucket_client.user.username @target_namespace = params[:new_namespace].presence || repo_owner - namespace = find_or_create_namespace(@target_namespace, repo_owner) + namespace = find_or_create_namespace(@target_namespace, current_user) if current_user.can?(:create_projects, namespace) @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @project_name, namespace, current_user, credentials).execute diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index 2fd01c865fb..ce7c0b334ee 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -7,7 +7,7 @@ describe Import::BitbucketController do let(:token) { "asdasd12345" } let(:secret) { "sekrettt" } let(:refresh_token) { SecureRandom.hex(15) } - let(:access_params) { { bitbucket_access_token: token, bitbucket_access_token_secret: secret } } + let(:access_params) { { token: token, expires_at: nil, expires_in: nil, refresh_token: nil } } def assign_session_tokens session[:bitbucket_token] = token @@ -77,19 +77,16 @@ describe Import::BitbucketController do let(:bitbucket_username) { user.username } let(:bitbucket_user) do - { user: { username: bitbucket_username } }.with_indifferent_access + double(username: bitbucket_username) end let(:bitbucket_repo) do - { slug: "vim", owner: bitbucket_username }.with_indifferent_access + double(slug: "vim", owner: bitbucket_username, name: 'vim') end before do - allow(Gitlab::BitbucketImport::KeyAdder). - to receive(:new).with(bitbucket_repo, user, access_params). - and_return(double(execute: true)) - - stub_client(user: bitbucket_user, project: bitbucket_repo) + allow_any_instance_of(Bitbucket::Client).to receive(:repo).and_return(bitbucket_repo) + allow_any_instance_of(Bitbucket::Client).to receive(:user).and_return(bitbucket_user) assign_session_tokens end @@ -97,7 +94,7 @@ describe Import::BitbucketController do context "when the Bitbucket user and GitLab user's usernames match" do it "takes the current user's namespace" do expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, user.namespace, user, access_params). + to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params). and_return(double(execute: true)) post :create, format: :js @@ -109,7 +106,7 @@ describe Import::BitbucketController do it "takes the current user's namespace" do expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, user.namespace, user, access_params). + to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params). and_return(double(execute: true)) post :create, format: :js @@ -121,7 +118,7 @@ describe Import::BitbucketController do let(:other_username) { "someone_else" } before do - bitbucket_repo["owner"] = other_username + allow(bitbucket_repo).to receive(:owner).and_return(other_username) end context "when a namespace with the Bitbucket user's username already exists" do @@ -130,7 +127,7 @@ describe Import::BitbucketController do context "when the namespace is owned by the GitLab user" do it "takes the existing namespace" do expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, existing_namespace, user, access_params). + to receive(:new).with(bitbucket_repo, bitbucket_repo.name, existing_namespace, user, access_params). and_return(double(execute: true)) post :create, format: :js @@ -163,7 +160,7 @@ describe Import::BitbucketController do it "takes the new namespace" do expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, an_instance_of(Group), user, access_params). + to receive(:new).with(bitbucket_repo, bitbucket_repo.name, an_instance_of(Group), user, access_params). and_return(double(execute: true)) post :create, format: :js @@ -184,7 +181,7 @@ describe Import::BitbucketController do it "takes the current user's namespace" do expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, user.namespace, user, access_params). + to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params). and_return(double(execute: true)) post :create, format: :js -- cgit v1.2.1 From 0b72994b63dbbbddaf0e77629249d92890a6e4a4 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 21 Nov 2016 08:25:47 -0800 Subject: Simplify Bitbucket::Page implementation --- lib/bitbucket/page.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bitbucket/page.rb b/lib/bitbucket/page.rb index bc51ce7dce2..b91a173b53c 100644 --- a/lib/bitbucket/page.rb +++ b/lib/bitbucket/page.rb @@ -23,7 +23,7 @@ module Bitbucket end def parse_values(raw, representation_class) - return [] if raw['values'].nil? || !raw['values'].is_a?(Array) + return [] unless raw['values'] && raw['values'].is_a?(Array) raw['values'].map { |hash| representation_class.new(hash) } end -- cgit v1.2.1 From 77cf855bb995eb8875b41d5683c30bae2d17f2f4 Mon Sep 17 00:00:00 2001 From: Semyon Pupkov Date: Tue, 22 Nov 2016 00:15:46 +0500 Subject: Define common helper for describe pagination params in api --- ...elper-for-describe-pagination-params-in-api.yml | 4 ++++ lib/api/broadcast_messages.rb | 5 +++-- lib/api/commits.rb | 5 +++-- lib/api/deployments.rb | 5 +++-- lib/api/environments.rb | 5 +++-- lib/api/pagination_params.rb | 24 ++++++++++++++++++++++ lib/api/pipelines.rb | 5 +++-- lib/api/variables.rb | 5 +++-- 8 files changed, 46 insertions(+), 12 deletions(-) create mode 100644 changelogs/unreleased/23532-define-common-helper-for-describe-pagination-params-in-api.yml create mode 100644 lib/api/pagination_params.rb diff --git a/changelogs/unreleased/23532-define-common-helper-for-describe-pagination-params-in-api.yml b/changelogs/unreleased/23532-define-common-helper-for-describe-pagination-params-in-api.yml new file mode 100644 index 00000000000..bb9e96d7581 --- /dev/null +++ b/changelogs/unreleased/23532-define-common-helper-for-describe-pagination-params-in-api.yml @@ -0,0 +1,4 @@ +--- +title: Define common helper for describe pagination params in api +merge_request: 7646 +author: Semyon Pupkov diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb index b6281a7f0ac..1217002bf8e 100644 --- a/lib/api/broadcast_messages.rb +++ b/lib/api/broadcast_messages.rb @@ -1,5 +1,7 @@ module API class BroadcastMessages < Grape::API + include PaginationParams + before { authenticate! } before { authenticated_as_admin! } @@ -15,8 +17,7 @@ module API success Entities::BroadcastMessage end params do - optional :page, type: Integer, desc: 'Current page number' - optional :per_page, type: Integer, desc: 'Number of messages per page' + use :pagination end get do messages = BroadcastMessage.all diff --git a/lib/api/commits.rb b/lib/api/commits.rb index f412e1da1bf..0319d076ecb 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -3,6 +3,8 @@ require 'mime/types' module API # Projects commits API class Commits < Grape::API + include PaginationParams + before { authenticate! } before { authorize! :download_code, user_project } @@ -107,9 +109,8 @@ module API failure [[404, 'Not Found']] end params do + use :pagination requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag' - optional :per_page, type: Integer, desc: 'The amount of items per page for paginaion' - optional :page, type: Integer, desc: 'The page number for pagination' end get ':id/repository/commits/:sha/comments' do commit = user_project.commit(params[:sha]) diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb index f782bcaf7e9..c5feb49b22f 100644 --- a/lib/api/deployments.rb +++ b/lib/api/deployments.rb @@ -1,6 +1,8 @@ module API # Deployments RESTfull API endpoints class Deployments < Grape::API + include PaginationParams + before { authenticate! } params do @@ -12,8 +14,7 @@ module API success Entities::Deployment end params do - optional :page, type: Integer, desc: 'Page number of the current request' - optional :per_page, type: Integer, desc: 'Number of items per page' + use :pagination end get ':id/deployments' do authorize! :read_deployment, user_project diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 00c901937b1..80bbd9bb6e4 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -1,6 +1,8 @@ module API # Environments RESTfull API endpoints class Environments < Grape::API + include PaginationParams + before { authenticate! } params do @@ -12,8 +14,7 @@ module API success Entities::Environment end params do - optional :page, type: Integer, desc: 'Page number of the current request' - optional :per_page, type: Integer, desc: 'Number of items per page' + use :pagination end get ':id/environments' do authorize! :read_environment, user_project diff --git a/lib/api/pagination_params.rb b/lib/api/pagination_params.rb new file mode 100644 index 00000000000..8c1e4381a74 --- /dev/null +++ b/lib/api/pagination_params.rb @@ -0,0 +1,24 @@ +module API + # Concern for declare pagination params. + # + # @example + # class CustomApiResource < Grape::API + # include PaginationParams + # + # params do + # use :pagination + # end + # end + module PaginationParams + extend ActiveSupport::Concern + + included do + helpers do + params :pagination do + optional :page, type: Integer, desc: 'Current page number' + optional :per_page, type: Integer, desc: 'Number of items per page' + end + end + end + end +end diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb index 2a0c8e1f2c0..24cc0932181 100644 --- a/lib/api/pipelines.rb +++ b/lib/api/pipelines.rb @@ -1,5 +1,7 @@ module API class Pipelines < Grape::API + include PaginationParams + before { authenticate! } params do @@ -11,8 +13,7 @@ module API success Entities::Pipeline end params do - optional :page, type: Integer, desc: 'Page number of the current request' - optional :per_page, type: Integer, desc: 'Number of items per page' + use :pagination optional :scope, type: String, values: ['running', 'branches', 'tags'], desc: 'Either running, branches, or tags' end diff --git a/lib/api/variables.rb b/lib/api/variables.rb index b9fb3c21dbb..90f904b8a12 100644 --- a/lib/api/variables.rb +++ b/lib/api/variables.rb @@ -1,6 +1,8 @@ module API # Projects variables API class Variables < Grape::API + include PaginationParams + before { authenticate! } before { authorize! :admin_build, user_project } @@ -13,8 +15,7 @@ module API success Entities::Variable end params do - optional :page, type: Integer, desc: 'The page number for pagination' - optional :per_page, type: Integer, desc: 'The value of items per page to show' + use :pagination end get ':id/variables' do variables = user_project.variables -- cgit v1.2.1 From 402cc95c1a1df8168467f74e21c6df7d48359714 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 21 Nov 2016 20:49:40 -0800 Subject: Fix Bitbucket importer spec to pass with 2.0 API --- lib/bitbucket/page.rb | 4 +-- lib/gitlab/bitbucket_import/importer.rb | 7 ---- spec/lib/gitlab/bitbucket_import/importer_spec.rb | 42 ++++++++++++++++------- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/lib/bitbucket/page.rb b/lib/bitbucket/page.rb index b91a173b53c..49d083cc66f 100644 --- a/lib/bitbucket/page.rb +++ b/lib/bitbucket/page.rb @@ -22,10 +22,10 @@ module Bitbucket attrs.map { |attr| { attr.to_sym => raw[attr] } }.reduce(&:merge) end - def parse_values(raw, representation_class) + def parse_values(raw, bitbucket_rep_class) return [] unless raw['values'] && raw['values'].is_a?(Array) - raw['values'].map { |hash| representation_class.new(hash) } + raw['values'].map { |hash| bitbucket_rep_class.new(hash) } end def representation_class(type) diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 1f7a691e6dd..729b465e861 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -62,13 +62,6 @@ module Gitlab ) end end - - project.issues.create!( - description: body, - title: issue["title"], - state: %w(resolved invalid duplicate wontfix closed).include?(issue["status"]) ? 'closed' : 'opened', - author_id: gitlab_user_id(project, reporter) - ) end rescue ActiveRecord::RecordInvalid nil diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index aa00f32becb..99a65f26f99 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -23,10 +23,14 @@ describe Gitlab::BitbucketImport::Importer, lib: true do statuses.map.with_index do |status, index| issues << { - local_id: index, - status: status, + id: index, + state: status, title: "Issue #{index}", - content: "Some content to issue #{index}" + content: { + raw: "Some content to issue #{index}", + markup: "markdown", + html: "Some content to issue #{index}" + } } end @@ -37,8 +41,8 @@ describe Gitlab::BitbucketImport::Importer, lib: true do let(:data) do { 'bb_session' => { - 'bitbucket_access_token' => "123456", - 'bitbucket_access_token_secret' => "secret" + 'bitbucket_token' => "123456", + 'bitbucket_refresh_token' => "secret" } } end @@ -53,7 +57,7 @@ describe Gitlab::BitbucketImport::Importer, lib: true do let(:issues_statuses_sample_data) do { count: sample_issues_statuses.count, - issues: sample_issues_statuses + values: sample_issues_statuses } end @@ -61,26 +65,40 @@ describe Gitlab::BitbucketImport::Importer, lib: true do before do stub_request( :get, - "https://bitbucket.org/api/1.0/repositories/#{project_identifier}" - ).to_return(status: 200, body: { has_issues: true }.to_json) + "https://api.bitbucket.org/2.0/repositories/#{project_identifier}" + ).to_return(status: 200, + headers: {"Content-Type" => "application/json"}, + body: { has_issues: true, full_name: project_identifier }.to_json) stub_request( :get, - "https://bitbucket.org/api/1.0/repositories/#{project_identifier}/issues?limit=50&sort=utc_created_on&start=0" - ).to_return(status: 200, body: issues_statuses_sample_data.to_json) + "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/issues?pagelen=50&sort=created_on" + ).to_return(status: 200, + headers: {"Content-Type" => "application/json"}, + body: issues_statuses_sample_data.to_json) sample_issues_statuses.each_with_index do |issue, index| stub_request( :get, - "https://bitbucket.org/api/1.0/repositories/#{project_identifier}/issues/#{issue[:local_id]}/comments" + "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/issues/#{issue[:id]}/comments?pagelen=50&sort=created_on" ).to_return( status: 200, - body: [{ author_info: { username: "username" }, utc_created_on: index }].to_json + headers: {"Content-Type" => "application/json"}, + body: { author_info: { username: "username" }, utc_created_on: index }.to_json ) end + + stub_request( + :get, + "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/pullrequests?pagelen=50&sort=created_on&state=ALL" + ).to_return(status: 200, + headers: {"Content-Type" => "application/json"}, + body: {}.to_json) end it 'map statuses to open or closed' do + # HACK: Bitbucket::Representation.const_get('Issue') seems to return Issue without this + Bitbucket::Representation::Issue importer.execute expect(project.issues.where(state: "closed").size).to eq(5) -- cgit v1.2.1 From fc40c3f28a67ecafa58191c0cd6065960dd59c7d Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 21 Nov 2016 20:58:46 -0800 Subject: Fix Rubocop errors with Bitbucket Importer spec --- spec/lib/gitlab/bitbucket_import/importer_spec.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index 99a65f26f99..36893751ee0 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -67,14 +67,14 @@ describe Gitlab::BitbucketImport::Importer, lib: true do :get, "https://api.bitbucket.org/2.0/repositories/#{project_identifier}" ).to_return(status: 200, - headers: {"Content-Type" => "application/json"}, + headers: { "Content-Type" => "application/json" }, body: { has_issues: true, full_name: project_identifier }.to_json) stub_request( :get, "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/issues?pagelen=50&sort=created_on" ).to_return(status: 200, - headers: {"Content-Type" => "application/json"}, + headers: { "Content-Type" => "application/json" }, body: issues_statuses_sample_data.to_json) sample_issues_statuses.each_with_index do |issue, index| @@ -83,22 +83,22 @@ describe Gitlab::BitbucketImport::Importer, lib: true do "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/issues/#{issue[:id]}/comments?pagelen=50&sort=created_on" ).to_return( status: 200, - headers: {"Content-Type" => "application/json"}, + headers: { "Content-Type" => "application/json" }, body: { author_info: { username: "username" }, utc_created_on: index }.to_json ) end stub_request( - :get, - "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/pullrequests?pagelen=50&sort=created_on&state=ALL" + :get, + "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/pullrequests?pagelen=50&sort=created_on&state=ALL" ).to_return(status: 200, - headers: {"Content-Type" => "application/json"}, + headers: { "Content-Type" => "application/json" }, body: {}.to_json) end it 'map statuses to open or closed' do # HACK: Bitbucket::Representation.const_get('Issue') seems to return Issue without this - Bitbucket::Representation::Issue + Bitbucket::Representation::Issue.new importer.execute expect(project.issues.where(state: "closed").size).to eq(5) -- cgit v1.2.1 From 7a155137a4fd965cb8ff512a9548a7e685b330f5 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 21 Nov 2016 22:06:09 -0800 Subject: Fix spec for Bitbucket importer --- lib/gitlab/bitbucket_import/importer.rb | 6 +++--- spec/lib/gitlab/bitbucket_import/importer_spec.rb | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 729b465e861..08705afcabb 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -94,7 +94,7 @@ module Gitlab import_pull_request_comments(pull_request, merge_request) if merge_request.persisted? rescue ActiveRecord::RecordInvalid - nil + Rails.log.error("Bitbucket importer ERROR in #{project.path_with_namespace}: Invalid pull request #{e.message}") end end end @@ -134,7 +134,7 @@ module Gitlab merge_request.notes.create!(attributes) rescue ActiveRecord::RecordInvalid => e - Rails.log.error("Bitbucket importer ERROR: Invalid pull request comment #{e.message}") + Rails.log.error("Bitbucket importer ERROR in #{project.path_with_namespace}: Invalid pull request comment #{e.message}") nil end end @@ -145,7 +145,7 @@ module Gitlab begin merge_request.notes.create!(pull_request_comment_attributes(comment)) rescue ActiveRecord::RecordInvalid => e - Rails.log.error("Bitbucket importer ERROR: Invalid standalone pull request comment #{e.message}") + Rails.log.error("Bitbucket importer ERROR in #{project.path_with_namespace}: Invalid standalone pull request comment #{e.message}") nil end end diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index 36893751ee0..ef4fc9fd08e 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -98,7 +98,7 @@ describe Gitlab::BitbucketImport::Importer, lib: true do it 'map statuses to open or closed' do # HACK: Bitbucket::Representation.const_get('Issue') seems to return Issue without this - Bitbucket::Representation::Issue.new + Bitbucket::Representation::Issue.new({}) importer.execute expect(project.issues.where(state: "closed").size).to eq(5) -- cgit v1.2.1 From cb11d3521ce99a5ce796e2fdf9e6d96a3e36791d Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Tue, 22 Nov 2016 08:23:43 +0100 Subject: Fix IID filter for merge requests and milestones --- doc/api/merge_requests.md | 2 +- doc/api/milestones.md | 4 ++-- lib/api/merge_requests.rb | 8 +++----- lib/api/milestones.rb | 2 +- spec/requests/api/merge_requests_spec.rb | 10 ++++++++++ spec/requests/api/milestones_spec.rb | 9 +++++++++ 6 files changed, 26 insertions(+), 9 deletions(-) diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index f4167403c2c..66d91732cc6 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -10,7 +10,7 @@ The pagination parameters `page` and `per_page` can be used to restrict the list GET /projects/:id/merge_requests GET /projects/:id/merge_requests?state=opened GET /projects/:id/merge_requests?state=all -GET /projects/:id/merge_requests?iid=42 +GET /projects/:id/merge_requests?iid[]=42 ``` Parameters: diff --git a/doc/api/milestones.md b/doc/api/milestones.md index ae7d22a4be5..ad80d7c4c01 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -6,7 +6,7 @@ Returns a list of project milestones. ``` GET /projects/:id/milestones -GET /projects/:id/milestones?iid=42 +GET /projects/:id/milestones?iid[]=42 GET /projects/:id/milestones?state=active GET /projects/:id/milestones?state=closed ``` @@ -16,7 +16,7 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer | yes | The ID of a project | -| `iid` | integer | optional | Return only the milestone having the given `iid` | +| `iid` | Array[integer] | optional | Return only the milestone having the given `iid` | | `state` | string | optional | Return only `active` or `closed` milestones` | ```bash diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 4176c7eec06..009913c6242 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -41,15 +41,13 @@ module API desc: 'Return merge requests ordered by `created_at` or `updated_at` fields.' optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'Return merge requests sorted in `asc` or `desc` order.' - optional :iid, type: Integer, desc: 'The IID of the merge requests' + optional :iid, type: Array[Integer], desc: 'The IID of the merge requests' end get ":id/merge_requests" do authorize! :read_merge_request, user_project - merge_requests = user_project.merge_requests.inc_notes_with_associations - unless params[:iid].nil? - merge_requests = filter_by_iid(merge_requests, params[:iid]) - end + merge_requests = user_project.merge_requests.inc_notes_with_associations + merge_requests = filter_by_iid(merge_requests, params[:iid]) if params[:iid].present? merge_requests = case params[:state] diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index 937c118779d..29bf73934d2 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -28,7 +28,7 @@ module API params do optional :state, type: String, values: %w[active closed all], default: 'all', desc: 'Return "active", "closed", or "all" milestones' - optional :iid, type: Integer, desc: 'The IID of the milestone' + optional :iid, type: Array[Integer], desc: 'The IID of the milestone' end get ":id/milestones" do authorize! :read_milestone, user_project diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 7b3d1460c90..37fcb2bc3a9 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -169,6 +169,16 @@ describe API::API, api: true do expect(json_response.first['id']).to eq merge_request.id end + it 'returns merge_request by iid array' do + get api("/projects/#{project.id}/merge_requests", user), iid: [merge_request.iid, merge_request_closed.iid] + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['title']).to eq merge_request_closed.title + expect(json_response.first['id']).to eq merge_request_closed.id + end + it "returns a 404 error if merge_request_id not found" do get api("/projects/#{project.id}/merge_requests/999", user) expect(response).to have_http_status(404) diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index 62327f64e50..5d7b39e71b8 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -61,6 +61,15 @@ describe API::API, api: true do expect(json_response.first['id']).to eq closed_milestone.id end + it 'returns a project milestone by iid array' do + get api("/projects/#{project.id}/milestones", user), iid: [milestone.iid, closed_milestone.iid] + + expect(response).to have_http_status(200) + expect(json_response.size).to eq(2) + expect(json_response.first['title']).to eq milestone.title + expect(json_response.first['id']).to eq milestone.id + end + it 'returns 401 error if user not authenticated' do get api("/projects/#{project.id}/milestones/#{milestone.id}") -- cgit v1.2.1 From 3ff0575669ecda15c5e72bd2987715a998f97d82 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Mon, 21 Nov 2016 20:48:18 +0100 Subject: Issue creation now accepts trailing whitespace --- lib/gitlab/chat_commands/issue_create.rb | 6 ++++-- spec/lib/gitlab/chat_commands/issue_create_spec.rb | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb index 98338ebfa27..99c1382af44 100644 --- a/lib/gitlab/chat_commands/issue_create.rb +++ b/lib/gitlab/chat_commands/issue_create.rb @@ -2,7 +2,9 @@ module Gitlab module ChatCommands class IssueCreate < IssueCommand def self.match(text) - /\Aissue\s+create\s+(?[^\n]*)\n*(?<description>.*)\z/.match(text) + # we can not match \n with the dot by passing the m modifier as than + # the title and description are not seperated + /\Aissue\s+create\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text) end def self.help_message @@ -15,7 +17,7 @@ module Gitlab def execute(match) title = match[:title] - description = match[:description] + description = match[:description].to_s.rstrip Issues::CreateService.new(project, current_user, title: title, description: description).execute end diff --git a/spec/lib/gitlab/chat_commands/issue_create_spec.rb b/spec/lib/gitlab/chat_commands/issue_create_spec.rb index df0c317ccea..dd07cff9243 100644 --- a/spec/lib/gitlab/chat_commands/issue_create_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_create_spec.rb @@ -32,6 +32,15 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do expect(Issue.last.description).to eq(description) end end + + context "with more newlines between the title and the description" do + let(:description) { "Surfin bird" } + let(:regex_match) { described_class.match("issue create bird is the word\n\n#{description}\n") } + + it 'creates the issue' do + expect { subject }.to change { project.issues.count }.by(1) + end + end end describe '.match' do -- cgit v1.2.1 From b86a784e1d9bb3dc789e2b289efe7b25e844d5aa Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Tue, 22 Nov 2016 01:26:47 +0600 Subject: resolves lowercase issue in system note for labels, label description and title removes unnecessary changelog entry makes toggle commit list and compare link as secondary removes unnecessary function call --- app/assets/javascripts/notes.js | 1 + app/assets/stylesheets/pages/notes.scss | 35 ++++++++++++++++++++++++++------ app/views/projects/notes/_note.html.haml | 2 +- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 6cb87f9ba81..a84c514dac7 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -113,6 +113,7 @@ $(document).off("click", ".js-note-discard"); $(document).off("keydown", ".js-note-text"); $(document).off('click', '.js-comment-resolve-button'); + $(document).off("click", '.system-note-commit-list-toggler'); $('.note .js-task-list-container').taskList('disable'); return $(document).off('tasklist:changed', '.note .js-task-list-container'); }; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 0dfd4ab7ec9..54c7caed8ed 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -43,12 +43,25 @@ ul.notes { } .system-note-message { - text-transform: lowercase; + display: inline-block; + + &::first-letter { + text-transform: lowercase; + } a { color: $gl-link-color; text-decoration: none; } + + p { + display: inline-block; + margin: 0; + + &::first-letter { + text-transform: lowercase; + } + } } .timeline-content { @@ -62,6 +75,11 @@ ul.notes { display: none; padding: 10px 0 0; cursor: pointer; + + &:hover { + color: $gl-link-color; + text-decoration: underline; + } } .note-text { @@ -87,6 +105,16 @@ ul.notes { display: none; } + p:last-child { + a { + color: $gl-text-color; + + &:hover { + color: $gl-link-color; + } + } + } + &::after { content: ''; width: 100%; @@ -188,11 +216,6 @@ ul.notes { padding-bottom: 3px; padding-right: 20px; - p { - display: inline; - margin: 0; - } - @media (min-width: $screen-sm-min) { padding-right: 0; } diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 89ae64554c0..ba8895438c5 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -16,7 +16,7 @@ commented - if note.system %span{class: 'system-note-message'} - = h(note.note_html.downcase.html_safe) + = note.redacted_note_html %a{ href: "##{dom_id(note)}" } = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') - unless note.system? -- cgit v1.2.1 From 9d3186e44aa9690c0c6c3af6f979132baf34cb41 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 22 Nov 2016 10:25:09 +0000 Subject: Disabled award emoji button when user is not logged in Closes #24680 --- app/views/award_emoji/_awards_block.html.haml | 5 +- changelogs/unreleased/emoji-btn-disabled.yml | 4 ++ spec/features/issues/award_emoji_spec.rb | 93 +++++++++++++++------------ 3 files changed, 60 insertions(+), 42 deletions(-) create mode 100644 changelogs/unreleased/emoji-btn-disabled.yml diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index fbe3ab912b6..d8912eda314 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -1,7 +1,10 @@ - grouped_emojis = awardable.grouped_awards(with_thumbs: inline) .awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } } - awards_sort(grouped_emojis).each do |emoji, awards| - %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } } + %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", + disabled: !current_user, + class: (award_active_class(awards, current_user)), + data: { placement: "bottom", title: award_user_list(awards, current_user) } } = emoji_icon(emoji, sprite: false) %span.award-control-text.js-counter = awards.count diff --git a/changelogs/unreleased/emoji-btn-disabled.yml b/changelogs/unreleased/emoji-btn-disabled.yml new file mode 100644 index 00000000000..a18b553d513 --- /dev/null +++ b/changelogs/unreleased/emoji-btn-disabled.yml @@ -0,0 +1,4 @@ +--- +title: Disabled emoji buttons when user is not logged in +merge_request: +author: diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb index ef00f209998..efb53026449 100644 --- a/spec/features/issues/award_emoji_spec.rb +++ b/spec/features/issues/award_emoji_spec.rb @@ -3,72 +3,83 @@ require 'rails_helper' describe 'Awards Emoji', feature: true do include WaitForAjax - let!(:project) { create(:project) } + let!(:project) { create(:project, :public) } let!(:user) { create(:user) } - - before do - project.team << [user, :master] - login_as(user) + let(:issue) do + create(:issue, + assignee: @user, + project: project) end - describe 'Click award emoji from issue#show' do - let!(:issue) do - create(:issue, - assignee: @user, - project: project) - end - - let!(:note) { create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") } - + context 'authorized user' do before do - visit namespace_project_issue_path(project.namespace, project, issue) + project.team << [user, :master] + login_as(user) end - it 'increments the thumbsdown emoji', js: true do - find('[data-emoji="thumbsdown"]').click - wait_for_ajax - expect(thumbsdown_emoji).to have_text("1") - end + describe 'Click award emoji from issue#show' do + let!(:note) { create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") } - context 'click the thumbsup emoji' do - it 'increments the thumbsup emoji', js: true do - find('[data-emoji="thumbsup"]').click - wait_for_ajax - expect(thumbsup_emoji).to have_text("1") + before do + visit namespace_project_issue_path(project.namespace, project, issue) end - it 'decrements the thumbsdown emoji', js: true do - expect(thumbsdown_emoji).to have_text("0") - end - end - - context 'click the thumbsdown emoji' do it 'increments the thumbsdown emoji', js: true do find('[data-emoji="thumbsdown"]').click wait_for_ajax expect(thumbsdown_emoji).to have_text("1") end - it 'decrements the thumbsup emoji', js: true do - expect(thumbsup_emoji).to have_text("0") + context 'click the thumbsup emoji' do + it 'increments the thumbsup emoji', js: true do + find('[data-emoji="thumbsup"]').click + wait_for_ajax + expect(thumbsup_emoji).to have_text("1") + end + + it 'decrements the thumbsdown emoji', js: true do + expect(thumbsdown_emoji).to have_text("0") + end end - end - it 'toggles the smiley emoji on a note', js: true do - toggle_smiley_emoji(true) + context 'click the thumbsdown emoji' do + it 'increments the thumbsdown emoji', js: true do + find('[data-emoji="thumbsdown"]').click + wait_for_ajax + expect(thumbsdown_emoji).to have_text("1") + end - within('.note-awards') do - expect(find(emoji_counter)).to have_text("1") + it 'decrements the thumbsup emoji', js: true do + expect(thumbsup_emoji).to have_text("0") + end end - toggle_smiley_emoji(false) + it 'toggles the smiley emoji on a note', js: true do + toggle_smiley_emoji(true) + + within('.note-awards') do + expect(find(emoji_counter)).to have_text("1") + end + + toggle_smiley_emoji(false) - within('.note-awards') do - expect(page).not_to have_selector(emoji_counter) + within('.note-awards') do + expect(page).not_to have_selector(emoji_counter) + end end end end + context 'unauthorized user', js: true do + before do + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it 'has disabled emoji button' do + expect(first('.award-control')[:disabled]).to be(true) + end + end + def thumbsup_emoji page.all(emoji_counter).first end -- cgit v1.2.1 From 1e19737ca20fe53327094196bbc9758e5c9e03fa Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 22 Nov 2016 11:52:58 +0100 Subject: Refactor feature tests for project builds page --- spec/features/projects/builds_spec.rb | 159 ++++++++++++++++++---------------- 1 file changed, 84 insertions(+), 75 deletions(-) diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb index a8022a5361f..eec1d337224 100644 --- a/spec/features/projects/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -2,51 +2,56 @@ require 'spec_helper' require 'tempfile' describe "Builds" do - let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } + let(:artifacts_file) do + fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') + end + + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + + let!(:build) { create(:ci_build, :trace, pipeline: pipeline) } + let!(:build2) { create(:ci_build) } before do - login_as(:user) - @commit = FactoryGirl.create :ci_pipeline - @build = FactoryGirl.create :ci_build, :trace, pipeline: @commit - @build2 = FactoryGirl.create :ci_build - @project = @commit.project - @project.team << [@user, :developer] + project.team << [user, :developer] + login_as(user) end describe "GET /:project/builds" do context "Pending scope" do before do - visit namespace_project_builds_path(@project.namespace, @project, scope: :pending) + visit namespace_project_builds_path(project.namespace, project, scope: :pending) end it "shows Pending tab builds" do expect(page).to have_link 'Cancel running' expect(page).to have_selector('.nav-links li.active', text: 'Pending') - expect(page).to have_content @build.short_sha - expect(page).to have_content @build.ref - expect(page).to have_content @build.name + expect(page).to have_content build.short_sha + expect(page).to have_content build.ref + expect(page).to have_content build.name end end context "Running scope" do before do - @build.run! - visit namespace_project_builds_path(@project.namespace, @project, scope: :running) + build.run! + visit namespace_project_builds_path(project.namespace, project, scope: :running) end it "shows Running tab builds" do expect(page).to have_selector('.nav-links li.active', text: 'Running') expect(page).to have_link 'Cancel running' - expect(page).to have_content @build.short_sha - expect(page).to have_content @build.ref - expect(page).to have_content @build.name + expect(page).to have_content build.short_sha + expect(page).to have_content build.ref + expect(page).to have_content build.name end end context "Finished scope" do before do - @build.run! - visit namespace_project_builds_path(@project.namespace, @project, scope: :finished) + build.run! + visit namespace_project_builds_path(project.namespace, project, scope: :finished) end it "shows Finished tab builds" do @@ -58,15 +63,15 @@ describe "Builds" do context "All builds" do before do - @project.builds.running_or_pending.each(&:success) - visit namespace_project_builds_path(@project.namespace, @project) + project.builds.running_or_pending.each(&:success) + visit namespace_project_builds_path(project.namespace, project) end it "shows All tab builds" do expect(page).to have_selector('.nav-links li.active', text: 'All') - expect(page).to have_content @build.short_sha - expect(page).to have_content @build.ref - expect(page).to have_content @build.name + expect(page).to have_content build.short_sha + expect(page).to have_content build.ref + expect(page).to have_content build.name expect(page).not_to have_link 'Cancel running' end end @@ -74,17 +79,17 @@ describe "Builds" do describe "POST /:project/builds/:id/cancel_all" do before do - @build.run! - visit namespace_project_builds_path(@project.namespace, @project) + build.run! + visit namespace_project_builds_path(project.namespace, project) click_link "Cancel running" end it 'shows all necessary content' do expect(page).to have_selector('.nav-links li.active', text: 'All') expect(page).to have_content 'canceled' - expect(page).to have_content @build.short_sha - expect(page).to have_content @build.ref - expect(page).to have_content @build.name + expect(page).to have_content build.short_sha + expect(page).to have_content build.ref + expect(page).to have_content build.name expect(page).not_to have_link 'Cancel running' end end @@ -92,20 +97,20 @@ describe "Builds" do describe "GET /:project/builds/:id" do context "Build from project" do before do - visit namespace_project_build_path(@project.namespace, @project, @build) + visit namespace_project_build_path(project.namespace, project, build) end it 'shows commit`s data' do expect(page.status_code).to eq(200) - expect(page).to have_content @commit.sha[0..7] - expect(page).to have_content @commit.git_commit_message - expect(page).to have_content @commit.git_author_name + expect(page).to have_content pipeline.sha[0..7] + expect(page).to have_content pipeline.git_commit_message + expect(page).to have_content pipeline.git_author_name end end context "Build from other project" do before do - visit namespace_project_build_path(@project.namespace, @project, @build2) + visit namespace_project_build_path(project.namespace, project, build2) end it { expect(page.status_code).to eq(404) } @@ -113,8 +118,8 @@ describe "Builds" do context "Download artifacts" do before do - @build.update_attributes(artifacts_file: artifacts_file) - visit namespace_project_build_path(@project.namespace, @project, @build) + build.update_attributes(artifacts_file: artifacts_file) + visit namespace_project_build_path(project.namespace, project, build) end it 'has button to download artifacts' do @@ -124,8 +129,8 @@ describe "Builds" do context 'Artifacts expire date' do before do - @build.update_attributes(artifacts_file: artifacts_file, artifacts_expire_at: expire_at) - visit namespace_project_build_path(@project.namespace, @project, @build) + build.update_attributes(artifacts_file: artifacts_file, artifacts_expire_at: expire_at) + visit namespace_project_build_path(project.namespace, project, build) end context 'no expire date defined' do @@ -160,8 +165,8 @@ describe "Builds" do context 'Build raw trace' do before do - @build.run! - visit namespace_project_build_path(@project.namespace, @project, @build) + build.run! + visit namespace_project_build_path(project.namespace, project, build) end it do @@ -170,10 +175,14 @@ describe "Builds" do end describe 'Variables' do + let(:trigger_request) { create(:ci_trigger_request_with_variables) } + + let(:build) do + create :ci_build, pipeline: pipeline, trigger_request: trigger_request + end + before do - @trigger_request = create :ci_trigger_request_with_variables - @build = create :ci_build, pipeline: @commit, trigger_request: @trigger_request - visit namespace_project_build_path(@project.namespace, @project, @build) + visit namespace_project_build_path(project.namespace, project, build) end it 'shows variable key and value after click', js: true do @@ -193,8 +202,8 @@ describe "Builds" do describe "POST /:project/builds/:id/cancel" do context "Build from project" do before do - @build.run! - visit namespace_project_build_path(@project.namespace, @project, @build) + build.run! + visit namespace_project_build_path(project.namespace, project, build) click_link "Cancel" end @@ -207,9 +216,9 @@ describe "Builds" do context "Build from other project" do before do - @build.run! - visit namespace_project_build_path(@project.namespace, @project, @build) - page.driver.post(cancel_namespace_project_build_path(@project.namespace, @project, @build2)) + build.run! + visit namespace_project_build_path(project.namespace, project, build) + page.driver.post(cancel_namespace_project_build_path(project.namespace, project, build2)) end it { expect(page.status_code).to eq(404) } @@ -219,8 +228,8 @@ describe "Builds" do describe "POST /:project/builds/:id/retry" do context "Build from project" do before do - @build.run! - visit namespace_project_build_path(@project.namespace, @project, @build) + build.run! + visit namespace_project_build_path(project.namespace, project, build) click_link 'Cancel' page.within('.build-header') do click_link 'Retry build' @@ -238,10 +247,10 @@ describe "Builds" do context "Build from other project" do before do - @build.run! - visit namespace_project_build_path(@project.namespace, @project, @build) + build.run! + visit namespace_project_build_path(project.namespace, project, build) click_link 'Cancel' - page.driver.post(retry_namespace_project_build_path(@project.namespace, @project, @build2)) + page.driver.post(retry_namespace_project_build_path(project.namespace, project, build2)) end it { expect(page).to have_http_status(404) } @@ -249,13 +258,13 @@ describe "Builds" do context "Build that current user is not allowed to retry" do before do - @build.run! - @build.cancel! - @project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + build.run! + build.cancel! + project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) logout_direct login_with(create(:user)) - visit namespace_project_build_path(@project.namespace, @project, @build) + visit namespace_project_build_path(project.namespace, project, build) end it 'does not show the Retry button' do @@ -268,15 +277,15 @@ describe "Builds" do describe "GET /:project/builds/:id/download" do before do - @build.update_attributes(artifacts_file: artifacts_file) - visit namespace_project_build_path(@project.namespace, @project, @build) + build.update_attributes(artifacts_file: artifacts_file) + visit namespace_project_build_path(project.namespace, project, build) click_link 'Download' end context "Build from other project" do before do - @build2.update_attributes(artifacts_file: artifacts_file) - visit download_namespace_project_build_artifacts_path(@project.namespace, @project, @build2) + build2.update_attributes(artifacts_file: artifacts_file) + visit download_namespace_project_build_artifacts_path(project.namespace, project, build2) end it { expect(page.status_code).to eq(404) } @@ -288,23 +297,23 @@ describe "Builds" do context 'build from project' do before do Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') - @build.run! - visit namespace_project_build_path(@project.namespace, @project, @build) + build.run! + visit namespace_project_build_path(project.namespace, project, build) page.within('.js-build-sidebar') { click_link 'Raw' } end it 'sends the right headers' do expect(page.status_code).to eq(200) expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8') - expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace) + expect(page.response_headers['X-Sendfile']).to eq(build.path_to_trace) end end context 'build from other project' do before do Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') - @build2.run! - visit raw_namespace_project_build_path(@project.namespace, @project, @build2) + build2.run! + visit raw_namespace_project_build_path(project.namespace, project, build2) end it 'sends the right headers' do @@ -325,8 +334,8 @@ describe "Builds" do context 'when build has trace in file' do before do Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') - @build.run! - visit namespace_project_build_path(@project.namespace, @project, @build) + build.run! + visit namespace_project_build_path(project.namespace, project, build) allow_any_instance_of(Project).to receive(:ci_id).and_return(nil) allow_any_instance_of(Ci::Build).to receive(:path_to_trace).and_return(existing_file) @@ -345,8 +354,8 @@ describe "Builds" do context 'when build has trace in old file' do before do Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') - @build.run! - visit namespace_project_build_path(@project.namespace, @project, @build) + build.run! + visit namespace_project_build_path(project.namespace, project, build) allow_any_instance_of(Project).to receive(:ci_id).and_return(999) allow_any_instance_of(Ci::Build).to receive(:path_to_trace).and_return(non_existing_file) @@ -365,8 +374,8 @@ describe "Builds" do context 'when build has trace in DB' do before do Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') - @build.run! - visit namespace_project_build_path(@project.namespace, @project, @build) + build.run! + visit namespace_project_build_path(project.namespace, project, build) allow_any_instance_of(Project).to receive(:ci_id).and_return(nil) allow_any_instance_of(Ci::Build).to receive(:path_to_trace).and_return(non_existing_file) @@ -385,7 +394,7 @@ describe "Builds" do describe "GET /:project/builds/:id/trace.json" do context "Build from project" do before do - visit trace_namespace_project_build_path(@project.namespace, @project, @build, format: :json) + visit trace_namespace_project_build_path(project.namespace, project, build, format: :json) end it { expect(page.status_code).to eq(200) } @@ -393,7 +402,7 @@ describe "Builds" do context "Build from other project" do before do - visit trace_namespace_project_build_path(@project.namespace, @project, @build2, format: :json) + visit trace_namespace_project_build_path(project.namespace, project, build2, format: :json) end it { expect(page.status_code).to eq(404) } @@ -403,7 +412,7 @@ describe "Builds" do describe "GET /:project/builds/:id/status" do context "Build from project" do before do - visit status_namespace_project_build_path(@project.namespace, @project, @build) + visit status_namespace_project_build_path(project.namespace, project, build) end it { expect(page.status_code).to eq(200) } @@ -411,7 +420,7 @@ describe "Builds" do context "Build from other project" do before do - visit status_namespace_project_build_path(@project.namespace, @project, @build2) + visit status_namespace_project_build_path(project.namespace, project, build2) end it { expect(page.status_code).to eq(404) } -- cgit v1.2.1 From 3566965417b7921cdb301c44cfb308551cdc1e82 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Tue, 22 Nov 2016 18:55:00 +0800 Subject: Passing a user to retry_failed in tests Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7508#note_18794547 --- spec/models/ci/pipeline_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index cbf25c23b05..03924a436de 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -503,7 +503,7 @@ describe Ci::Pipeline, models: true do create(:ci_build, :failed, name: 'build', pipeline: pipeline) create(:generic_commit_status, :failed, name: 'jenkins', pipeline: pipeline) - pipeline.retry_failed(nil) + pipeline.retry_failed(create(:user)) end it 'retries only build' do @@ -516,7 +516,7 @@ describe Ci::Pipeline, models: true do create(:ci_build, :failed, name: 'build', stage_idx: 0, pipeline: pipeline) create(:ci_build, :failed, name: 'jenkins', stage_idx: 1, pipeline: pipeline) - pipeline.retry_failed(nil) + pipeline.retry_failed(create(:user)) end it 'retries both builds' do @@ -529,7 +529,7 @@ describe Ci::Pipeline, models: true do create(:ci_build, :failed, name: 'build', stage_idx: 0, pipeline: pipeline) create(:ci_build, :canceled, name: 'jenkins', stage_idx: 1, pipeline: pipeline) - pipeline.retry_failed(nil) + pipeline.retry_failed(create(:user)) end it 'retries both builds' do -- cgit v1.2.1 From cc43c9acc29117cd9ec3c25805e6c5ea874e5969 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Tue, 22 Nov 2016 19:07:12 +0800 Subject: Expand the loop and reduce overlapped conditions Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7508#note_18794681 --- spec/models/ci/pipeline_spec.rb | 42 +++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 03924a436de..438810d8206 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -425,18 +425,36 @@ describe Ci::Pipeline, models: true do end %i[success failed canceled].each do |status1| - %i[ci_build generic_commit_status].each do |type0| - %i[ci_build generic_commit_status].each do |type1| - context "when there are #{type0} and #{type1} for #{status0} and #{status1}" do - before do - create(type0, status0, pipeline: pipeline) - create(type1, status1, pipeline: pipeline) - end - - it 'is cancelable' do - expect(pipeline.cancelable?).to be_truthy - end - end + context "when there are generic_commit_status jobs for #{status0} and #{status1}" do + before do + create(:generic_commit_status, status0, pipeline: pipeline) + create(:generic_commit_status, status1, pipeline: pipeline) + end + + it 'is cancelable' do + expect(pipeline.cancelable?).to be_truthy + end + end + + context "when there are generic_commit_status and ci_build jobs for #{status0} and #{status1}" do + before do + create(:generic_commit_status, status0, pipeline: pipeline) + create(:ci_build, status1, pipeline: pipeline) + end + + it 'is cancelable' do + expect(pipeline.cancelable?).to be_truthy + end + end + + context "when there are ci_build jobs for #{status0} and #{status1}" do + before do + create(:ci_build, status0, pipeline: pipeline) + create(:ci_build, status1, pipeline: pipeline) + end + + it 'is cancelable' do + expect(pipeline.cancelable?).to be_truthy end end end -- cgit v1.2.1 From e249a35161401e9b2449346132f1130c6cd1b824 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 22 Nov 2016 12:19:46 +0100 Subject: Add missing specs for loading build HTML trace --- spec/features/projects/builds_spec.rb | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb index eec1d337224..f0ad936ca69 100644 --- a/spec/features/projects/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -1,11 +1,7 @@ require 'spec_helper' require 'tempfile' -describe "Builds" do - let(:artifacts_file) do - fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') - end - +feature 'Builds', :feature do let(:user) { create(:user) } let(:project) { create(:project) } let(:pipeline) { create(:ci_pipeline, project: project) } @@ -13,6 +9,10 @@ describe "Builds" do let!(:build) { create(:ci_build, :trace, pipeline: pipeline) } let!(:build2) { create(:ci_build) } + let(:artifacts_file) do + fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') + end + before do project.team << [user, :developer] login_as(user) @@ -163,7 +163,7 @@ describe "Builds" do end end - context 'Build raw trace' do + feature 'Raw trace' do before do build.run! visit namespace_project_build_path(project.namespace, project, build) @@ -174,7 +174,23 @@ describe "Builds" do end end - describe 'Variables' do + feature 'HTML trace', :js do + before do + build.run! + + visit namespace_project_build_path(project.namespace, project, build) + end + + it 'loads build trace' do + expect(page).to have_content 'BUILD TRACE' + + build.append_trace(' and more trace', 11) + + expect(page).to have_content 'BUILD TRACE and more trace' + end + end + + feature 'Variables' do let(:trigger_request) { create(:ci_trigger_request_with_variables) } let(:build) do -- cgit v1.2.1 From a085e3fb97c0ae9ebfd61f5045a28a5809252e45 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 22 Nov 2016 11:47:26 +0000 Subject: Fixed resolved discussion timeago not rendering Closes #24787 --- app/assets/javascripts/diff_notes/models/discussion.js.es6 | 9 ++++++--- changelogs/unreleased/resolve-discussions-timeago.yml | 4 ++++ 2 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/resolve-discussions-timeago.yml diff --git a/app/assets/javascripts/diff_notes/models/discussion.js.es6 b/app/assets/javascripts/diff_notes/models/discussion.js.es6 index 439f55520ef..badcdccc840 100644 --- a/app/assets/javascripts/diff_notes/models/discussion.js.es6 +++ b/app/assets/javascripts/diff_notes/models/discussion.js.es6 @@ -57,14 +57,17 @@ class DiscussionModel { } updateHeadline (data) { - const $discussionHeadline = $(`.discussion[data-discussion-id="${this.id}"] .js-discussion-headline`); + const discussionSelector = `.discussion[data-discussion-id="${this.id}"]`; + const $discussionHeadline = $(`${discussionSelector} .js-discussion-headline`); if (data.discussion_headline_html) { if ($discussionHeadline.length) { $discussionHeadline.replaceWith(data.discussion_headline_html); } else { - $(`.discussion[data-discussion-id="${this.id}"] .discussion-header`).append(data.discussion_headline_html); + $(`${discussionSelector} .discussion-header`).append(data.discussion_headline_html); } + + gl.utils.localTimeAgo($('.js-timeago', `${discussionSelector}`)); } else { $discussionHeadline.remove(); } @@ -74,7 +77,7 @@ class DiscussionModel { if (!this.canResolve) { return false; } - + for (const noteId in this.notes) { const note = this.notes[noteId]; diff --git a/changelogs/unreleased/resolve-discussions-timeago.yml b/changelogs/unreleased/resolve-discussions-timeago.yml new file mode 100644 index 00000000000..ffedeb93f1d --- /dev/null +++ b/changelogs/unreleased/resolve-discussions-timeago.yml @@ -0,0 +1,4 @@ +--- +title: Fixed timeago not rendering when resolving a discussion +merge_request: +author: -- cgit v1.2.1 From 264eda9f744f82173da4aac756643ac498b5b6a3 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Thu, 17 Nov 2016 13:42:41 +0000 Subject: Revert "Merge branch 'docs/jira-old' into 'master'" This reverts merge request !7365 --- doc/integration/README.md | 2 +- doc/integration/jira.md | 196 +++++++++++++++- doc/project_services/img/builds_emails_service.png | Bin 33943 -> 30956 bytes .../img/jira_add_gitlab_commit_message.png | Bin 46590 -> 0 bytes .../img/jira_add_user_to_group.png | Bin 41994 -> 0 bytes doc/project_services/img/jira_create_new_group.png | Bin 32934 -> 0 bytes .../img/jira_create_new_group_name.png | Bin 9054 -> 0 bytes doc/project_services/img/jira_create_new_user.png | Bin 21081 -> 0 bytes doc/project_services/img/jira_group_access.png | Bin 32210 -> 0 bytes doc/project_services/img/jira_issue_closed.png | Bin 77028 -> 0 bytes doc/project_services/img/jira_issue_reference.png | Bin 36188 -> 0 bytes doc/project_services/img/jira_issues_workflow.png | Bin 87067 -> 0 bytes .../img/jira_merge_request_close.png | Bin 102835 -> 0 bytes doc/project_services/img/jira_project_name.png | Bin 41572 -> 0 bytes ...jira_reference_commit_message_in_jira_issue.png | Bin 33706 -> 0 bytes doc/project_services/img/jira_service.png | Bin 56834 -> 0 bytes .../img/jira_service_close_issue.png | Bin 79569 -> 0 bytes doc/project_services/img/jira_service_page.png | Bin 36280 -> 0 bytes .../img/jira_submit_gitlab_merge_request.png | Bin 51913 -> 0 bytes .../img/jira_user_management_link.png | Bin 43095 -> 0 bytes .../img/jira_workflow_screenshot.png | Bin 111093 -> 0 bytes doc/project_services/jira.md | 247 +-------------------- doc/project_services/project_services.md | 2 +- 23 files changed, 198 insertions(+), 249 deletions(-) delete mode 100644 doc/project_services/img/jira_add_gitlab_commit_message.png delete mode 100644 doc/project_services/img/jira_add_user_to_group.png delete mode 100644 doc/project_services/img/jira_create_new_group.png delete mode 100644 doc/project_services/img/jira_create_new_group_name.png delete mode 100644 doc/project_services/img/jira_create_new_user.png delete mode 100644 doc/project_services/img/jira_group_access.png delete mode 100644 doc/project_services/img/jira_issue_closed.png delete mode 100644 doc/project_services/img/jira_issue_reference.png delete mode 100644 doc/project_services/img/jira_issues_workflow.png delete mode 100644 doc/project_services/img/jira_merge_request_close.png delete mode 100644 doc/project_services/img/jira_project_name.png delete mode 100644 doc/project_services/img/jira_reference_commit_message_in_jira_issue.png delete mode 100644 doc/project_services/img/jira_service.png delete mode 100644 doc/project_services/img/jira_service_close_issue.png delete mode 100644 doc/project_services/img/jira_service_page.png delete mode 100644 doc/project_services/img/jira_submit_gitlab_merge_request.png delete mode 100644 doc/project_services/img/jira_user_management_link.png delete mode 100644 doc/project_services/img/jira_workflow_screenshot.png diff --git a/doc/integration/README.md b/doc/integration/README.md index ae4387e2577..417dc874516 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -5,7 +5,7 @@ trackers and external authentication. See the documentation below for details on how to configure these services. -- [Jira](../project_services/jira.md) Integrate with the JIRA issue tracker +- [JIRA](jira.md) Integrate with the JIRA issue tracker - [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc. - [LDAP](ldap.md) Set up sign in via LDAP - [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd and Azure diff --git a/doc/integration/jira.md b/doc/integration/jira.md index 78aa6634116..2e31fd994de 100644 --- a/doc/integration/jira.md +++ b/doc/integration/jira.md @@ -1,3 +1,197 @@ # GitLab JIRA integration -This document was moved under [project_services/jira](../project_services/jira.md). +GitLab can be configured to interact with JIRA. Configuration happens via +user name and password. Connecting to a JIRA server via CAS is not possible. + +Each project can be configured to connect to a different JIRA instance, see the +[configuration](#configuration) section. If you have one JIRA instance you can +pre-fill the settings page with a default template. To configure the template +see the [Services Templates][services-templates] document. + +Once the project is connected to JIRA, you can reference and close the issues +in JIRA directly from GitLab. + +## Table of Contents +* [Referencing JIRA Issues from GitLab](#referencing-JIRA-issues) +* [Closing JIRA Issues from GitLab](#closing-JIRA-issues) +* [Configuration](#configuration) + +### Referencing JIRA Issues + +When GitLab project has JIRA issue tracker configured and enabled, mentioning +JIRA issue in GitLab will automatically add a comment in JIRA issue with the +link back to GitLab. This means that in comments in merge requests and commits +referencing an issue, eg. `PROJECT-7`, will add a comment in JIRA issue in the +format: + +``` + USER mentioned this issue in RESOURCE_NAME of [PROJECT_NAME|LINK_TO_COMMENT]: + ENTITY_TITLE +``` + +* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab. +* `LINK_TO_THE_COMMENT` Link to the origin of mention with a name of the entity where JIRA issue was mentioned. +* `RESOURCE_NAME` Kind of resource which referenced the issue. Can be a commit or merge request. +* `PROJECT_NAME` GitLab project name. +* `ENTITY_TITLE` Merge request title or commit message first line. + +![example of mentioning or closing the JIRA issue](img/jira_issue_reference.png) + +--- + +### Closing JIRA Issues + +JIRA issues can be closed directly from GitLab by using trigger words, eg. +`Resolves PROJECT-1`, `Closes PROJECT-1` or `Fixes PROJECT-1`, in commits and +merge requests. When a commit which contains the trigger word in the commit +message is pushed, GitLab will add a comment in the mentioned JIRA issue. + +For example, for project named `PROJECT` in JIRA, we implemented a new feature +and created a merge request in GitLab. + +This feature was requested in JIRA issue `PROJECT-7`. Merge request in GitLab +contains the improvement and in merge request description we say that this +merge request `Closes PROJECT-7` issue. + +Once this merge request is merged, the JIRA issue will be automatically closed +with a link to the commit that resolved the issue. + +![A Git commit that causes the JIRA issue to be closed](img/jira_merge_request_close.png) + +--- + +![The GitLab integration user leaves a comment on JIRA](img/jira_service_close_issue.png) + +--- + +## Configuration + +### Configuring JIRA + +We need to create a user in JIRA which will have access to all projects that +need to integrate with GitLab. Login to your JIRA instance as admin and under +Administration go to User Management and create a new user. + +As an example, we'll create a user named `gitlab` and add it to `JIRA-developers` +group. + +**It is important that the user `GitLab` has write-access to projects in JIRA** + +We have split this stage in steps so it is easier to follow. + +--- + +1. Login to your JIRA instance as an administrator and under **Administration** + go to **User Management** to create a new user. + + ![JIRA user management link](img/jira_user_management_link.png) + + --- + +1. The next step is to create a new user (e.g., `gitlab`) who has write access + to projects in JIRA. Enter the user's name and a _valid_ e-mail address + since JIRA sends a verification e-mail to set-up the password. + _**Note:** JIRA creates the username automatically by using the e-mail + prefix. You can change it later if you want._ + + ![JIRA create new user](img/jira_create_new_user.png) + + --- + +1. Now, let's create a `gitlab-developers` group which will have write access + to projects in JIRA. Go to the **Groups** tab and select **Create group**. + + ![JIRA create new user](img/jira_create_new_group.png) + + --- + + Give it an optional description and hit **Create group**. + + ![jira create new group](img/jira_create_new_group_name.png) + + --- + +1. Give the newly-created group write access by going to + **Application access > View configuration** and adding the `gitlab-developers` + group to JIRA Core. + + ![JIRA group access](img/jira_group_access.png) + + --- + +1. Add the `gitlab` user to the `gitlab-developers` group by going to + **Users > GitLab user > Add group** and selecting the `gitlab-developers` + group from the dropdown menu. Notice that the group says _Access_ which is + what we aim for. + + ![JIRA add user to group](img/jira_add_user_to_group.png) + +--- + +The JIRA configuration is over. Write down the new JIRA username and its +password as they will be needed when configuring GitLab in the next section. + +### Configuring GitLab + +JIRA configuration in GitLab is done via a project's **Services**. + +#### GitLab 8.13.0 with JIRA v1000.x + +To enable JIRA integration in a project, navigate to the project's +and open the context menu clicking on the top right gear icon, then go to +**Services > JIRA**. + +Fill in the required details on the page as described in the table below. + +| Field | Description | +| ----- | ----------- | +| `URL` | The base URL to the JIRA project which is being linked to this GitLab project. Ex. https://JIRA.example.com | +| `Project key` | The short, all capital letter identifier for your JIRA project. | +| `Username` | The user name created in [configuring JIRA step](#configuring-JIRA). | +| `Password` |The password of the user created in [configuring JIRA step](#configuring-JIRA). | +| `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). | + +After saving the configuration, your GitLab project will be able to interact +with the linked JIRA project. + +![JIRA service page](img/jira_service_page.png) + +--- + +#### GitLab 6.x-7.7 with JIRA v6.x + +_**Note:** GitLab versions 8.13.0 and up contain various integration improvements. +We strongly recommend upgrading._ + +In `gitlab.yml` enable the JIRA issue tracker section by +[uncommenting these lines][JIRA-gitlab-yml]. This will make sure that all +issues within GitLab are pointing to the JIRA issue tracker. + +After you set this, you will be able to close issues in JIRA by a commit in +GitLab. + +Go to your project's **Settings** page and fill in the project name for the +JIRA project: + +![Set the JIRA project name in GitLab to 'NEW'](img/jira_project_name.png) + +--- + +You can also enable the JIRA service that will allow you to interact with JIRA +issues. Go to the **Settings > Services > JIRA** and: + +1. Tick the active check box to enable the service +1. Supply the URL to JIRA server, for example http://JIRA.example.com +1. Supply the username of a user we created under `Configuring JIRA` section, + for example `gitlab` +1. Supply the password of the user +1. Optional: supply the JIRA API version, default is version `2` +1. Optional: supply the JIRA issue transition ID (issue transition to closed). + This is dependent on JIRA settings, default is `2` +1. Hit save + + +![JIRA services page](img/jira_service.png) + +[services-templates]: ../project_services/services_templates.md +[JIRA-gitlab-yml]: https://gitlab.com/subscribers/gitlab-ee/blob/6-8-stable-ee/config/gitlab.yml.example#L111-115 diff --git a/doc/project_services/img/builds_emails_service.png b/doc/project_services/img/builds_emails_service.png index 88943dc410e..440728795be 100644 Binary files a/doc/project_services/img/builds_emails_service.png and b/doc/project_services/img/builds_emails_service.png differ diff --git a/doc/project_services/img/jira_add_gitlab_commit_message.png b/doc/project_services/img/jira_add_gitlab_commit_message.png deleted file mode 100644 index aec472b9118..00000000000 Binary files a/doc/project_services/img/jira_add_gitlab_commit_message.png and /dev/null differ diff --git a/doc/project_services/img/jira_add_user_to_group.png b/doc/project_services/img/jira_add_user_to_group.png deleted file mode 100644 index 0ba737bda9a..00000000000 Binary files a/doc/project_services/img/jira_add_user_to_group.png and /dev/null differ diff --git a/doc/project_services/img/jira_create_new_group.png b/doc/project_services/img/jira_create_new_group.png deleted file mode 100644 index 0609060cb05..00000000000 Binary files a/doc/project_services/img/jira_create_new_group.png and /dev/null differ diff --git a/doc/project_services/img/jira_create_new_group_name.png b/doc/project_services/img/jira_create_new_group_name.png deleted file mode 100644 index 53d77b17df0..00000000000 Binary files a/doc/project_services/img/jira_create_new_group_name.png and /dev/null differ diff --git a/doc/project_services/img/jira_create_new_user.png b/doc/project_services/img/jira_create_new_user.png deleted file mode 100644 index 9eaa444ed25..00000000000 Binary files a/doc/project_services/img/jira_create_new_user.png and /dev/null differ diff --git a/doc/project_services/img/jira_group_access.png b/doc/project_services/img/jira_group_access.png deleted file mode 100644 index 8d4657427ae..00000000000 Binary files a/doc/project_services/img/jira_group_access.png and /dev/null differ diff --git a/doc/project_services/img/jira_issue_closed.png b/doc/project_services/img/jira_issue_closed.png deleted file mode 100644 index acdd83702d3..00000000000 Binary files a/doc/project_services/img/jira_issue_closed.png and /dev/null differ diff --git a/doc/project_services/img/jira_issue_reference.png b/doc/project_services/img/jira_issue_reference.png deleted file mode 100644 index 1a2d9f04a6c..00000000000 Binary files a/doc/project_services/img/jira_issue_reference.png and /dev/null differ diff --git a/doc/project_services/img/jira_issues_workflow.png b/doc/project_services/img/jira_issues_workflow.png deleted file mode 100644 index 0703081d77b..00000000000 Binary files a/doc/project_services/img/jira_issues_workflow.png and /dev/null differ diff --git a/doc/project_services/img/jira_merge_request_close.png b/doc/project_services/img/jira_merge_request_close.png deleted file mode 100644 index 47785e3ba27..00000000000 Binary files a/doc/project_services/img/jira_merge_request_close.png and /dev/null differ diff --git a/doc/project_services/img/jira_project_name.png b/doc/project_services/img/jira_project_name.png deleted file mode 100644 index e785ec6140d..00000000000 Binary files a/doc/project_services/img/jira_project_name.png and /dev/null differ diff --git a/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png b/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png deleted file mode 100644 index fb270d85e3c..00000000000 Binary files a/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png and /dev/null differ diff --git a/doc/project_services/img/jira_service.png b/doc/project_services/img/jira_service.png deleted file mode 100644 index 13aefce6f84..00000000000 Binary files a/doc/project_services/img/jira_service.png and /dev/null differ diff --git a/doc/project_services/img/jira_service_close_issue.png b/doc/project_services/img/jira_service_close_issue.png deleted file mode 100644 index eed69e80d2c..00000000000 Binary files a/doc/project_services/img/jira_service_close_issue.png and /dev/null differ diff --git a/doc/project_services/img/jira_service_page.png b/doc/project_services/img/jira_service_page.png deleted file mode 100644 index a5b49c501ba..00000000000 Binary files a/doc/project_services/img/jira_service_page.png and /dev/null differ diff --git a/doc/project_services/img/jira_submit_gitlab_merge_request.png b/doc/project_services/img/jira_submit_gitlab_merge_request.png deleted file mode 100644 index 77630d39d39..00000000000 Binary files a/doc/project_services/img/jira_submit_gitlab_merge_request.png and /dev/null differ diff --git a/doc/project_services/img/jira_user_management_link.png b/doc/project_services/img/jira_user_management_link.png deleted file mode 100644 index 5f002b59bac..00000000000 Binary files a/doc/project_services/img/jira_user_management_link.png and /dev/null differ diff --git a/doc/project_services/img/jira_workflow_screenshot.png b/doc/project_services/img/jira_workflow_screenshot.png deleted file mode 100644 index 937a50a77d9..00000000000 Binary files a/doc/project_services/img/jira_workflow_screenshot.png and /dev/null differ diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index b626c746c79..2ea1c58cb31 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -1,246 +1 @@ -# GitLab JIRA integration - ->**Note:** -Full JIRA integration was previously exclusive to GitLab Enterprise Edition. -With [GitLab 8.3 forward][8_3_post], this feature in now [backported][jira-ce] -to GitLab Community Edition as well. - ---- - -GitLab can be configured to interact with [JIRA Core] either using an -on-premises instance or the SaaS solution that Atlassian offers. Configuration -happens via username and password on a per-project basis. Connecting to a JIRA -server via CAS is not possible. - -Each project can be configured to connect to a different JIRA instance or, in -case you have a single JIRA instance, you can pre-fill the JIRA service -settings page in GitLab with a default template. To configure the JIRA template, -see the [Services Templates documentation][services-templates]. - -Once the GitLab project is connected to JIRA, you can reference and close the -issues in JIRA directly from GitLab's merge requests. - -## Configuration - -The configuration consists of two parts: - -- [JIRA configuration](#configuring-jira) -- [GitLab configuration](#configuring-gitlab) - -### Configuring JIRA - -First things first, we need to create a user in JIRA which will have access to -all projects that need to integrate with GitLab. - -We have split this stage in steps so it is easier to follow. - ---- - -1. Login to your JIRA instance as an administrator and under **Administration** - go to **User Management** to create a new user. - - ![JIRA user management link](img/jira_user_management_link.png) - - --- - -1. The next step is to create a new user (e.g., `gitlab`) who has write access - to projects in JIRA. Enter the user's name and a _valid_ e-mail address - since JIRA sends a verification e-mail to set-up the password. - _**Note:** JIRA creates the username automatically by using the e-mail - prefix. You can change it later if you want._ - - ![JIRA create new user](img/jira_create_new_user.png) - - --- - -1. Now, let's create a `gitlab-developers` group which will have write access - to projects in JIRA. Go to the **Groups** tab and select **Create group**. - - ![JIRA create new user](img/jira_create_new_group.png) - - --- - - Give it an optional description and hit **Create group**. - - ![JIRA create new group](img/jira_create_new_group_name.png) - - --- - -1. Give the newly-created group write access by going to - **Application access > View configuration** and adding the `gitlab-developers` - group to JIRA Core. - - ![JIRA group access](img/jira_group_access.png) - - --- - -1. Add the `gitlab` user to the `gitlab-developers` group by going to - **Users > GitLab user > Add group** and selecting the `gitlab-developers` - group from the dropdown menu. Notice that the group says _Access_ which is - what we aim for. - - ![JIRA add user to group](img/jira_add_user_to_group.png) - ---- - -The JIRA configuration is over. Write down the new JIRA username and its -password as they will be needed when configuring GitLab in the next section. - -### Configuring GitLab - ->**Note:** -The currently supported JIRA versions are v6.x and v7.x. and GitLab -7.8 or higher is required. - ---- - -Assuming you [have already configured JIRA](#configuring-jira), now it's time -to configure GitLab. - -JIRA configuration in GitLab is done via a project's -[**Services**](../project_services/project_services.md). - -To enable JIRA integration in a project, navigate to the project's -**Settings > Services > JIRA**. - -Fill in the required details on the page, as described in the table below. - -| Setting | Description | -| ------- | ----------- | -| `Description` | A name for the issue tracker (to differentiate between instances, for example). | -| `Project url` | The URL to the JIRA project which is being linked to this GitLab project. It is of the form: `https://<jira_host_url>/issues/?jql=project=<jira_project>`. | -| `Issues url` | The URL to the JIRA project issues overview for the project that is linked to this GitLab project. It is of the form: `https://<jira_host_url>/browse/:id`. Leave `:id` as-is, it gets replaced by GitLab at runtime. | -| `New issue url` | This is the URL to create a new issue in JIRA for the project linked to this GitLab project, and it is of the form: `https://<jira_host_url>/secure/CreateIssue.jspa` | -| `Api url` | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`. It is of the form: `https://<jira_host_url>/rest/api/2`. | -| `Username` | The username of the user created in [configuring JIRA step](#configuring-jira). | -| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | -| `JIRA issue transition` | This setting is very important to set up correctly. It is the ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. | - -After saving the configuration, your GitLab project will be able to interact -with the linked JIRA project. - -For example, given the settings below: - -- the JIRA URL is `https://jira.example.com` -- the project is named `GITLAB` -- the user is named `gitlab` -- the JIRA issue transition is 151 (based on the [JIRA issue transition][trans]) - -the following screenshot shows how the JIRA service settings should look like. - -![JIRA service page](img/jira_service_page.png) - -[trans]: img/jira_issues_workflow.png - ---- - -## JIRA issues - -By now you should have [configured JIRA](#configuring-jira) and enabled the -[JIRA service in GitLab](#configuring-gitlab). If everything is set up correctly -you should be able to reference and close JIRA issues by just mentioning their -ID in GitLab commits and merge requests. - -### Referencing JIRA Issues - -If you reference a JIRA issue, e.g., `GITLAB-1`, in a commit comment, a link -which points back to JIRA is created. - -The same works for comments in merge requests as well. - -![JIRA add GitLab commit message](img/jira_add_gitlab_commit_message.png) - ---- - -The mentioning action is two-fold, so a comment with a JIRA issue in GitLab -will automatically add a comment in that particular JIRA issue with the link -back to GitLab. - - -![JIRA reference commit message](img/jira_reference_commit_message_in_jira_issue.png) - ---- - -The comment on the JIRA issue is of the form: - -> USER mentioned this issue in LINK_TO_THE_MENTION - -Where: - -| Format | Description | -| ------ | ----------- | -| `USER` | A user that mentioned the issue. This is the link to the user profile in GitLab. | -| `LINK_TO_THE_MENTION` | Link to the origin of mention with a name of the entity where JIRA issue was mentioned. Can be commit or merge request. | - -### Closing JIRA issues - -JIRA issues can be closed directly from GitLab by using trigger words in -commits and merge requests. When a commit which contains the trigger word -followed by the JIRA issue ID in the commit message is pushed, GitLab will -add a comment in the mentioned JIRA issue and immediately close it (provided -the transition ID was set up correctly). - -There are currently three trigger words, and you can use either one to achieve -the same goal: - -- `Resolves GITLAB-1` -- `Closes GITLAB-1` -- `Fixes GITLAB-1` - -where `GITLAB-1` the issue ID of the JIRA project. - -### JIRA issue closing example - -Let's say for example that we submitted a bug fix and created a merge request -in GitLab. The workflow would be something like this: - -1. Create a new branch -1. Fix the bug -1. Commit the changes and push branch to GitLab -1. Open a new merge request and reference the JIRA issue including one of the - trigger words, e.g.: `Fixes GITLAB-1`, in the description -1. Submit the merge request -1. Ask someone to review -1. Merge the merge request -1. The JIRA issue is automatically closed - ---- - -In the following screenshot you can see what the link references to the JIRA -issue look like. - -![JIRA - submit a GitLab merge request](img/jira_submit_gitlab_merge_request.png) - ---- - -Once this merge request is merged, the JIRA issue will be automatically closed -with a link to the commit that resolved the issue. - -![The GitLab integration user leaves a comment on JIRA](img/jira_issue_closed.png) - ---- - -You can see from the above image that there are four references to GitLab: - -- The first is from a comment in a specific commit -- The second is from the JIRA issue reference in the merge request description -- The third is from the actual commit that solved the issue -- And the fourth is from the commit that the merge request created - -[services-templates]: ../project_services/services_templates.md "Services templates documentation" -[JIRA Core]: https://www.atlassian.com/software/jira/core "The JIRA Core website" -[jira-ce]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2146 "MR - Backport JIRA service" -[8_3_post]: https://about.gitlab.com/2015/12/22/gitlab-8-3-released/ "GitLab 8.3 release post" - -## Troubleshooting - -### GitLab is unable to comment on a ticket - -Make sure that the user you set up for GitLab to communicate with JIRA has the -correct access permission to post comments on a ticket and to also transition the -ticket, if you'd like GitLab to also take care of closing them. - -### GitLab is unable to close a ticket - -Make sure the the `Transition ID` you set within the JIRA settings matches the -one your project needs to close a ticket. +GitLab JIRA integration documentation has moved to [here](../integration/jira.md). diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index 890f7525b0e..6b0fc685fd5 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -40,7 +40,7 @@ further configuration instructions and details. Contributions are welcome. | Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities | | [HipChat](hipchat.md) | Private group chat and IM | | [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway | -| [JIRA](jira.md) | JIRA issue tracker | +| [JIRA](../integration/jira.md) | JIRA issue tracker | | JetBrains TeamCity CI | A continuous integration and build server | | [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands | | PivotalTracker | Project Management Software (Source Commits Endpoint) | -- cgit v1.2.1 From d85d9533b1b60cc17796a7a471192d4b37f41473 Mon Sep 17 00:00:00 2001 From: Felipe Artur <felipefac@gmail.com> Date: Thu, 17 Nov 2016 16:10:25 -0200 Subject: Add remove links to JIRA documentation --- doc/integration/img/jira_issue_reference.png | Bin 36188 -> 49151 bytes doc/integration/img/jira_service_close_comment.png | Bin 0 -> 29716 bytes doc/integration/img/jira_service_close_issue.png | Bin 79569 -> 89666 bytes doc/integration/jira.md | 7 +++++-- 4 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 doc/integration/img/jira_service_close_comment.png diff --git a/doc/integration/img/jira_issue_reference.png b/doc/integration/img/jira_issue_reference.png index 1a2d9f04a6c..463200da6aa 100644 Binary files a/doc/integration/img/jira_issue_reference.png and b/doc/integration/img/jira_issue_reference.png differ diff --git a/doc/integration/img/jira_service_close_comment.png b/doc/integration/img/jira_service_close_comment.png new file mode 100644 index 00000000000..84a71c692b1 Binary files /dev/null and b/doc/integration/img/jira_service_close_comment.png differ diff --git a/doc/integration/img/jira_service_close_issue.png b/doc/integration/img/jira_service_close_issue.png index eed69e80d2c..b033b210469 100644 Binary files a/doc/integration/img/jira_service_close_issue.png and b/doc/integration/img/jira_service_close_issue.png differ diff --git a/doc/integration/jira.md b/doc/integration/jira.md index 2e31fd994de..4c1b8b2bdd3 100644 --- a/doc/integration/jira.md +++ b/doc/integration/jira.md @@ -54,16 +54,19 @@ contains the improvement and in merge request description we say that this merge request `Closes PROJECT-7` issue. Once this merge request is merged, the JIRA issue will be automatically closed -with a link to the commit that resolved the issue. +with a comment and a associated link to the commit that resolved the issue. ![A Git commit that causes the JIRA issue to be closed](img/jira_merge_request_close.png) --- -![The GitLab integration user leaves a comment on JIRA](img/jira_service_close_issue.png) +![The GitLab integration closes JIRA issue](img/jira_service_close_issue.png) --- +![The GitLab integration creates a comment and a link on JIRA issue.](img/jira_service_close_comment.png) + + ## Configuration ### Configuring JIRA -- cgit v1.2.1 From a478a1dde4e02fcddc41d4e799fed70031162cd0 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Tue, 22 Nov 2016 13:05:58 +0100 Subject: Move JIRA service doc back to its old location --- doc/integration/README.md | 2 +- doc/integration/img/jira_add_user_to_group.png | Bin 41994 -> 0 bytes doc/integration/img/jira_create_new_group.png | Bin 32934 -> 0 bytes doc/integration/img/jira_create_new_group_name.png | Bin 9054 -> 0 bytes doc/integration/img/jira_create_new_user.png | Bin 21081 -> 0 bytes doc/integration/img/jira_group_access.png | Bin 32210 -> 0 bytes doc/integration/img/jira_issue_reference.png | Bin 49151 -> 0 bytes doc/integration/img/jira_merge_request_close.png | Bin 52556 -> 0 bytes doc/integration/img/jira_project_name.png | Bin 41572 -> 0 bytes doc/integration/img/jira_service.png | Bin 56834 -> 0 bytes doc/integration/img/jira_service_close_comment.png | Bin 29716 -> 0 bytes doc/integration/img/jira_service_close_issue.png | Bin 89666 -> 0 bytes doc/integration/img/jira_service_page.png | Bin 45089 -> 0 bytes doc/integration/img/jira_user_management_link.png | Bin 43095 -> 0 bytes doc/integration/img/jira_workflow_screenshot.png | Bin 111093 -> 0 bytes doc/integration/jira.md | 200 -------------------- .../img/jira_add_user_to_group.png | Bin 0 -> 41994 bytes doc/project_services/img/jira_create_new_group.png | Bin 0 -> 32934 bytes .../img/jira_create_new_group_name.png | Bin 0 -> 9054 bytes doc/project_services/img/jira_create_new_user.png | Bin 0 -> 21081 bytes doc/project_services/img/jira_group_access.png | Bin 0 -> 32210 bytes doc/project_services/img/jira_issue_reference.png | Bin 0 -> 49151 bytes .../img/jira_merge_request_close.png | Bin 0 -> 52556 bytes doc/project_services/img/jira_project_name.png | Bin 0 -> 41572 bytes doc/project_services/img/jira_service.png | Bin 0 -> 56834 bytes .../img/jira_service_close_comment.png | Bin 0 -> 29716 bytes .../img/jira_service_close_issue.png | Bin 0 -> 89666 bytes doc/project_services/img/jira_service_page.png | Bin 0 -> 45089 bytes .../img/jira_user_management_link.png | Bin 0 -> 43095 bytes .../img/jira_workflow_screenshot.png | Bin 0 -> 111093 bytes doc/project_services/jira.md | 201 ++++++++++++++++++++- doc/project_services/project_services.md | 2 +- 32 files changed, 202 insertions(+), 203 deletions(-) delete mode 100644 doc/integration/img/jira_add_user_to_group.png delete mode 100644 doc/integration/img/jira_create_new_group.png delete mode 100644 doc/integration/img/jira_create_new_group_name.png delete mode 100644 doc/integration/img/jira_create_new_user.png delete mode 100644 doc/integration/img/jira_group_access.png delete mode 100644 doc/integration/img/jira_issue_reference.png delete mode 100644 doc/integration/img/jira_merge_request_close.png delete mode 100644 doc/integration/img/jira_project_name.png delete mode 100644 doc/integration/img/jira_service.png delete mode 100644 doc/integration/img/jira_service_close_comment.png delete mode 100644 doc/integration/img/jira_service_close_issue.png delete mode 100644 doc/integration/img/jira_service_page.png delete mode 100644 doc/integration/img/jira_user_management_link.png delete mode 100644 doc/integration/img/jira_workflow_screenshot.png delete mode 100644 doc/integration/jira.md create mode 100644 doc/project_services/img/jira_add_user_to_group.png create mode 100644 doc/project_services/img/jira_create_new_group.png create mode 100644 doc/project_services/img/jira_create_new_group_name.png create mode 100644 doc/project_services/img/jira_create_new_user.png create mode 100644 doc/project_services/img/jira_group_access.png create mode 100644 doc/project_services/img/jira_issue_reference.png create mode 100644 doc/project_services/img/jira_merge_request_close.png create mode 100644 doc/project_services/img/jira_project_name.png create mode 100644 doc/project_services/img/jira_service.png create mode 100644 doc/project_services/img/jira_service_close_comment.png create mode 100644 doc/project_services/img/jira_service_close_issue.png create mode 100644 doc/project_services/img/jira_service_page.png create mode 100644 doc/project_services/img/jira_user_management_link.png create mode 100644 doc/project_services/img/jira_workflow_screenshot.png diff --git a/doc/integration/README.md b/doc/integration/README.md index 417dc874516..f8ffa6dcb7f 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -5,7 +5,7 @@ trackers and external authentication. See the documentation below for details on how to configure these services. -- [JIRA](jira.md) Integrate with the JIRA issue tracker +- [JIRA](../project_services/jira.md) Integrate with the JIRA issue tracker - [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc. - [LDAP](ldap.md) Set up sign in via LDAP - [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd and Azure diff --git a/doc/integration/img/jira_add_user_to_group.png b/doc/integration/img/jira_add_user_to_group.png deleted file mode 100644 index 0ba737bda9a..00000000000 Binary files a/doc/integration/img/jira_add_user_to_group.png and /dev/null differ diff --git a/doc/integration/img/jira_create_new_group.png b/doc/integration/img/jira_create_new_group.png deleted file mode 100644 index 0609060cb05..00000000000 Binary files a/doc/integration/img/jira_create_new_group.png and /dev/null differ diff --git a/doc/integration/img/jira_create_new_group_name.png b/doc/integration/img/jira_create_new_group_name.png deleted file mode 100644 index 53d77b17df0..00000000000 Binary files a/doc/integration/img/jira_create_new_group_name.png and /dev/null differ diff --git a/doc/integration/img/jira_create_new_user.png b/doc/integration/img/jira_create_new_user.png deleted file mode 100644 index 9eaa444ed25..00000000000 Binary files a/doc/integration/img/jira_create_new_user.png and /dev/null differ diff --git a/doc/integration/img/jira_group_access.png b/doc/integration/img/jira_group_access.png deleted file mode 100644 index 8d4657427ae..00000000000 Binary files a/doc/integration/img/jira_group_access.png and /dev/null differ diff --git a/doc/integration/img/jira_issue_reference.png b/doc/integration/img/jira_issue_reference.png deleted file mode 100644 index 463200da6aa..00000000000 Binary files a/doc/integration/img/jira_issue_reference.png and /dev/null differ diff --git a/doc/integration/img/jira_merge_request_close.png b/doc/integration/img/jira_merge_request_close.png deleted file mode 100644 index b8f6058a514..00000000000 Binary files a/doc/integration/img/jira_merge_request_close.png and /dev/null differ diff --git a/doc/integration/img/jira_project_name.png b/doc/integration/img/jira_project_name.png deleted file mode 100644 index e785ec6140d..00000000000 Binary files a/doc/integration/img/jira_project_name.png and /dev/null differ diff --git a/doc/integration/img/jira_service.png b/doc/integration/img/jira_service.png deleted file mode 100644 index 13aefce6f84..00000000000 Binary files a/doc/integration/img/jira_service.png and /dev/null differ diff --git a/doc/integration/img/jira_service_close_comment.png b/doc/integration/img/jira_service_close_comment.png deleted file mode 100644 index 84a71c692b1..00000000000 Binary files a/doc/integration/img/jira_service_close_comment.png and /dev/null differ diff --git a/doc/integration/img/jira_service_close_issue.png b/doc/integration/img/jira_service_close_issue.png deleted file mode 100644 index b033b210469..00000000000 Binary files a/doc/integration/img/jira_service_close_issue.png and /dev/null differ diff --git a/doc/integration/img/jira_service_page.png b/doc/integration/img/jira_service_page.png deleted file mode 100644 index 0cc160bebe2..00000000000 Binary files a/doc/integration/img/jira_service_page.png and /dev/null differ diff --git a/doc/integration/img/jira_user_management_link.png b/doc/integration/img/jira_user_management_link.png deleted file mode 100644 index 5f002b59bac..00000000000 Binary files a/doc/integration/img/jira_user_management_link.png and /dev/null differ diff --git a/doc/integration/img/jira_workflow_screenshot.png b/doc/integration/img/jira_workflow_screenshot.png deleted file mode 100644 index 937a50a77d9..00000000000 Binary files a/doc/integration/img/jira_workflow_screenshot.png and /dev/null differ diff --git a/doc/integration/jira.md b/doc/integration/jira.md deleted file mode 100644 index 4c1b8b2bdd3..00000000000 --- a/doc/integration/jira.md +++ /dev/null @@ -1,200 +0,0 @@ -# GitLab JIRA integration - -GitLab can be configured to interact with JIRA. Configuration happens via -user name and password. Connecting to a JIRA server via CAS is not possible. - -Each project can be configured to connect to a different JIRA instance, see the -[configuration](#configuration) section. If you have one JIRA instance you can -pre-fill the settings page with a default template. To configure the template -see the [Services Templates][services-templates] document. - -Once the project is connected to JIRA, you can reference and close the issues -in JIRA directly from GitLab. - -## Table of Contents -* [Referencing JIRA Issues from GitLab](#referencing-JIRA-issues) -* [Closing JIRA Issues from GitLab](#closing-JIRA-issues) -* [Configuration](#configuration) - -### Referencing JIRA Issues - -When GitLab project has JIRA issue tracker configured and enabled, mentioning -JIRA issue in GitLab will automatically add a comment in JIRA issue with the -link back to GitLab. This means that in comments in merge requests and commits -referencing an issue, eg. `PROJECT-7`, will add a comment in JIRA issue in the -format: - -``` - USER mentioned this issue in RESOURCE_NAME of [PROJECT_NAME|LINK_TO_COMMENT]: - ENTITY_TITLE -``` - -* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab. -* `LINK_TO_THE_COMMENT` Link to the origin of mention with a name of the entity where JIRA issue was mentioned. -* `RESOURCE_NAME` Kind of resource which referenced the issue. Can be a commit or merge request. -* `PROJECT_NAME` GitLab project name. -* `ENTITY_TITLE` Merge request title or commit message first line. - -![example of mentioning or closing the JIRA issue](img/jira_issue_reference.png) - ---- - -### Closing JIRA Issues - -JIRA issues can be closed directly from GitLab by using trigger words, eg. -`Resolves PROJECT-1`, `Closes PROJECT-1` or `Fixes PROJECT-1`, in commits and -merge requests. When a commit which contains the trigger word in the commit -message is pushed, GitLab will add a comment in the mentioned JIRA issue. - -For example, for project named `PROJECT` in JIRA, we implemented a new feature -and created a merge request in GitLab. - -This feature was requested in JIRA issue `PROJECT-7`. Merge request in GitLab -contains the improvement and in merge request description we say that this -merge request `Closes PROJECT-7` issue. - -Once this merge request is merged, the JIRA issue will be automatically closed -with a comment and a associated link to the commit that resolved the issue. - -![A Git commit that causes the JIRA issue to be closed](img/jira_merge_request_close.png) - ---- - -![The GitLab integration closes JIRA issue](img/jira_service_close_issue.png) - ---- - -![The GitLab integration creates a comment and a link on JIRA issue.](img/jira_service_close_comment.png) - - -## Configuration - -### Configuring JIRA - -We need to create a user in JIRA which will have access to all projects that -need to integrate with GitLab. Login to your JIRA instance as admin and under -Administration go to User Management and create a new user. - -As an example, we'll create a user named `gitlab` and add it to `JIRA-developers` -group. - -**It is important that the user `GitLab` has write-access to projects in JIRA** - -We have split this stage in steps so it is easier to follow. - ---- - -1. Login to your JIRA instance as an administrator and under **Administration** - go to **User Management** to create a new user. - - ![JIRA user management link](img/jira_user_management_link.png) - - --- - -1. The next step is to create a new user (e.g., `gitlab`) who has write access - to projects in JIRA. Enter the user's name and a _valid_ e-mail address - since JIRA sends a verification e-mail to set-up the password. - _**Note:** JIRA creates the username automatically by using the e-mail - prefix. You can change it later if you want._ - - ![JIRA create new user](img/jira_create_new_user.png) - - --- - -1. Now, let's create a `gitlab-developers` group which will have write access - to projects in JIRA. Go to the **Groups** tab and select **Create group**. - - ![JIRA create new user](img/jira_create_new_group.png) - - --- - - Give it an optional description and hit **Create group**. - - ![jira create new group](img/jira_create_new_group_name.png) - - --- - -1. Give the newly-created group write access by going to - **Application access > View configuration** and adding the `gitlab-developers` - group to JIRA Core. - - ![JIRA group access](img/jira_group_access.png) - - --- - -1. Add the `gitlab` user to the `gitlab-developers` group by going to - **Users > GitLab user > Add group** and selecting the `gitlab-developers` - group from the dropdown menu. Notice that the group says _Access_ which is - what we aim for. - - ![JIRA add user to group](img/jira_add_user_to_group.png) - ---- - -The JIRA configuration is over. Write down the new JIRA username and its -password as they will be needed when configuring GitLab in the next section. - -### Configuring GitLab - -JIRA configuration in GitLab is done via a project's **Services**. - -#### GitLab 8.13.0 with JIRA v1000.x - -To enable JIRA integration in a project, navigate to the project's -and open the context menu clicking on the top right gear icon, then go to -**Services > JIRA**. - -Fill in the required details on the page as described in the table below. - -| Field | Description | -| ----- | ----------- | -| `URL` | The base URL to the JIRA project which is being linked to this GitLab project. Ex. https://JIRA.example.com | -| `Project key` | The short, all capital letter identifier for your JIRA project. | -| `Username` | The user name created in [configuring JIRA step](#configuring-JIRA). | -| `Password` |The password of the user created in [configuring JIRA step](#configuring-JIRA). | -| `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). | - -After saving the configuration, your GitLab project will be able to interact -with the linked JIRA project. - -![JIRA service page](img/jira_service_page.png) - ---- - -#### GitLab 6.x-7.7 with JIRA v6.x - -_**Note:** GitLab versions 8.13.0 and up contain various integration improvements. -We strongly recommend upgrading._ - -In `gitlab.yml` enable the JIRA issue tracker section by -[uncommenting these lines][JIRA-gitlab-yml]. This will make sure that all -issues within GitLab are pointing to the JIRA issue tracker. - -After you set this, you will be able to close issues in JIRA by a commit in -GitLab. - -Go to your project's **Settings** page and fill in the project name for the -JIRA project: - -![Set the JIRA project name in GitLab to 'NEW'](img/jira_project_name.png) - ---- - -You can also enable the JIRA service that will allow you to interact with JIRA -issues. Go to the **Settings > Services > JIRA** and: - -1. Tick the active check box to enable the service -1. Supply the URL to JIRA server, for example http://JIRA.example.com -1. Supply the username of a user we created under `Configuring JIRA` section, - for example `gitlab` -1. Supply the password of the user -1. Optional: supply the JIRA API version, default is version `2` -1. Optional: supply the JIRA issue transition ID (issue transition to closed). - This is dependent on JIRA settings, default is `2` -1. Hit save - - -![JIRA services page](img/jira_service.png) - -[services-templates]: ../project_services/services_templates.md -[JIRA-gitlab-yml]: https://gitlab.com/subscribers/gitlab-ee/blob/6-8-stable-ee/config/gitlab.yml.example#L111-115 diff --git a/doc/project_services/img/jira_add_user_to_group.png b/doc/project_services/img/jira_add_user_to_group.png new file mode 100644 index 00000000000..0ba737bda9a Binary files /dev/null and b/doc/project_services/img/jira_add_user_to_group.png differ diff --git a/doc/project_services/img/jira_create_new_group.png b/doc/project_services/img/jira_create_new_group.png new file mode 100644 index 00000000000..0609060cb05 Binary files /dev/null and b/doc/project_services/img/jira_create_new_group.png differ diff --git a/doc/project_services/img/jira_create_new_group_name.png b/doc/project_services/img/jira_create_new_group_name.png new file mode 100644 index 00000000000..53d77b17df0 Binary files /dev/null and b/doc/project_services/img/jira_create_new_group_name.png differ diff --git a/doc/project_services/img/jira_create_new_user.png b/doc/project_services/img/jira_create_new_user.png new file mode 100644 index 00000000000..9eaa444ed25 Binary files /dev/null and b/doc/project_services/img/jira_create_new_user.png differ diff --git a/doc/project_services/img/jira_group_access.png b/doc/project_services/img/jira_group_access.png new file mode 100644 index 00000000000..8d4657427ae Binary files /dev/null and b/doc/project_services/img/jira_group_access.png differ diff --git a/doc/project_services/img/jira_issue_reference.png b/doc/project_services/img/jira_issue_reference.png new file mode 100644 index 00000000000..463200da6aa Binary files /dev/null and b/doc/project_services/img/jira_issue_reference.png differ diff --git a/doc/project_services/img/jira_merge_request_close.png b/doc/project_services/img/jira_merge_request_close.png new file mode 100644 index 00000000000..b8f6058a514 Binary files /dev/null and b/doc/project_services/img/jira_merge_request_close.png differ diff --git a/doc/project_services/img/jira_project_name.png b/doc/project_services/img/jira_project_name.png new file mode 100644 index 00000000000..e785ec6140d Binary files /dev/null and b/doc/project_services/img/jira_project_name.png differ diff --git a/doc/project_services/img/jira_service.png b/doc/project_services/img/jira_service.png new file mode 100644 index 00000000000..13aefce6f84 Binary files /dev/null and b/doc/project_services/img/jira_service.png differ diff --git a/doc/project_services/img/jira_service_close_comment.png b/doc/project_services/img/jira_service_close_comment.png new file mode 100644 index 00000000000..84a71c692b1 Binary files /dev/null and b/doc/project_services/img/jira_service_close_comment.png differ diff --git a/doc/project_services/img/jira_service_close_issue.png b/doc/project_services/img/jira_service_close_issue.png new file mode 100644 index 00000000000..b033b210469 Binary files /dev/null and b/doc/project_services/img/jira_service_close_issue.png differ diff --git a/doc/project_services/img/jira_service_page.png b/doc/project_services/img/jira_service_page.png new file mode 100644 index 00000000000..0cc160bebe2 Binary files /dev/null and b/doc/project_services/img/jira_service_page.png differ diff --git a/doc/project_services/img/jira_user_management_link.png b/doc/project_services/img/jira_user_management_link.png new file mode 100644 index 00000000000..5f002b59bac Binary files /dev/null and b/doc/project_services/img/jira_user_management_link.png differ diff --git a/doc/project_services/img/jira_workflow_screenshot.png b/doc/project_services/img/jira_workflow_screenshot.png new file mode 100644 index 00000000000..937a50a77d9 Binary files /dev/null and b/doc/project_services/img/jira_workflow_screenshot.png differ diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index 2ea1c58cb31..4c1b8b2bdd3 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -1 +1,200 @@ -GitLab JIRA integration documentation has moved to [here](../integration/jira.md). +# GitLab JIRA integration + +GitLab can be configured to interact with JIRA. Configuration happens via +user name and password. Connecting to a JIRA server via CAS is not possible. + +Each project can be configured to connect to a different JIRA instance, see the +[configuration](#configuration) section. If you have one JIRA instance you can +pre-fill the settings page with a default template. To configure the template +see the [Services Templates][services-templates] document. + +Once the project is connected to JIRA, you can reference and close the issues +in JIRA directly from GitLab. + +## Table of Contents +* [Referencing JIRA Issues from GitLab](#referencing-JIRA-issues) +* [Closing JIRA Issues from GitLab](#closing-JIRA-issues) +* [Configuration](#configuration) + +### Referencing JIRA Issues + +When GitLab project has JIRA issue tracker configured and enabled, mentioning +JIRA issue in GitLab will automatically add a comment in JIRA issue with the +link back to GitLab. This means that in comments in merge requests and commits +referencing an issue, eg. `PROJECT-7`, will add a comment in JIRA issue in the +format: + +``` + USER mentioned this issue in RESOURCE_NAME of [PROJECT_NAME|LINK_TO_COMMENT]: + ENTITY_TITLE +``` + +* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab. +* `LINK_TO_THE_COMMENT` Link to the origin of mention with a name of the entity where JIRA issue was mentioned. +* `RESOURCE_NAME` Kind of resource which referenced the issue. Can be a commit or merge request. +* `PROJECT_NAME` GitLab project name. +* `ENTITY_TITLE` Merge request title or commit message first line. + +![example of mentioning or closing the JIRA issue](img/jira_issue_reference.png) + +--- + +### Closing JIRA Issues + +JIRA issues can be closed directly from GitLab by using trigger words, eg. +`Resolves PROJECT-1`, `Closes PROJECT-1` or `Fixes PROJECT-1`, in commits and +merge requests. When a commit which contains the trigger word in the commit +message is pushed, GitLab will add a comment in the mentioned JIRA issue. + +For example, for project named `PROJECT` in JIRA, we implemented a new feature +and created a merge request in GitLab. + +This feature was requested in JIRA issue `PROJECT-7`. Merge request in GitLab +contains the improvement and in merge request description we say that this +merge request `Closes PROJECT-7` issue. + +Once this merge request is merged, the JIRA issue will be automatically closed +with a comment and a associated link to the commit that resolved the issue. + +![A Git commit that causes the JIRA issue to be closed](img/jira_merge_request_close.png) + +--- + +![The GitLab integration closes JIRA issue](img/jira_service_close_issue.png) + +--- + +![The GitLab integration creates a comment and a link on JIRA issue.](img/jira_service_close_comment.png) + + +## Configuration + +### Configuring JIRA + +We need to create a user in JIRA which will have access to all projects that +need to integrate with GitLab. Login to your JIRA instance as admin and under +Administration go to User Management and create a new user. + +As an example, we'll create a user named `gitlab` and add it to `JIRA-developers` +group. + +**It is important that the user `GitLab` has write-access to projects in JIRA** + +We have split this stage in steps so it is easier to follow. + +--- + +1. Login to your JIRA instance as an administrator and under **Administration** + go to **User Management** to create a new user. + + ![JIRA user management link](img/jira_user_management_link.png) + + --- + +1. The next step is to create a new user (e.g., `gitlab`) who has write access + to projects in JIRA. Enter the user's name and a _valid_ e-mail address + since JIRA sends a verification e-mail to set-up the password. + _**Note:** JIRA creates the username automatically by using the e-mail + prefix. You can change it later if you want._ + + ![JIRA create new user](img/jira_create_new_user.png) + + --- + +1. Now, let's create a `gitlab-developers` group which will have write access + to projects in JIRA. Go to the **Groups** tab and select **Create group**. + + ![JIRA create new user](img/jira_create_new_group.png) + + --- + + Give it an optional description and hit **Create group**. + + ![jira create new group](img/jira_create_new_group_name.png) + + --- + +1. Give the newly-created group write access by going to + **Application access > View configuration** and adding the `gitlab-developers` + group to JIRA Core. + + ![JIRA group access](img/jira_group_access.png) + + --- + +1. Add the `gitlab` user to the `gitlab-developers` group by going to + **Users > GitLab user > Add group** and selecting the `gitlab-developers` + group from the dropdown menu. Notice that the group says _Access_ which is + what we aim for. + + ![JIRA add user to group](img/jira_add_user_to_group.png) + +--- + +The JIRA configuration is over. Write down the new JIRA username and its +password as they will be needed when configuring GitLab in the next section. + +### Configuring GitLab + +JIRA configuration in GitLab is done via a project's **Services**. + +#### GitLab 8.13.0 with JIRA v1000.x + +To enable JIRA integration in a project, navigate to the project's +and open the context menu clicking on the top right gear icon, then go to +**Services > JIRA**. + +Fill in the required details on the page as described in the table below. + +| Field | Description | +| ----- | ----------- | +| `URL` | The base URL to the JIRA project which is being linked to this GitLab project. Ex. https://JIRA.example.com | +| `Project key` | The short, all capital letter identifier for your JIRA project. | +| `Username` | The user name created in [configuring JIRA step](#configuring-JIRA). | +| `Password` |The password of the user created in [configuring JIRA step](#configuring-JIRA). | +| `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). | + +After saving the configuration, your GitLab project will be able to interact +with the linked JIRA project. + +![JIRA service page](img/jira_service_page.png) + +--- + +#### GitLab 6.x-7.7 with JIRA v6.x + +_**Note:** GitLab versions 8.13.0 and up contain various integration improvements. +We strongly recommend upgrading._ + +In `gitlab.yml` enable the JIRA issue tracker section by +[uncommenting these lines][JIRA-gitlab-yml]. This will make sure that all +issues within GitLab are pointing to the JIRA issue tracker. + +After you set this, you will be able to close issues in JIRA by a commit in +GitLab. + +Go to your project's **Settings** page and fill in the project name for the +JIRA project: + +![Set the JIRA project name in GitLab to 'NEW'](img/jira_project_name.png) + +--- + +You can also enable the JIRA service that will allow you to interact with JIRA +issues. Go to the **Settings > Services > JIRA** and: + +1. Tick the active check box to enable the service +1. Supply the URL to JIRA server, for example http://JIRA.example.com +1. Supply the username of a user we created under `Configuring JIRA` section, + for example `gitlab` +1. Supply the password of the user +1. Optional: supply the JIRA API version, default is version `2` +1. Optional: supply the JIRA issue transition ID (issue transition to closed). + This is dependent on JIRA settings, default is `2` +1. Hit save + + +![JIRA services page](img/jira_service.png) + +[services-templates]: ../project_services/services_templates.md +[JIRA-gitlab-yml]: https://gitlab.com/subscribers/gitlab-ee/blob/6-8-stable-ee/config/gitlab.yml.example#L111-115 diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index 6b0fc685fd5..890f7525b0e 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -40,7 +40,7 @@ further configuration instructions and details. Contributions are welcome. | Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities | | [HipChat](hipchat.md) | Private group chat and IM | | [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway | -| [JIRA](../integration/jira.md) | JIRA issue tracker | +| [JIRA](jira.md) | JIRA issue tracker | | JetBrains TeamCity CI | A continuous integration and build server | | [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands | | PivotalTracker | Project Management Software (Source Commits Endpoint) | -- cgit v1.2.1 From 89e367f2e5c9e52e3b8f3e966b5e541d4e1cde72 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 22 Nov 2016 13:15:10 +0100 Subject: Add missing feature tests for loading build trace See #24638 --- spec/features/projects/builds_spec.rb | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb index f0ad936ca69..eb50d397b29 100644 --- a/spec/features/projects/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -181,12 +181,24 @@ feature 'Builds', :feature do visit namespace_project_build_path(project.namespace, project, build) end - it 'loads build trace' do - expect(page).to have_content 'BUILD TRACE' + context 'when build has an initial trace' do + it 'loads build trace' do + expect(page).to have_content 'BUILD TRACE' - build.append_trace(' and more trace', 11) + build.append_trace(' and more trace', 11) - expect(page).to have_content 'BUILD TRACE and more trace' + expect(page).to have_content 'BUILD TRACE and more trace' + end + end + + context 'when build does not have an initial trace' do + let(:build) { create(:ci_build, pipeline: pipeline) } + + it 'loads new trace' do + build.append_trace('build trace', 0) + + expect(page).to have_content 'build trace' + end end end -- cgit v1.2.1 From 07ce4c28b3c854a912a1b98d0715d0ed3f8c937d Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 22 Nov 2016 13:35:18 +0100 Subject: Improve reproducibility of build trace test example --- spec/features/projects/builds_spec.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb index eb50d397b29..a0ccc472d11 100644 --- a/spec/features/projects/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -6,8 +6,8 @@ feature 'Builds', :feature do let(:project) { create(:project) } let(:pipeline) { create(:ci_pipeline, project: project) } - let!(:build) { create(:ci_build, :trace, pipeline: pipeline) } - let!(:build2) { create(:ci_build) } + let(:build) { create(:ci_build, :trace, pipeline: pipeline) } + let(:build2) { create(:ci_build) } let(:artifacts_file) do fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') @@ -19,6 +19,8 @@ feature 'Builds', :feature do end describe "GET /:project/builds" do + let!(:build) { create(:ci_build, pipeline: pipeline) } + context "Pending scope" do before do visit namespace_project_builds_path(project.namespace, project, scope: :pending) -- cgit v1.2.1 From 6a18a78184293863eafc9b9b1a9e7fd5687a507c Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 22 Nov 2016 13:37:23 +0100 Subject: Include build trace status in data attribute always --- app/helpers/builds_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb index fde297c588e..9662ac7e341 100644 --- a/app/helpers/builds_helper.rb +++ b/app/helpers/builds_helper.rb @@ -12,7 +12,7 @@ module BuildsHelper build_url: namespace_project_build_url(@project.namespace, @project, @build, :json), build_status: @build.status, build_stage: @build.stage, - state1: @build.trace_with_state[:state] + state1: @build.trace_with_state[:state].to_s } end end -- cgit v1.2.1 From 22d16ccdb59b1604fd71ce5c8469ee7217bed693 Mon Sep 17 00:00:00 2001 From: Robert Schilling <rschilling@student.tugraz.at> Date: Tue, 22 Nov 2016 13:27:35 +0100 Subject: Document both valid cases for API iid filter --- doc/api/merge_requests.md | 3 ++- doc/api/milestones.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 66d91732cc6..4cc385e36fe 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -10,7 +10,8 @@ The pagination parameters `page` and `per_page` can be used to restrict the list GET /projects/:id/merge_requests GET /projects/:id/merge_requests?state=opened GET /projects/:id/merge_requests?state=all -GET /projects/:id/merge_requests?iid[]=42 +GET /projects/:id/merge_requests?iid=42 +GET /projects/:id/merge_requests?iid[]=42&iid[]=43 ``` Parameters: diff --git a/doc/api/milestones.md b/doc/api/milestones.md index ad80d7c4c01..28e4b3105a6 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -6,7 +6,8 @@ Returns a list of project milestones. ``` GET /projects/:id/milestones -GET /projects/:id/milestones?iid[]=42 +GET /projects/:id/milestones?iid=42 +GET /projects/:id/milestones?iid[]=42&iid[]=43 GET /projects/:id/milestones?state=active GET /projects/:id/milestones?state=closed ``` -- cgit v1.2.1 From 0914efbfceb32c0d303b5113bc13f86cc1b120cd Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 22 Nov 2016 13:40:43 +0100 Subject: Add Changelog entry for build trace exceptions fix --- changelogs/unreleased/fix-build-without-trace-exceptions.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-build-without-trace-exceptions.yml diff --git a/changelogs/unreleased/fix-build-without-trace-exceptions.yml b/changelogs/unreleased/fix-build-without-trace-exceptions.yml new file mode 100644 index 00000000000..3b95e96e212 --- /dev/null +++ b/changelogs/unreleased/fix-build-without-trace-exceptions.yml @@ -0,0 +1,4 @@ +--- +title: Fix exceptions when loading build trace +merge_request: 7658 +author: -- cgit v1.2.1 From 33980a0f0cc6c6690f500006d0cb7088a9642530 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Tue, 22 Nov 2016 14:11:48 +0100 Subject: Refactor JIRA docs and bring back some old information --- doc/project_services/img/jira_service_page.png | Bin 45089 -> 30309 bytes doc/project_services/jira.md | 195 +++++++++++++------------ 2 files changed, 101 insertions(+), 94 deletions(-) diff --git a/doc/project_services/img/jira_service_page.png b/doc/project_services/img/jira_service_page.png index 0cc160bebe2..1cda73be83d 100644 Binary files a/doc/project_services/img/jira_service_page.png and b/doc/project_services/img/jira_service_page.png differ diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index 4c1b8b2bdd3..e537a24028c 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -11,64 +11,11 @@ see the [Services Templates][services-templates] document. Once the project is connected to JIRA, you can reference and close the issues in JIRA directly from GitLab. -## Table of Contents -* [Referencing JIRA Issues from GitLab](#referencing-JIRA-issues) -* [Closing JIRA Issues from GitLab](#closing-JIRA-issues) -* [Configuration](#configuration) - -### Referencing JIRA Issues - -When GitLab project has JIRA issue tracker configured and enabled, mentioning -JIRA issue in GitLab will automatically add a comment in JIRA issue with the -link back to GitLab. This means that in comments in merge requests and commits -referencing an issue, eg. `PROJECT-7`, will add a comment in JIRA issue in the -format: - -``` - USER mentioned this issue in RESOURCE_NAME of [PROJECT_NAME|LINK_TO_COMMENT]: - ENTITY_TITLE -``` - -* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab. -* `LINK_TO_THE_COMMENT` Link to the origin of mention with a name of the entity where JIRA issue was mentioned. -* `RESOURCE_NAME` Kind of resource which referenced the issue. Can be a commit or merge request. -* `PROJECT_NAME` GitLab project name. -* `ENTITY_TITLE` Merge request title or commit message first line. - -![example of mentioning or closing the JIRA issue](img/jira_issue_reference.png) - ---- - -### Closing JIRA Issues - -JIRA issues can be closed directly from GitLab by using trigger words, eg. -`Resolves PROJECT-1`, `Closes PROJECT-1` or `Fixes PROJECT-1`, in commits and -merge requests. When a commit which contains the trigger word in the commit -message is pushed, GitLab will add a comment in the mentioned JIRA issue. - -For example, for project named `PROJECT` in JIRA, we implemented a new feature -and created a merge request in GitLab. - -This feature was requested in JIRA issue `PROJECT-7`. Merge request in GitLab -contains the improvement and in merge request description we say that this -merge request `Closes PROJECT-7` issue. - -Once this merge request is merged, the JIRA issue will be automatically closed -with a comment and a associated link to the commit that resolved the issue. - -![A Git commit that causes the JIRA issue to be closed](img/jira_merge_request_close.png) - ---- - -![The GitLab integration closes JIRA issue](img/jira_service_close_issue.png) - ---- - -![The GitLab integration creates a comment and a link on JIRA issue.](img/jira_service_close_comment.png) - - ## Configuration +In order to enable the JIRA service in GitLab, you need to first configure the +project in JIRA and then enter the correct values in GitLab. + ### Configuring JIRA We need to create a user in JIRA which will have access to all projects that @@ -115,7 +62,7 @@ We have split this stage in steps so it is easier to follow. --- 1. Give the newly-created group write access by going to - **Application access > View configuration** and adding the `gitlab-developers` + **Application access ➔ View configuration** and adding the `gitlab-developers` group to JIRA Core. ![JIRA group access](img/jira_group_access.png) @@ -123,7 +70,7 @@ We have split this stage in steps so it is easier to follow. --- 1. Add the `gitlab` user to the `gitlab-developers` group by going to - **Users > GitLab user > Add group** and selecting the `gitlab-developers` + **Users ➔ GitLab user ➔ Add group** and selecting the `gitlab-developers` group from the dropdown menu. Notice that the group says _Access_ which is what we aim for. @@ -136,22 +83,23 @@ password as they will be needed when configuring GitLab in the next section. ### Configuring GitLab -JIRA configuration in GitLab is done via a project's **Services**. - -#### GitLab 8.13.0 with JIRA v1000.x +>**Notes:** +- The currently supported JIRA versions are `v6.x` and `v7.x.`. GitLab 7.8 or + higher is required. +- GitLab 8.14 introduced a new way to integrate with JIRA which greatly simplified + the configuration options you have to enter. If you are using an older version, + [follow this documentation][jira-repo-docs]. -To enable JIRA integration in a project, navigate to the project's -and open the context menu clicking on the top right gear icon, then go to -**Services > JIRA**. - -Fill in the required details on the page as described in the table below. +To enable JIRA integration in a project, navigate to your project's +**Services ➔ JIRA** and fill in the required details on the page as described +in the table below. | Field | Description | | ----- | ----------- | -| `URL` | The base URL to the JIRA project which is being linked to this GitLab project. Ex. https://JIRA.example.com | -| `Project key` | The short, all capital letter identifier for your JIRA project. | -| `Username` | The user name created in [configuring JIRA step](#configuring-JIRA). | -| `Password` |The password of the user created in [configuring JIRA step](#configuring-JIRA). | +| `URL` | The base URL to the JIRA project which is being linked to this GitLab project. E.g., `https://jira.example.com`. | +| `Project key` | The short, the identifier for your JIRA project, all uppercase. | +| `Username` | The user name created in [configuring JIRA step](#configuring-jira). | +| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | | `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). | After saving the configuration, your GitLab project will be able to interact @@ -161,40 +109,99 @@ with the linked JIRA project. --- -#### GitLab 6.x-7.7 with JIRA v6.x +## JIRA issues + +By now you should have [configured JIRA](#configuring-jira) and enabled the +[JIRA service in GitLab](#configuring-gitlab). If everything is set up correctly +you should be able to reference and close JIRA issues by just mentioning their +ID in GitLab commits and merge requests. + +### Referencing JIRA Issues + +When GitLab project has JIRA issue tracker configured and enabled, mentioning +JIRA issue in GitLab will automatically add a comment in JIRA issue with the +link back to GitLab. This means that in comments in merge requests and commits +referencing an issue, eg. `PROJECT-7`, will add a comment in JIRA issue in the +format: + +``` +USER mentioned this issue in RESOURCE_NAME of [PROJECT_NAME|LINK_TO_COMMENT]: +ENTITY_TITLE +``` + +* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab. +* `LINK_TO_THE_COMMENT` Link to the origin of mention with a name of the entity where JIRA issue was mentioned. +* `RESOURCE_NAME` Kind of resource which referenced the issue. Can be a commit or merge request. +* `PROJECT_NAME` GitLab project name. +* `ENTITY_TITLE` Merge request title or commit message first line. + +![example of mentioning or closing the JIRA issue](img/jira_issue_reference.png) + +--- + +### Closing JIRA Issues + +JIRA issues can be closed directly from GitLab by using trigger words in +commits and merge requests. When a commit which contains the trigger word +followed by the JIRA issue ID in the commit message is pushed, GitLab will +add a comment in the mentioned JIRA issue and immediately close it (provided +the transition ID was set up correctly). + +There are currently three trigger words, and you can use either one to achieve +the same goal: -_**Note:** GitLab versions 8.13.0 and up contain various integration improvements. -We strongly recommend upgrading._ +- `Resolves GITLAB-1` +- `Closes GITLAB-1` +- `Fixes GITLAB-1` -In `gitlab.yml` enable the JIRA issue tracker section by -[uncommenting these lines][JIRA-gitlab-yml]. This will make sure that all -issues within GitLab are pointing to the JIRA issue tracker. +- where `GITLAB-1` the issue ID of the JIRA project. -After you set this, you will be able to close issues in JIRA by a commit in -GitLab. +### JIRA issue closing example -Go to your project's **Settings** page and fill in the project name for the -JIRA project: +Let's consider the following example: -![Set the JIRA project name in GitLab to 'NEW'](img/jira_project_name.png) +1. For the project named `PROJECT` in JIRA, we implemented a new feature + and created a merge request in GitLab. +1. This feature was requested in JIRA issue `PROJECT-7` and the merge request + in GitLab contains the improvement +1. In the merge request description we use the issue closing trigger + `Closes PROJECT-7`. +1. Once the merge request is merged, the JIRA issue will be automatically closed + with a comment and an associated link to the commit that resolved the issue. + +--- + +In the following screenshot you can see what the link references to the JIRA +issue look like. + +![A Git commit that causes the JIRA issue to be closed](img/jira_merge_request_close.png) --- -You can also enable the JIRA service that will allow you to interact with JIRA -issues. Go to the **Settings > Services > JIRA** and: +Once this merge request is merged, the JIRA issue will be automatically closed +with a link to the commit that resolved the issue. + +![The GitLab integration closes JIRA issue](img/jira_service_close_issue.png) + +--- + +![The GitLab integration creates a comment and a link on JIRA issue.](img/jira_service_close_comment.png) + +## Troubleshooting + +If things don't work as expected that's usually because you have configured +incorrectly the JIRA-GitLab integration. + +### GitLab is unable to comment on a ticket -1. Tick the active check box to enable the service -1. Supply the URL to JIRA server, for example http://JIRA.example.com -1. Supply the username of a user we created under `Configuring JIRA` section, - for example `gitlab` -1. Supply the password of the user -1. Optional: supply the JIRA API version, default is version `2` -1. Optional: supply the JIRA issue transition ID (issue transition to closed). - This is dependent on JIRA settings, default is `2` -1. Hit save +Make sure that the user you set up for GitLab to communicate with JIRA has the +correct access permission to post comments on a ticket and to also transition +the ticket, if you'd like GitLab to also take care of closing them. +### GitLab is unable to close a ticket -![JIRA services page](img/jira_service.png) +Make sure the `Transition ID` you set within the JIRA settings matches the one +your project needs to close a ticket. [services-templates]: ../project_services/services_templates.md -[JIRA-gitlab-yml]: https://gitlab.com/subscribers/gitlab-ee/blob/6-8-stable-ee/config/gitlab.yml.example#L111-115 +[jira-repo-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/project_services/jira.md -- cgit v1.2.1 From c78a0bcc00fe442dead38f3ccd97aecc6b4d181e Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Tue, 22 Nov 2016 14:12:09 +0100 Subject: Change documentation link in JIRA service --- app/models/project_services/jira_service.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index aeded715893..70bbbbcda85 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -57,9 +57,9 @@ class JiraService < IssueTrackerService end def help - 'See the ' \ - '[integration doc](http://doc.gitlab.com/ce/integration/external-issue-tracker.html) '\ - 'for details.' + 'You need to configure JIRA before enabling this service. For more details + read the + [JIRA service documentation](https://docs.gitlab.com/ce/project_services/jira.html).' end def title -- cgit v1.2.1 From de24902852454a7805c30912eaca32903f491aa7 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 22 Nov 2016 14:48:14 +0100 Subject: Improve name of build log state data attribute --- app/assets/javascripts/build.js | 2 +- app/helpers/builds_helper.rb | 2 +- spec/javascripts/fixtures/build.html.haml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index e198306e67a..116a47b0907 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -12,7 +12,7 @@ this.pageUrl = options.pageUrl; this.buildUrl = options.buildUrl; this.buildStatus = options.buildStatus; - this.state = options.state1; + this.state = options.logState; this.buildStage = options.buildStage; this.updateDropdown = bind(this.updateDropdown, this); this.$document = $(document); diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb index 9662ac7e341..9fc69e12266 100644 --- a/app/helpers/builds_helper.rb +++ b/app/helpers/builds_helper.rb @@ -12,7 +12,7 @@ module BuildsHelper build_url: namespace_project_build_url(@project.namespace, @project, @build, :json), build_status: @build.status, build_stage: @build.stage, - state1: @build.trace_with_state[:state].to_s + log_state: @build.trace_with_state[:state].to_s } end end diff --git a/spec/javascripts/fixtures/build.html.haml b/spec/javascripts/fixtures/build.html.haml index 27136beb14c..06b49516e5c 100644 --- a/spec/javascripts/fixtures/build.html.haml +++ b/spec/javascripts/fixtures/build.html.haml @@ -54,7 +54,7 @@ build_url: 'http://example.com/root/test-build/builds/2.json', build_status: 'passed', build_stage: 'test', - state1: 'buildstate' }} + log_state: 'buildstate' }} %p.build-detail-row The artifacts will be removed in -- cgit v1.2.1 From 5586ff5784a917840c7069e70a521e13e66749c1 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Tue, 22 Nov 2016 13:12:23 +0100 Subject: Handle orphans when removing soft deleted groups There may be more tables but these were the tables that were problematic for GitLab.com due to foreign key constraints (without cascading deletes). --- .../20161117114805_remove_undeleted_groups.rb | 41 ++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/db/migrate/20161117114805_remove_undeleted_groups.rb b/db/migrate/20161117114805_remove_undeleted_groups.rb index ebc2d974ae0..696914f8e4d 100644 --- a/db/migrate/20161117114805_remove_undeleted_groups.rb +++ b/db/migrate/20161117114805_remove_undeleted_groups.rb @@ -5,6 +5,47 @@ class RemoveUndeletedGroups < ActiveRecord::Migration DOWNTIME = false def up + execute <<-EOF.strip_heredoc + DELETE FROM projects + WHERE namespace_id IN ( + SELECT id FROM ( + SELECT id + FROM namespaces + WHERE deleted_at IS NOT NULL + ) namespace_ids + ); + EOF + + if defined?(Gitlab::License) + # EE adds these columns but we have to make sure this data is cleaned up + # here before we run the DELETE below. An alternative would be patching + # this migration in EE but this will only result in a mess and confusing + # migrations. + execute <<-EOF.strip_heredoc + DELETE FROM protected_branch_push_access_levels + WHERE group_id IN ( + SELECT id FROM ( + SELECT id + FROM namespaces + WHERE deleted_at IS NOT NULL + ) namespace_ids + ); + EOF + + execute <<-EOF.strip_heredoc + DELETE FROM protected_branch_merge_access_levels + WHERE group_id IN ( + SELECT id FROM ( + SELECT id + FROM namespaces + WHERE deleted_at IS NOT NULL + ) namespace_ids + ); + EOF + end + + # This removes namespaces that were supposed to be soft deleted but still + # reside in the database. execute "DELETE FROM namespaces WHERE deleted_at IS NOT NULL;" end -- cgit v1.2.1 From 02d1adecc39e50d2dbaf71fcdd3b2636c0d87856 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra <dimitriehoekstra@gmail.com> Date: Tue, 22 Nov 2016 15:00:24 +0100 Subject: added proper changelog entry --- ...4266-Afraid-to-press-the-Orange-button-on-Merge-request-screen.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/24266-Afraid-to-press-the-Orange-button-on-Merge-request-screen.yml diff --git a/changelogs/unreleased/24266-Afraid-to-press-the-Orange-button-on-Merge-request-screen.yml b/changelogs/unreleased/24266-Afraid-to-press-the-Orange-button-on-Merge-request-screen.yml new file mode 100644 index 00000000000..28ca20c7dcc --- /dev/null +++ b/changelogs/unreleased/24266-Afraid-to-press-the-Orange-button-on-Merge-request-screen.yml @@ -0,0 +1,4 @@ +--- +title: If Build running change accept merge request when build succeeds button from orange to blue +merge_request: 7577 +author: -- cgit v1.2.1 From e4c282c06724c93c494a6fee2bfa71a8fedad056 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra <dimitriehoekstra@gmail.com> Date: Tue, 22 Nov 2016 15:02:23 +0100 Subject: deleted old changelog entry --- .../24266-Afraid to press the Orange button on Merge request screen | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 changelogs/unreleased/24266-Afraid to press the Orange button on Merge request screen diff --git a/changelogs/unreleased/24266-Afraid to press the Orange button on Merge request screen b/changelogs/unreleased/24266-Afraid to press the Orange button on Merge request screen deleted file mode 100644 index 28ca20c7dcc..00000000000 --- a/changelogs/unreleased/24266-Afraid to press the Orange button on Merge request screen +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: If Build running change accept merge request when build succeeds button from orange to blue -merge_request: 7577 -author: -- cgit v1.2.1 From db62eb957157ffe14a474f5f858c753cade067b1 Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Tue, 22 Nov 2016 15:05:37 +0100 Subject: fixed bug to do with calculating durations --- app/serializers/analytics_build_entity.rb | 2 +- app/serializers/entity_date_helper.rb | 2 +- spec/serializers/analytics_build_entity_spec.rb | 10 +++++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/serializers/analytics_build_entity.rb b/app/serializers/analytics_build_entity.rb index 5fdf2bbf7c3..abefcd5cc02 100644 --- a/app/serializers/analytics_build_entity.rb +++ b/app/serializers/analytics_build_entity.rb @@ -13,7 +13,7 @@ class AnalyticsBuildEntity < Grape::Entity end expose :duration, as: :total_time do |build| - distance_of_time_as_hash(build[:duration].to_f) + distance_of_time_as_hash(build.duration.to_f) end expose :branch do diff --git a/app/serializers/entity_date_helper.rb b/app/serializers/entity_date_helper.rb index b333b3344c3..918abba8d99 100644 --- a/app/serializers/entity_date_helper.rb +++ b/app/serializers/entity_date_helper.rb @@ -2,7 +2,7 @@ module EntityDateHelper include ActionView::Helpers::DateHelper def interval_in_words(diff) - "#{distance_of_time_in_words(diff.to_f)} ago" + "#{distance_of_time_in_words(Time.now, diff)} ago" end # Converts seconds into a hash such as: diff --git a/spec/serializers/analytics_build_entity_spec.rb b/spec/serializers/analytics_build_entity_spec.rb index 9ac6f20fd3c..a802c0fa49d 100644 --- a/spec/serializers/analytics_build_entity_spec.rb +++ b/spec/serializers/analytics_build_entity_spec.rb @@ -7,7 +7,7 @@ describe AnalyticsBuildEntity do context 'build with an author' do let(:user) { create(:user) } - let(:build) { create(:ci_build, author: user) } + let(:build) { create(:ci_build, author: user, started_at: 2.hours.ago, finished_at: 1.hour.ago) } subject { entity.as_json } @@ -23,5 +23,13 @@ describe AnalyticsBuildEntity do expect(subject).not_to include(/token/) expect(subject).not_to include(/variables/) end + + it 'contains the right started at' do + expect(subject[:date]).to eq('about 2 hours ago') + end + + it 'contains the duration' do + expect(subject[:total_time]).to eq({ :hours => 1 }) + end end end -- cgit v1.2.1 From d7bd8f128e08da9f4aa9d395d430bb35e4a5e3eb Mon Sep 17 00:00:00 2001 From: Adam Niedzielski <adamsunday@gmail.com> Date: Tue, 22 Nov 2016 15:21:01 +0100 Subject: Do not use MergeRequest#commits in define_pipelines_vars MergeRequest#commits requires merge_request_diff to be present. This is not the case when creating a new merge request. --- app/controllers/projects/merge_requests_controller.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index dbbd2ad849e..e24a670631f 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -563,11 +563,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController def define_pipelines_vars @pipelines = @merge_request.all_pipelines - - if @pipelines.present? && @merge_request.commits.present? - @pipeline = @pipelines.first - @statuses = @pipeline.statuses.relevant - end + @pipeline = @merge_request.pipeline + @statuses = @pipeline.statuses.relevant if @pipeline.present? end def define_new_vars -- cgit v1.2.1 From 42855f116abf6822f3e7d6923bbc296f35054067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Tue, 22 Nov 2016 16:12:47 +0100 Subject: Fix wrong template rendered when CI/CD settings aren't update successfully MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- app/controllers/projects/pipelines_settings_controller.rb | 2 +- ...-be-render-show-in-projects-pipelinessettingscontroller-update.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/24804-wrong-render-index-should-be-render-show-in-projects-pipelinessettingscontroller-update.yml diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb index 9136633b87a..53ce23221ed 100644 --- a/app/controllers/projects/pipelines_settings_controller.rb +++ b/app/controllers/projects/pipelines_settings_controller.rb @@ -17,7 +17,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController flash[:notice] = "CI/CD Pipelines settings for '#{@project.name}' were successfully updated." redirect_to namespace_project_pipelines_settings_path(@project.namespace, @project) else - render 'index' + render 'show' end end diff --git a/changelogs/unreleased/24804-wrong-render-index-should-be-render-show-in-projects-pipelinessettingscontroller-update.yml b/changelogs/unreleased/24804-wrong-render-index-should-be-render-show-in-projects-pipelinessettingscontroller-update.yml new file mode 100644 index 00000000000..92dbbe3d164 --- /dev/null +++ b/changelogs/unreleased/24804-wrong-render-index-should-be-render-show-in-projects-pipelinessettingscontroller-update.yml @@ -0,0 +1,4 @@ +--- +title: Fix wrong template rendered when CI/CD settings aren't update successfully +merge_request: 7665 +author: -- cgit v1.2.1 From ed85fa7b10ed2214c849b7d0d0b3164414afaa16 Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Tue, 22 Nov 2016 16:30:27 +0100 Subject: fix rubocop warning --- spec/serializers/analytics_build_entity_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/serializers/analytics_build_entity_spec.rb b/spec/serializers/analytics_build_entity_spec.rb index a802c0fa49d..c0b7e86b17c 100644 --- a/spec/serializers/analytics_build_entity_spec.rb +++ b/spec/serializers/analytics_build_entity_spec.rb @@ -29,7 +29,7 @@ describe AnalyticsBuildEntity do end it 'contains the duration' do - expect(subject[:total_time]).to eq({ :hours => 1 }) + expect(subject[:total_time]).to eq(hours: 1 ) end end end -- cgit v1.2.1 From c9084654a134906f2442a505bfdc1c2832cdf05f Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Tue, 22 Nov 2016 16:33:02 +0100 Subject: Add new image for Cycle Analytics [ci skip] --- doc/user/project/cycle_analytics.md | 9 +++------ .../project/img/cycle_analytics_landing_page.png | Bin 66080 -> 42122 bytes 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md index 1892ccabb70..86fe52ef4ff 100644 --- a/doc/user/project/cycle_analytics.md +++ b/doc/user/project/cycle_analytics.md @@ -1,10 +1,7 @@ # Cycle Analytics -> [Introduced][ce-5986] in GitLab 8.12. -> -> **Note:** -There are more changes coming to Cycle Analytics, you can follow the following -issue to track the changes to this feature: [#20975][ce-20975]. +> [Introduced][ce-5986] in GitLab 8.12. Further features were added in GitLab + 8.14. Cycle Analytics measures the time it takes to go from an [idea to production] for each project you have. This is achieved by not only indicating the total time it @@ -16,7 +13,7 @@ calculates a separate median for each stage. ## Overview -You can find the Cycle Analytics page under your project's **Pipelines > Cycle +You can find the Cycle Analytics page under your project's **Pipelines ➔ Cycle Analytics** tab. ![Cycle Analytics landing page](img/cycle_analytics_landing_page.png) diff --git a/doc/user/project/img/cycle_analytics_landing_page.png b/doc/user/project/img/cycle_analytics_landing_page.png index b212134d5ed..aab060ad0ed 100644 Binary files a/doc/user/project/img/cycle_analytics_landing_page.png and b/doc/user/project/img/cycle_analytics_landing_page.png differ -- cgit v1.2.1 From daf26fa0db66de0f3ef121ba362c5073c7f183e1 Mon Sep 17 00:00:00 2001 From: Semyon Pupkov <mail@semyonpupkov.com> Date: Sun, 20 Nov 2016 13:42:58 +0500 Subject: Refactor create service spec before: 1 minute 11.81 seconds after: 52.47 seconds --- .../unreleased/refactor-create-service-spec.yml | 4 + spec/services/projects/create_service_spec.rb | 212 ++++++++++----------- 2 files changed, 110 insertions(+), 106 deletions(-) create mode 100644 changelogs/unreleased/refactor-create-service-spec.yml diff --git a/changelogs/unreleased/refactor-create-service-spec.yml b/changelogs/unreleased/refactor-create-service-spec.yml new file mode 100644 index 00000000000..148a0fee02c --- /dev/null +++ b/changelogs/unreleased/refactor-create-service-spec.yml @@ -0,0 +1,4 @@ +--- +title: Refactor create service spec +merge_request: 7609 +author: Semyon Pupkov diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index fbd22560d6e..a1539b69401 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -1,150 +1,150 @@ require 'spec_helper' -describe Projects::CreateService, services: true do - describe :create_by_user do - before do - @user = create :user - @opts = { - name: "GitLab", - namespace: @user.namespace - } - end +describe Projects::CreateService, '#execute', services: true do + let(:user) { create :user } + let(:opts) do + { + name: "GitLab", + namespace: user.namespace + } + end - it 'creates labels on Project creation if there are templates' do - Label.create(title: "bug", template: true) - project = create_project(@user, @opts) - project.reload + it 'creates labels on Project creation if there are templates' do + Label.create(title: "bug", template: true) + project = create_project(user, opts) - expect(project.labels).not_to be_empty - end + expect(project.labels).not_to be_empty + end - context 'user namespace' do - before do - @project = create_project(@user, @opts) - end + context 'user namespace' do + it do + project = create_project(user, opts) - it { expect(@project).to be_valid } - it { expect(@project.owner).to eq(@user) } - it { expect(@project.team.masters).to include(@user) } - it { expect(@project.namespace).to eq(@user.namespace) } + expect(project).to be_valid + expect(project.owner).to eq(user) + expect(project.team.masters).to include(user) + expect(project.namespace).to eq(user.namespace) end + end - context 'group namespace' do - before do - @group = create :group - @group.add_owner(@user) + context 'group namespace' do + let(:group) do + create(:group).tap do |group| + group.add_owner(user) + end + end - @user.refresh_authorized_projects # Ensure cache is warm + before do + user.refresh_authorized_projects # Ensure cache is warm + end - @opts.merge!(namespace_id: @group.id) - @project = create_project(@user, @opts) - end + it do + project = create_project(user, opts.merge!(namespace_id: group.id)) - it { expect(@project).to be_valid } - it { expect(@project.owner).to eq(@group) } - it { expect(@project.namespace).to eq(@group) } - it { expect(@user.authorized_projects).to include(@project) } + expect(project).to be_valid + expect(project.owner).to eq(group) + expect(project.namespace).to eq(group) + expect(user.authorized_projects).to include(project) end + end - context 'error handling' do - it 'handles invalid options' do - @opts.merge!({ default_branch: 'master' } ) - expect(create_project(@user, @opts)).to eq(nil) - end + context 'error handling' do + it 'handles invalid options' do + opts.merge!({ default_branch: 'master' } ) + expect(create_project(user, opts)).to eq(nil) end + end - context 'wiki_enabled creates repository directory' do - context 'wiki_enabled true creates wiki repository directory' do - before do - @project = create_project(@user, @opts) - @path = ProjectWiki.new(@project, @user).send(:path_to_repo) - end + context 'wiki_enabled creates repository directory' do + context 'wiki_enabled true creates wiki repository directory' do + it do + project = create_project(user, opts) + path = ProjectWiki.new(project, user).send(:path_to_repo) - it { expect(File.exist?(@path)).to be_truthy } + expect(File.exist?(path)).to be_truthy end + end - context 'wiki_enabled false does not create wiki repository directory' do - before do - @opts.merge!(wiki_enabled: false) - @project = create_project(@user, @opts) - @path = ProjectWiki.new(@project, @user).send(:path_to_repo) - end + context 'wiki_enabled false does not create wiki repository directory' do + it do + opts.merge!(wiki_enabled: false) + project = create_project(user, opts) + path = ProjectWiki.new(project, user).send(:path_to_repo) - it { expect(File.exist?(@path)).to be_falsey } + expect(File.exist?(path)).to be_falsey end end + end - context 'builds_enabled global setting' do - let(:project) { create_project(@user, @opts) } - - subject { project.builds_enabled? } + context 'builds_enabled global setting' do + let(:project) { create_project(user, opts) } - context 'global builds_enabled false does not enable CI by default' do - before do - project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED) - end + subject { project.builds_enabled? } - it { is_expected.to be_falsey } + context 'global builds_enabled false does not enable CI by default' do + before do + project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED) end - context 'global builds_enabled true does enable CI by default' do - before do - project.project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED) - end + it { is_expected.to be_falsey } + end - it { is_expected.to be_truthy } + context 'global builds_enabled true does enable CI by default' do + before do + project.project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED) end + + it { is_expected.to be_truthy } end + end - context 'restricted visibility level' do - before do - stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) + context 'restricted visibility level' do + before do + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) - @opts.merge!( - visibility_level: Gitlab::VisibilityLevel.options['Public'] - ) - end + opts.merge!( + visibility_level: Gitlab::VisibilityLevel.options['Public'] + ) + end - it 'does not allow a restricted visibility level for non-admins' do - project = create_project(@user, @opts) - expect(project).to respond_to(:errors) - expect(project.errors.messages).to have_key(:visibility_level) - expect(project.errors.messages[:visibility_level].first).to( - match('restricted by your GitLab administrator') - ) - end + it 'does not allow a restricted visibility level for non-admins' do + project = create_project(user, opts) + expect(project).to respond_to(:errors) + expect(project.errors.messages).to have_key(:visibility_level) + expect(project.errors.messages[:visibility_level].first).to( + match('restricted by your GitLab administrator') + ) + end - it 'allows a restricted visibility level for admins' do - admin = create(:admin) - project = create_project(admin, @opts) + it 'allows a restricted visibility level for admins' do + admin = create(:admin) + project = create_project(admin, opts) - expect(project.errors.any?).to be(false) - expect(project.saved?).to be(true) - end + expect(project.errors.any?).to be(false) + expect(project.saved?).to be(true) end + end - context 'repository creation' do - it 'synchronously creates the repository' do - expect_any_instance_of(Project).to receive(:create_repository) + context 'repository creation' do + it 'synchronously creates the repository' do + expect_any_instance_of(Project).to receive(:create_repository) - project = create_project(@user, @opts) - expect(project).to be_valid - expect(project.owner).to eq(@user) - expect(project.namespace).to eq(@user.namespace) - end + project = create_project(user, opts) + expect(project).to be_valid + expect(project.owner).to eq(user) + expect(project.namespace).to eq(user.namespace) end + end - context 'when there is an active service template' do - before do - create(:service, project: nil, template: true, active: true) - end + context 'when there is an active service template' do + before do + create(:service, project: nil, template: true, active: true) + end - it 'creates a service from this template' do - project = create_project(@user, @opts) - project.reload + it 'creates a service from this template' do + project = create_project(user, opts) - expect(project.services.count).to eq 1 - end + expect(project.services.count).to eq 1 end end -- cgit v1.2.1 From d6f98cc2de7db1aa27375d8bfbceca8099f64504 Mon Sep 17 00:00:00 2001 From: Semyon Pupkov <mail@semyonpupkov.com> Date: Tue, 22 Nov 2016 21:27:16 +0500 Subject: Remove empty describe block in service spec --- spec/models/service_spec.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index b1615a95004..691511cd93f 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -6,9 +6,6 @@ describe Service, models: true do it { is_expected.to have_one :service_hook } end - describe "Mass assignment" do - end - describe "Test Button" do before do @service = Service.new -- cgit v1.2.1 From 29f7b88f0d231d46e7d96dfd45ac7754e313f7a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= <alejorro70@gmail.com> Date: Tue, 22 Nov 2016 14:03:42 -0300 Subject: [ci skip] Fix unreleased changelog --- changelogs/unreleased/master-recursiveTree.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/unreleased/master-recursiveTree.yml b/changelogs/unreleased/master-recursiveTree.yml index c6384d172e2..a0eb99205d5 100644 --- a/changelogs/unreleased/master-recursiveTree.yml +++ b/changelogs/unreleased/master-recursiveTree.yml @@ -1,4 +1,4 @@ --- -title: API: allow recursive tree request +title: 'API: allow recursive tree request' merge_request: 6088 author: Rebeca Méndez -- cgit v1.2.1 From 93792e8da6fe91cfb380eda96bdb83441117be46 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Tue, 22 Nov 2016 18:31:34 +0100 Subject: Add changes to JIRA api docs [ci skip] --- doc/api/services.md | 16 +++++++--------- doc/project_services/jira.md | 4 ++-- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/doc/api/services.md b/doc/api/services.md index c7f537aceb6..a5d733fe6c7 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -465,10 +465,10 @@ GET /projects/:id/services/jira Set JIRA service for a project. ->**Note:** -Setting `project_url`, `issues_url` and `new_issue_url` will allow a user to -easily navigate to the JIRA issue tracker. See the [integration doc][jira-doc] -for details. +>**Notes:** +- Starting with GitLab 8.14, `api_url`, `issues_url`, `new_issue_url` and + `project_url` are replaced by `project_key`, `url`. If you are using an + older version, [follow this documentation][old-jira-api]. ``` PUT /projects/:id/services/jira @@ -477,11 +477,8 @@ PUT /projects/:id/services/jira | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `active` | boolean| no | Enable/disable the JIRA service. | -| `project_url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project. It is of the form: `https://<jira_host_url>/issues/?jql=project=<jira_project>`. | -| `issues_url` | string | yes | The URL to the JIRA project issues overview for the project that is linked to this GitLab project. It is of the form: `https://<jira_host_url>/browse/:id`. Leave `:id` as-is, it gets replaced by GitLab at runtime.| -| `new_issue_url` | string | yes | This is the URL to create a new issue in JIRA for the project linked to this GitLab project, and it is of the form: `https://<jira_host_url>/secure/CreateIssue.jspa` | -| `api_url` | string | yes | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`. It is of the form: `https://<jira_host_url>/rest/api/2`. | -| `description` | string | no | A name for the issue tracker. | +| `url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project, e.g., `https://jira.example.com`. | +| `project_key` | string | yes | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. | | `username` | string | no | The username of the user created to be used with GitLab/JIRA. | | `password` | string | no | The password of the user created to be used with GitLab/JIRA. | | `jira_issue_transition_id` | string | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. | @@ -670,3 +667,4 @@ GET /projects/:id/services/teamcity ``` [jira-doc]: ../project_services/jira.md +[old-jira-api]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/api/services.md#jira diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index e537a24028c..ef14def78d9 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -97,7 +97,7 @@ in the table below. | Field | Description | | ----- | ----------- | | `URL` | The base URL to the JIRA project which is being linked to this GitLab project. E.g., `https://jira.example.com`. | -| `Project key` | The short, the identifier for your JIRA project, all uppercase. | +| `Project key` | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. | | `Username` | The user name created in [configuring JIRA step](#configuring-jira). | | `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | | `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). | @@ -121,7 +121,7 @@ ID in GitLab commits and merge requests. When GitLab project has JIRA issue tracker configured and enabled, mentioning JIRA issue in GitLab will automatically add a comment in JIRA issue with the link back to GitLab. This means that in comments in merge requests and commits -referencing an issue, eg. `PROJECT-7`, will add a comment in JIRA issue in the +referencing an issue, e.g., `PROJECT-7`, will add a comment in JIRA issue in the format: ``` -- cgit v1.2.1 From 7367744401a04b78fa424b5949f688f6c2cb4c7f Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Tue, 22 Nov 2016 18:37:21 +0100 Subject: Fix list in JIRA service docs [ci skip] --- doc/project_services/jira.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index ef14def78d9..366e4b2d306 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -150,11 +150,11 @@ the transition ID was set up correctly). There are currently three trigger words, and you can use either one to achieve the same goal: -- `Resolves GITLAB-1` -- `Closes GITLAB-1` -- `Fixes GITLAB-1` +- `Resolves PROJECT-1` +- `Closes PROJECT-1` +- `Fixes PROJECT-1` -- where `GITLAB-1` the issue ID of the JIRA project. +where `PROJECT-1` is the issue ID of the JIRA project. ### JIRA issue closing example -- cgit v1.2.1 From 8087aa3be1288c9849f9671e90d25b52288028ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= <alejorro70@gmail.com> Date: Tue, 22 Nov 2016 14:45:23 -0300 Subject: [ci skip] Fix encoding on unreleased changelog entry --- changelogs/unreleased/master-recursiveTree.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/unreleased/master-recursiveTree.yml b/changelogs/unreleased/master-recursiveTree.yml index a0eb99205d5..f408f4700f8 100644 --- a/changelogs/unreleased/master-recursiveTree.yml +++ b/changelogs/unreleased/master-recursiveTree.yml @@ -1,4 +1,4 @@ --- title: 'API: allow recursive tree request' merge_request: 6088 -author: Rebeca Méndez +author: Rebeca Mendez -- cgit v1.2.1 From 2fabaa52283c27235a52c1e7a0fdcd7c840c0e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= <alejorro70@gmail.com> Date: Tue, 22 Nov 2016 14:59:48 -0300 Subject: Update CHANGELOG.md for 8.14.0 [ci skip] --- CHANGELOG.md | 125 +++++++++++++++++++++ ...ricting-global-visibility-levels-is-unclear.yml | 4 - ...81-admin-links-new-group-default-visibility.yml | 4 - ...-add-setting-to-check-unresolved-discussion.yml | 4 - .../unreleased/21076-deleted-merged-branches.yml | 4 - ...ncorrect-workhorse-version-number-displayed.yml | 4 - .../21992-disable-access-requests-by-default.yml | 4 - .../22307-pipeline-link-in-builds-view.yml | 4 - changelogs/unreleased/22539-display-folders.yml | 4 - .../22588-todos-filter-shows-all-users.yml | 4 - .../22699-group-permssion-background-migration.yml | 4 - .../22790-mention-autocomplete-avatar.yml | 4 - .../unreleased/22947-fix_issues_atom_feed_url.yml | 4 - ...lame-spinach-tests-with-rspec-feature-tests.yml | 4 - .../23117-search-for-a-filename-in-a-project.yml | 4 - .../23223-group-deletion-race-condition.yml | 4 - .../23449-cycle-analytics-2-frontend.yml | 4 - .../23584-triggering-builds-from-webhooks.yml | 4 - .../unreleased/23637-title-bar-pipelines.yml | 4 - .../unreleased/23731-add-param-to-user-api.yml | 4 - .../23961-can-t-share-project-with-groups.yml | 4 - .../23990-project-show-error-when-empty-repo.yml | 4 - .../24010-change-anchor-link-to-mr-diff.yml | 4 - .../unreleased/24010-double-event-trigger.yml | 4 - .../24048-dropdown-issue-with-devider.yml | 4 - ...uest-sees-some-project-details-and-gets-404.yml | 4 - .../24059-round-robin-repository-storage.yml | 4 - changelogs/unreleased/24070-project-margins.yml | 4 - ...2-improve-importing-of-github-pull-requests.yml | 4 - ...ve-source-branch-when-editing-merge-request.yml | 4 - changelogs/unreleased/24107-slack-comment-link.yml | 4 - changelogs/unreleased/24255-search-fix.yml | 4 - .../unreleased/24276-usernames-with-dots.yml | 4 - ...quest-sidebar-todo-button-style-improvement.yml | 4 - .../unreleased/24369-remove-additional-padding.yml | 4 - changelogs/unreleased/24492-promise-polyfill.yml | 4 - .../24496-fix-internal-api-project-lookup.yml | 4 - ...99-fix-activity-autoload-on-large-viewports.yml | 4 - ...equest-failed-although-builds-still-running.yml | 4 - .../unreleased/24627-fix-bad-mr-error-message.yml | 4 - .../adam-build-missing-services-when-necessary.yml | 4 - ...-fix-collapsed-diff-symlink-file-conversion.yml | 4 - changelogs/unreleased/add-api-label-id.yml | 4 - changelogs/unreleased/add-chat-names.yml | 4 - .../unreleased/add-project-import-data-index.yml | 4 - .../unreleased/always-show-download-button.yml | 4 - changelogs/unreleased/api-label-priorities.yml | 4 - .../api-return-400-if-post-systemhook-fails.yml | 4 - .../unreleased/assignee-dropdown-autocomplete.yml | 4 - .../unreleased/broken-link-frontend-dev-guide.yml | 4 - changelogs/unreleased/bugfix-html-only-mail.yml | 4 - changelogs/unreleased/changelog-update.yml | 4 - changelogs/unreleased/chatops-deploy-command.yml | 4 - changelogs/unreleased/create-pipeline-endpoint.yml | 4 - changelogs/unreleased/dz-fix-500-group-git.yml | 4 - changelogs/unreleased/dz-fix-group-name-dot.yml | 4 - changelogs/unreleased/faster_project_search.yml | 4 - .../unreleased/feature-api_owned_resource.yml | 4 - .../unreleased/feature-cycle-analytics-events.yml | 4 - ...re-environment-teardown-when-branch-deleted.yml | 4 - .../feature-precalculate-authorized-projects.yml | 4 - ...re-send-registry-address-with-build-payload.yml | 4 - .../feature-subscribe-to-group-level-labels.yml | 4 - ...rk-when-entering-a-nonexistent-git-revision.yml | 4 - changelogs/unreleased/fix-Build-timeFor.yml | 4 - changelogs/unreleased/fix-admin-ci-table.yml | 4 - .../unreleased/fix-ci-linter-undefined-error.yml | 4 - .../unreleased/fix-cycle-analytics-permissions.yml | 4 - ...-do-not-add-todo-when-build-allowed-to-fail.yml | 4 - ...r-when-invalid-branch-for-new-pipeline-used.yml | 4 - changelogs/unreleased/fix-help-page-links.yml | 4 - .../unreleased/fix-invalid-filename-eslint.yml | 4 - ...ix-require-build-script-configuration-entry.yml | 4 - changelogs/unreleased/fix-search-input-padding.yml | 4 - .../unreleased/fix-shibboleth-auth-with-no-uid.yml | 4 - .../fix-singin-redirect-for-fork-new.yml | 5 - .../unreleased/fix-trace-patch-updated-at.yml | 4 - .../fix_labels_api_adding_missing_parameter.yml | 4 - .../fix_navigation_bar_issuables_counters.yml | 4 - .../unreleased/forking-in-progress-title.yml | 4 - changelogs/unreleased/git-gc-improvements.yml | 4 - .../unreleased/hide-empty-merge-request-diffs.yml | 4 - changelogs/unreleased/issue-13823.yml | 4 - changelogs/unreleased/issue-24512.yml | 4 - .../unreleased/issue-boards-counter-border-fix.yml | 4 - changelogs/unreleased/issue_13232.yml | 4 - changelogs/unreleased/issue_23032.yml | 4 - changelogs/unreleased/issue_24303.yml | 4 - changelogs/unreleased/issue_5541.yml | 4 - changelogs/unreleased/jira_service_simplify.yml | 4 - changelogs/unreleased/ldap_check_bind.yml | 4 - .../unreleased/less-intrusive-system-note.yml | 4 - changelogs/unreleased/mailroom_idle_timeout.yml | 4 - changelogs/unreleased/master-recursiveTree.yml | 4 - .../unreleased/milestone-project-require.yml | 4 - changelogs/unreleased/namespace-validation.yml | 4 - .../new-note-worker-record-not-found-fix.yml | 4 - changelogs/unreleased/optimize-mr-index.yml | 4 - .../pass-correct-tag-target-to-post-receive.yml | 4 - changelogs/unreleased/pipeline-notifications.yml | 6 - .../unreleased/process-commits-using-sidekiq.yml | 4 - changelogs/unreleased/rack_attack_logging.yml | 4 - changelogs/unreleased/related-mr-labels.yml | 4 - .../remove-heading-space-from-diff-content.yml | 4 - changelogs/unreleased/setter-for-key.yml | 4 - changelogs/unreleased/sh-bump-omniauth-gitlab.yml | 4 - changelogs/unreleased/show-status-from-branch.yml | 4 - changelogs/unreleased/sidekiq-job-throttling.yml | 4 - changelogs/unreleased/sidekiq_default_retries.yml | 4 - .../unreleased/smarter-cache-invalidation.yml | 4 - changelogs/unreleased/sort-api-groups.yml | 4 - ...anhu-gitlab-ce-fix-error-500-with-mr-images.yml | 4 - changelogs/unreleased/upgrade-timeago.yml | 4 - .../use-separate-token-for-incoming-email.yml | 4 - .../user-dropdown-multiple-requests-fix.yml | 4 - changelogs/unreleased/user_filter_auth.yml | 4 - .../zj-mattermost-command-help-message.yml | 4 - .../unreleased/zj-slash-commands-mattermost.yml | 4 - 118 files changed, 125 insertions(+), 471 deletions(-) delete mode 100644 changelogs/unreleased/18136-ui-for-restricting-global-visibility-levels-is-unclear.yml delete mode 100644 changelogs/unreleased/19981-admin-links-new-group-default-visibility.yml delete mode 100644 changelogs/unreleased/20968-add-setting-to-check-unresolved-discussion.yml delete mode 100644 changelogs/unreleased/21076-deleted-merged-branches.yml delete mode 100644 changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml delete mode 100644 changelogs/unreleased/21992-disable-access-requests-by-default.yml delete mode 100644 changelogs/unreleased/22307-pipeline-link-in-builds-view.yml delete mode 100644 changelogs/unreleased/22539-display-folders.yml delete mode 100644 changelogs/unreleased/22588-todos-filter-shows-all-users.yml delete mode 100644 changelogs/unreleased/22699-group-permssion-background-migration.yml delete mode 100644 changelogs/unreleased/22790-mention-autocomplete-avatar.yml delete mode 100644 changelogs/unreleased/22947-fix_issues_atom_feed_url.yml delete mode 100644 changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml delete mode 100644 changelogs/unreleased/23117-search-for-a-filename-in-a-project.yml delete mode 100644 changelogs/unreleased/23223-group-deletion-race-condition.yml delete mode 100644 changelogs/unreleased/23449-cycle-analytics-2-frontend.yml delete mode 100644 changelogs/unreleased/23584-triggering-builds-from-webhooks.yml delete mode 100644 changelogs/unreleased/23637-title-bar-pipelines.yml delete mode 100644 changelogs/unreleased/23731-add-param-to-user-api.yml delete mode 100644 changelogs/unreleased/23961-can-t-share-project-with-groups.yml delete mode 100644 changelogs/unreleased/23990-project-show-error-when-empty-repo.yml delete mode 100644 changelogs/unreleased/24010-change-anchor-link-to-mr-diff.yml delete mode 100644 changelogs/unreleased/24010-double-event-trigger.yml delete mode 100644 changelogs/unreleased/24048-dropdown-issue-with-devider.yml delete mode 100644 changelogs/unreleased/24056-guest-sees-some-project-details-and-gets-404.yml delete mode 100644 changelogs/unreleased/24059-round-robin-repository-storage.yml delete mode 100644 changelogs/unreleased/24070-project-margins.yml delete mode 100644 changelogs/unreleased/24072-improve-importing-of-github-pull-requests.yml delete mode 100644 changelogs/unreleased/24102-cannot-unselect-remove-source-branch-when-editing-merge-request.yml delete mode 100644 changelogs/unreleased/24107-slack-comment-link.yml delete mode 100644 changelogs/unreleased/24255-search-fix.yml delete mode 100644 changelogs/unreleased/24276-usernames-with-dots.yml delete mode 100644 changelogs/unreleased/24279-issue-merge-request-sidebar-todo-button-style-improvement.yml delete mode 100644 changelogs/unreleased/24369-remove-additional-padding.yml delete mode 100644 changelogs/unreleased/24492-promise-polyfill.yml delete mode 100644 changelogs/unreleased/24496-fix-internal-api-project-lookup.yml delete mode 100644 changelogs/unreleased/24499-fix-activity-autoload-on-large-viewports.yml delete mode 100644 changelogs/unreleased/24616-mr-shows-the-build-for-this-merge-request-failed-although-builds-still-running.yml delete mode 100644 changelogs/unreleased/24627-fix-bad-mr-error-message.yml delete mode 100644 changelogs/unreleased/adam-build-missing-services-when-necessary.yml delete mode 100644 changelogs/unreleased/adam-fix-collapsed-diff-symlink-file-conversion.yml delete mode 100644 changelogs/unreleased/add-api-label-id.yml delete mode 100644 changelogs/unreleased/add-chat-names.yml delete mode 100644 changelogs/unreleased/add-project-import-data-index.yml delete mode 100644 changelogs/unreleased/always-show-download-button.yml delete mode 100644 changelogs/unreleased/api-label-priorities.yml delete mode 100644 changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml delete mode 100644 changelogs/unreleased/assignee-dropdown-autocomplete.yml delete mode 100644 changelogs/unreleased/broken-link-frontend-dev-guide.yml delete mode 100644 changelogs/unreleased/bugfix-html-only-mail.yml delete mode 100644 changelogs/unreleased/changelog-update.yml delete mode 100644 changelogs/unreleased/chatops-deploy-command.yml delete mode 100644 changelogs/unreleased/create-pipeline-endpoint.yml delete mode 100644 changelogs/unreleased/dz-fix-500-group-git.yml delete mode 100644 changelogs/unreleased/dz-fix-group-name-dot.yml delete mode 100644 changelogs/unreleased/faster_project_search.yml delete mode 100644 changelogs/unreleased/feature-api_owned_resource.yml delete mode 100644 changelogs/unreleased/feature-cycle-analytics-events.yml delete mode 100644 changelogs/unreleased/feature-environment-teardown-when-branch-deleted.yml delete mode 100644 changelogs/unreleased/feature-precalculate-authorized-projects.yml delete mode 100644 changelogs/unreleased/feature-send-registry-address-with-build-payload.yml delete mode 100644 changelogs/unreleased/feature-subscribe-to-group-level-labels.yml delete mode 100644 changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml delete mode 100644 changelogs/unreleased/fix-Build-timeFor.yml delete mode 100644 changelogs/unreleased/fix-admin-ci-table.yml delete mode 100644 changelogs/unreleased/fix-ci-linter-undefined-error.yml delete mode 100644 changelogs/unreleased/fix-cycle-analytics-permissions.yml delete mode 100644 changelogs/unreleased/fix-do-not-add-todo-when-build-allowed-to-fail.yml delete mode 100644 changelogs/unreleased/fix-error-when-invalid-branch-for-new-pipeline-used.yml delete mode 100644 changelogs/unreleased/fix-help-page-links.yml delete mode 100644 changelogs/unreleased/fix-invalid-filename-eslint.yml delete mode 100644 changelogs/unreleased/fix-require-build-script-configuration-entry.yml delete mode 100644 changelogs/unreleased/fix-search-input-padding.yml delete mode 100644 changelogs/unreleased/fix-shibboleth-auth-with-no-uid.yml delete mode 100644 changelogs/unreleased/fix-singin-redirect-for-fork-new.yml delete mode 100644 changelogs/unreleased/fix-trace-patch-updated-at.yml delete mode 100644 changelogs/unreleased/fix_labels_api_adding_missing_parameter.yml delete mode 100644 changelogs/unreleased/fix_navigation_bar_issuables_counters.yml delete mode 100644 changelogs/unreleased/forking-in-progress-title.yml delete mode 100644 changelogs/unreleased/git-gc-improvements.yml delete mode 100644 changelogs/unreleased/hide-empty-merge-request-diffs.yml delete mode 100644 changelogs/unreleased/issue-13823.yml delete mode 100644 changelogs/unreleased/issue-24512.yml delete mode 100644 changelogs/unreleased/issue-boards-counter-border-fix.yml delete mode 100644 changelogs/unreleased/issue_13232.yml delete mode 100644 changelogs/unreleased/issue_23032.yml delete mode 100644 changelogs/unreleased/issue_24303.yml delete mode 100644 changelogs/unreleased/issue_5541.yml delete mode 100644 changelogs/unreleased/jira_service_simplify.yml delete mode 100644 changelogs/unreleased/ldap_check_bind.yml delete mode 100644 changelogs/unreleased/less-intrusive-system-note.yml delete mode 100644 changelogs/unreleased/mailroom_idle_timeout.yml delete mode 100644 changelogs/unreleased/master-recursiveTree.yml delete mode 100644 changelogs/unreleased/milestone-project-require.yml delete mode 100644 changelogs/unreleased/namespace-validation.yml delete mode 100644 changelogs/unreleased/new-note-worker-record-not-found-fix.yml delete mode 100644 changelogs/unreleased/optimize-mr-index.yml delete mode 100644 changelogs/unreleased/pass-correct-tag-target-to-post-receive.yml delete mode 100644 changelogs/unreleased/pipeline-notifications.yml delete mode 100644 changelogs/unreleased/process-commits-using-sidekiq.yml delete mode 100644 changelogs/unreleased/rack_attack_logging.yml delete mode 100644 changelogs/unreleased/related-mr-labels.yml delete mode 100644 changelogs/unreleased/remove-heading-space-from-diff-content.yml delete mode 100644 changelogs/unreleased/setter-for-key.yml delete mode 100644 changelogs/unreleased/sh-bump-omniauth-gitlab.yml delete mode 100644 changelogs/unreleased/show-status-from-branch.yml delete mode 100644 changelogs/unreleased/sidekiq-job-throttling.yml delete mode 100644 changelogs/unreleased/sidekiq_default_retries.yml delete mode 100644 changelogs/unreleased/smarter-cache-invalidation.yml delete mode 100644 changelogs/unreleased/sort-api-groups.yml delete mode 100644 changelogs/unreleased/stanhu-gitlab-ce-fix-error-500-with-mr-images.yml delete mode 100644 changelogs/unreleased/upgrade-timeago.yml delete mode 100644 changelogs/unreleased/use-separate-token-for-incoming-email.yml delete mode 100644 changelogs/unreleased/user-dropdown-multiple-requests-fix.yml delete mode 100644 changelogs/unreleased/user_filter_auth.yml delete mode 100644 changelogs/unreleased/zj-mattermost-command-help-message.yml delete mode 100644 changelogs/unreleased/zj-slash-commands-mattermost.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b139ac8314..549336e4dff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,131 @@ entry. ## 8.14.0 (2016-11-22) +- Use separate email-token for incoming email and revert back the inactive feature. !5914 +- API: allow recursive tree request. !6088 (Rebeca Mendez) +- Replace jQuery.timeago with timeago.js. !6274 (ClemMakesApps) +- Add CI notifications. Who triggered a pipeline would receive an email after the pipeline is succeeded or failed. Users could also update notification settings accordingly. !6342 +- Add button to delete all merged branches. !6449 (Toon Claes) +- Finer-grained Git gargage collection. !6588 +- Introduce better credential and error checking to `rake gitlab:ldap:check`. !6601 +- Centralize LDAP config/filter logic. !6606 +- Make system notes less intrusive. !6755 +- Process commits using a dedicated Sidekiq worker. !6802 +- Show random messages when the To Do list is empty. !6818 (Josep Llaneras) +- Precalculate user's authorized projects in database. !6839 +- Fix record not found error on NewNoteWorker processing. !6863 (Oswaldo Ferreira) +- Show avatars in mention dropdown. !6865 +- Fix expanding a collapsed diff when converting a symlink to a regular file. !6953 +- Defer saving project services to the database if there are no user changes. !6958 +- Omniauth auto link LDAP user falls back to find by DN when user cannot be found by UID. !7002 +- Display "folders" for environments. !7015 +- Make it possible to trigger builds from webhooks. !7022 (Dmitry Poray) +- Fix showing pipeline status for a given commit from correct branch. !7034 +- Add link to build pipeline within individual build pages. !7082 +- Add api endpoint `/groups/owned`. !7103 (Borja Aparicio) +- Add query param to filter users by external & blocked type. !7109 (Yatish Mehta) +- Issues atom feed url reflect filters on dashboard. !7114 (Lucas Deschamps) +- Add setting to only allow merge requests to be merged when all discussions are resolved. !7125 (Rodolfo Arruda) +- Remove an extra leading space from diff paste data. !7133 (Hiroyuki Sato) +- Fix trace patching feature - update the updated_at value. !7146 +- Fix 404 on network page when entering non-existent git revision. !7172 (Hiroyuki Sato) +- Rewrite git blame spinach feature tests to rspec feature tests. !7197 (Lisanne Fellinger) +- Add api endpoint for creating a pipeline. !7209 (Ido Leibovich) +- Allow users to subscribe to group labels. !7215 +- Reduce API calls needed when importing issues and pull requests from GitHub. !7241 (Andrew Smith (EspadaV8)) +- Only skip group when it's actually a group in the "Share with group" select. !7262 +- Introduce round-robin project creation to spread load over multiple shards. !7266 +- Ensure merge request's "remove branch" accessors return booleans. !7267 +- Fix no "Register" tab if ldap auth is enabled (#24038). !7274 (Luc Didry) +- Expose label IDs in API. !7275 (Rares Sfirlogea) +- Fix invalid filename validation on eslint. !7281 +- API: Ability to retrieve version information. !7286 (Robert Schilling) +- Added ability to throttle Sidekiq Jobs. !7292 +- Set default Sidekiq retries to 3. !7294 +- Fix double event and ajax request call on MR page. !7298 (YarNayar) +- Unify anchor link format for MR diff files. !7298 (YarNayar) +- Require projects before creating milestone. !7301 (gfyoung) +- Fix error when using invalid branch name when creating a new pipeline. !7324 +- Return 400 when creating a system hook fails. !7350 (Robert Schilling) +- Auto-close environment when branch is deleted. !7355 +- Rework cache invalidation so only changed data is refreshed. !7360 +- Navigation bar issuables counters reflects dashboard issuables counters. !7368 (Lucas Deschamps) +- Fix cache for commit status in commits list to respect branches. !7372 +- fixes 500 error on project show when user is not logged in and project is still empty. !7376 +- Removed gray button styling from todo buttons in sidebars. !7387 +- Fix project records with invalid visibility_level values. !7391 +- Use 'Forking in progress' title when appropriate. !7394 (Philip Karpiak) +- Fix error links in help index page. !7396 (Fu Xu) +- Add support for reply-by-email when the email only contains HTML. !7397 +- [Fix] Extra divider issue in dropdown. !7398 +- Project download buttons always show. !7405 (Philip Karpiak) +- Give search-input correct padding-right value. !7407 (Philip Karpiak) +- Remove additional padding on right-aligned items in MR widget. !7411 (Didem Acet) +- Fix issue causing Labels not to appear in sidebar on MR page. !7416 (Alex Sanford) +- Allow mail_room idle_timeout option to be configurable. !7423 +- Fix misaligned buttons on admin builds page. !7424 (Didem Acet) +- Disable "Request Access" functionality by default for new projects and groups. !7425 +- fix shibboleth misconfigurations resulting in authentication bypass. !7428 +- Added Mattermost slash command. !7438 +- Allow to connect Chat account with GitLab. !7450 +- Make New Group form respect default visibility application setting. !7454 (Jacopo Beschi @jacopo-beschi) +- Fix Error 500 when creating a merge request that contains an image that was deleted and added. !7457 +- Fix labels API by adding missing current_user parameter. !7458 (Francesco Coda Zabetta) +- Changed restricted visibility admin buttons to checkboxes. !7463 +- Send credentials (currently for registry only) with build data to GitLab Runner. !7474 +- Fix POST /internal/allowed to cope with gitlab-shell v4.0.0 project paths. !7480 +- Adds es6-promise Polyfill. !7482 +- Added colored labels to related MR list. !7486 (Didem Acet) +- Use setter for key instead AR callback. !7488 (Semyon Pupkov) +- Limit labels returned for a specific project as an administrator. !7496 +- Change slack notification comment link. !7498 (Herbert Kagumba) +- Allow registering users whose username contains dots. !7500 (Timothy Andrew) +- Fix race condition during group deletion and remove stale records present due to this bug. !7528 (Timothy Andrew) +- Check all namespaces on validation of new username. !7537 +- Pass correct tag target to post-receive hook when creating tag via UI. !7556 +- Add help message for configuring Mattermost slash commands. !7558 +- Fix typo in Build page JavaScript. !7563 (winniehell) +- Make job script a required configuration entry. !7566 +- Fix errors happening when source branch of merge request is removed and then restored. !7568 +- Fix a wrong "The build for this merge request failed" message. !7579 +- Fix Margins look weird in Project page with pinned sidebar in project stats bar. !7580 +- Fix regression causing bad error message to appear on Merge Request form. !7599 (Alex Sanford) +- Fix activity page endless scroll on large viewports. !7608 +- Fix 404 on some group pages when name contains dot. !7614 +- Do not create a new TODO when failed build is allowed to fail. !7618 +- Add deployment command to ChatOps. !7619 +- Fix 500 error when group name ends with git. !7630 +- Fix undefined error in CI linter. !7650 +- Show events per stage on Cycle Analytics page. !23449 +- Add JIRA remotelinks and prevent duplicated closing messages. +- Fixed issue boards counter border when unauthorized. +- Add placeholder for the example text for custom hex color on label creation popup. (Luis Alonso Chavez Armendariz) +- Add an index for project_id in project_import_data to improve performance. +- Fix broken commits search. +- Assignee dropdown now searches author of issue or merge request. +- Clicking "force remove source branch" label now toggles the checkbox again. +- More aggressively preload on merge request and issue index pages. +- Fix broken link to observatory cli on Frontend Dev Guide. (Sam Rose) +- Fixing the issue of the project fork url giving 500 when not signed instead of being redirected to sign in page. (Cagdas Gerede) +- Fix: Guest sees some repository details and gets 404. +- Add logging for rack attack events to production.log. +- Add environment info to builds page. +- Allow commit note to be visible if repo is visible. +- Bump omniauth-gitlab to 1.0.2 to fix incompatibility with omniauth-oauth2. +- Redesign pipelines page. +- Faster search inside Project. +- Search for a filename in a project. +- Allow sorting groups in the API. +- Fix: Todos Filter Shows All Users. +- Use the Gitlab Workhorse HTTP header in the admin dashboard. (Chris Wright) +- Fixed multiple requests sent when opening dropdowns. +- Added permissions per stage to cycle analytics endpoint. +- Fix project Visibility Level selector not using default values. +- Add events per stage to cycle analytics. +- Allow to test JIRA service settings without having a repository. +- Fix JIRA references for project snippets. +- Allow enabling and disabling commit and MR events for JIRA. +- simplify url generation. (Jarka Kadlecova) - Show correct environment log in admin/logs (@duk3luk3 !7191) - Fix Milestone dropdown not stay selected for `Upcoming` and `No Milestone` option !7117 - Diff collapse won't shift when collapsing. diff --git a/changelogs/unreleased/18136-ui-for-restricting-global-visibility-levels-is-unclear.yml b/changelogs/unreleased/18136-ui-for-restricting-global-visibility-levels-is-unclear.yml deleted file mode 100644 index b8b8810ecfa..00000000000 --- a/changelogs/unreleased/18136-ui-for-restricting-global-visibility-levels-is-unclear.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Changed restricted visibility admin buttons to checkboxes -merge_request: 7463 -author: diff --git a/changelogs/unreleased/19981-admin-links-new-group-default-visibility.yml b/changelogs/unreleased/19981-admin-links-new-group-default-visibility.yml deleted file mode 100644 index 18fb8a6ad45..00000000000 --- a/changelogs/unreleased/19981-admin-links-new-group-default-visibility.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Make New Group form respect default visibility application setting -merge_request: 7454 -author: Jacopo Beschi @jacopo-beschi diff --git a/changelogs/unreleased/20968-add-setting-to-check-unresolved-discussion.yml b/changelogs/unreleased/20968-add-setting-to-check-unresolved-discussion.yml deleted file mode 100644 index 8f03746ff80..00000000000 --- a/changelogs/unreleased/20968-add-setting-to-check-unresolved-discussion.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add setting to only allow merge requests to be merged when all discussions are resolved -merge_request: 7125 -author: Rodolfo Arruda diff --git a/changelogs/unreleased/21076-deleted-merged-branches.yml b/changelogs/unreleased/21076-deleted-merged-branches.yml deleted file mode 100644 index b7fa7f14384..00000000000 --- a/changelogs/unreleased/21076-deleted-merged-branches.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add button to delete all merged branches -merge_request: 6449 -author: Toon Claes diff --git a/changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml b/changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml deleted file mode 100644 index 95d8fef1099..00000000000 --- a/changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use the Gitlab Workhorse HTTP header in the admin dashboard -merge_request: -author: Chris Wright diff --git a/changelogs/unreleased/21992-disable-access-requests-by-default.yml b/changelogs/unreleased/21992-disable-access-requests-by-default.yml deleted file mode 100644 index ddcb2169407..00000000000 --- a/changelogs/unreleased/21992-disable-access-requests-by-default.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Disable "Request Access" functionality by default for new projects and groups -merge_request: 7425 -author: diff --git a/changelogs/unreleased/22307-pipeline-link-in-builds-view.yml b/changelogs/unreleased/22307-pipeline-link-in-builds-view.yml deleted file mode 100644 index 3af746cd92a..00000000000 --- a/changelogs/unreleased/22307-pipeline-link-in-builds-view.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add link to build pipeline within individual build pages -merge_request: 7082 -author: diff --git a/changelogs/unreleased/22539-display-folders.yml b/changelogs/unreleased/22539-display-folders.yml deleted file mode 100644 index d46cdedf7a7..00000000000 --- a/changelogs/unreleased/22539-display-folders.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Display "folders" for environments -merge_request: 7015 -author: diff --git a/changelogs/unreleased/22588-todos-filter-shows-all-users.yml b/changelogs/unreleased/22588-todos-filter-shows-all-users.yml deleted file mode 100644 index 1da72142880..00000000000 --- a/changelogs/unreleased/22588-todos-filter-shows-all-users.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Fix: Todos Filter Shows All Users' -merge_request: -author: diff --git a/changelogs/unreleased/22699-group-permssion-background-migration.yml b/changelogs/unreleased/22699-group-permssion-background-migration.yml deleted file mode 100644 index e8c221b6c42..00000000000 --- a/changelogs/unreleased/22699-group-permssion-background-migration.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix project records with invalid visibility_level values -merge_request: 7391 -author: diff --git a/changelogs/unreleased/22790-mention-autocomplete-avatar.yml b/changelogs/unreleased/22790-mention-autocomplete-avatar.yml deleted file mode 100644 index 53068ca5607..00000000000 --- a/changelogs/unreleased/22790-mention-autocomplete-avatar.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Show avatars in mention dropdown -merge_request: 6865 -author: diff --git a/changelogs/unreleased/22947-fix_issues_atom_feed_url.yml b/changelogs/unreleased/22947-fix_issues_atom_feed_url.yml deleted file mode 100644 index 2312afdb3d7..00000000000 --- a/changelogs/unreleased/22947-fix_issues_atom_feed_url.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Issues atom feed url reflect filters on dashboard -merge_request: 7114 -author: Lucas Deschamps diff --git a/changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml b/changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml deleted file mode 100644 index 7b54d3df56d..00000000000 --- a/changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Rewrite git blame spinach feature tests to rspec feature tests -merge_request: 7197 -author: Lisanne Fellinger diff --git a/changelogs/unreleased/23117-search-for-a-filename-in-a-project.yml b/changelogs/unreleased/23117-search-for-a-filename-in-a-project.yml deleted file mode 100644 index 156f8d779ca..00000000000 --- a/changelogs/unreleased/23117-search-for-a-filename-in-a-project.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Search for a filename in a project -merge_request: -author: diff --git a/changelogs/unreleased/23223-group-deletion-race-condition.yml b/changelogs/unreleased/23223-group-deletion-race-condition.yml deleted file mode 100644 index 6f22e85fb4b..00000000000 --- a/changelogs/unreleased/23223-group-deletion-race-condition.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix race condition during group deletion and remove stale records present due to this bug -merge_request: 7528 -author: Timothy Andrew diff --git a/changelogs/unreleased/23449-cycle-analytics-2-frontend.yml b/changelogs/unreleased/23449-cycle-analytics-2-frontend.yml deleted file mode 100644 index 5140c09be8a..00000000000 --- a/changelogs/unreleased/23449-cycle-analytics-2-frontend.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Show events per stage on Cycle Analytics page -merge_request: 23449 -author: diff --git a/changelogs/unreleased/23584-triggering-builds-from-webhooks.yml b/changelogs/unreleased/23584-triggering-builds-from-webhooks.yml deleted file mode 100644 index 59e0d851366..00000000000 --- a/changelogs/unreleased/23584-triggering-builds-from-webhooks.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Make it possible to trigger builds from webhooks -merge_request: 7022 -author: Dmitry Poray diff --git a/changelogs/unreleased/23637-title-bar-pipelines.yml b/changelogs/unreleased/23637-title-bar-pipelines.yml deleted file mode 100644 index 3d4cf88c54c..00000000000 --- a/changelogs/unreleased/23637-title-bar-pipelines.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Redesign pipelines page -merge_request: -author: diff --git a/changelogs/unreleased/23731-add-param-to-user-api.yml b/changelogs/unreleased/23731-add-param-to-user-api.yml deleted file mode 100644 index e31029ffb27..00000000000 --- a/changelogs/unreleased/23731-add-param-to-user-api.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add query param to filter users by external & blocked type -merge_request: 7109 -author: Yatish Mehta diff --git a/changelogs/unreleased/23961-can-t-share-project-with-groups.yml b/changelogs/unreleased/23961-can-t-share-project-with-groups.yml deleted file mode 100644 index b3bfcbda4b7..00000000000 --- a/changelogs/unreleased/23961-can-t-share-project-with-groups.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Only skip group when it's actually a group in the "Share with group" select -merge_request: 7262 -author: diff --git a/changelogs/unreleased/23990-project-show-error-when-empty-repo.yml b/changelogs/unreleased/23990-project-show-error-when-empty-repo.yml deleted file mode 100644 index 8d4593d4df7..00000000000 --- a/changelogs/unreleased/23990-project-show-error-when-empty-repo.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: fixes 500 error on project show when user is not logged in and project is still empty -merge_request: 7376 -author: diff --git a/changelogs/unreleased/24010-change-anchor-link-to-mr-diff.yml b/changelogs/unreleased/24010-change-anchor-link-to-mr-diff.yml deleted file mode 100644 index 33ce18b2141..00000000000 --- a/changelogs/unreleased/24010-change-anchor-link-to-mr-diff.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Unify anchor link format for MR diff files -merge_request: 7298 -author: YarNayar diff --git a/changelogs/unreleased/24010-double-event-trigger.yml b/changelogs/unreleased/24010-double-event-trigger.yml deleted file mode 100644 index 3c2f20d391f..00000000000 --- a/changelogs/unreleased/24010-double-event-trigger.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix double event and ajax request call on MR page -merge_request: 7298 -author: YarNayar diff --git a/changelogs/unreleased/24048-dropdown-issue-with-devider.yml b/changelogs/unreleased/24048-dropdown-issue-with-devider.yml deleted file mode 100644 index b889da61957..00000000000 --- a/changelogs/unreleased/24048-dropdown-issue-with-devider.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "[Fix] Extra divider issue in dropdown" -merge_request: 7398 -author: diff --git a/changelogs/unreleased/24056-guest-sees-some-project-details-and-gets-404.yml b/changelogs/unreleased/24056-guest-sees-some-project-details-and-gets-404.yml deleted file mode 100644 index 8ca0c5beab3..00000000000 --- a/changelogs/unreleased/24056-guest-sees-some-project-details-and-gets-404.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Fix: Guest sees some repository details and gets 404' -merge_request: -author: diff --git a/changelogs/unreleased/24059-round-robin-repository-storage.yml b/changelogs/unreleased/24059-round-robin-repository-storage.yml deleted file mode 100644 index 109536114ff..00000000000 --- a/changelogs/unreleased/24059-round-robin-repository-storage.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Introduce round-robin project creation to spread load over multiple shards -merge_request: 7266 -author: diff --git a/changelogs/unreleased/24070-project-margins.yml b/changelogs/unreleased/24070-project-margins.yml deleted file mode 100644 index cbc2b0269f0..00000000000 --- a/changelogs/unreleased/24070-project-margins.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix Margins look weird in Project page with pinned sidebar in project stats bar -merge_request: 7580 -author: diff --git a/changelogs/unreleased/24072-improve-importing-of-github-pull-requests.yml b/changelogs/unreleased/24072-improve-importing-of-github-pull-requests.yml deleted file mode 100644 index 2c265960d67..00000000000 --- a/changelogs/unreleased/24072-improve-importing-of-github-pull-requests.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Reduce API calls needed when importing issues and pull requests from GitHub -merge_request: 7241 -author: Andrew Smith (EspadaV8) diff --git a/changelogs/unreleased/24102-cannot-unselect-remove-source-branch-when-editing-merge-request.yml b/changelogs/unreleased/24102-cannot-unselect-remove-source-branch-when-editing-merge-request.yml deleted file mode 100644 index 50d018170f1..00000000000 --- a/changelogs/unreleased/24102-cannot-unselect-remove-source-branch-when-editing-merge-request.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Ensure merge request's "remove branch" accessors return booleans -merge_request: 7267 -author: diff --git a/changelogs/unreleased/24107-slack-comment-link.yml b/changelogs/unreleased/24107-slack-comment-link.yml deleted file mode 100644 index 9c17d6fd825..00000000000 --- a/changelogs/unreleased/24107-slack-comment-link.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Change slack notification comment link -merge_request: 7498 -author: Herbert Kagumba diff --git a/changelogs/unreleased/24255-search-fix.yml b/changelogs/unreleased/24255-search-fix.yml deleted file mode 100644 index c0afade9bc8..00000000000 --- a/changelogs/unreleased/24255-search-fix.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix broken commits search -merge_request: -author: diff --git a/changelogs/unreleased/24276-usernames-with-dots.yml b/changelogs/unreleased/24276-usernames-with-dots.yml deleted file mode 100644 index 9aeeb33e9ef..00000000000 --- a/changelogs/unreleased/24276-usernames-with-dots.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow registering users whose username contains dots -merge_request: 7500 -author: Timothy Andrew diff --git a/changelogs/unreleased/24279-issue-merge-request-sidebar-todo-button-style-improvement.yml b/changelogs/unreleased/24279-issue-merge-request-sidebar-todo-button-style-improvement.yml deleted file mode 100644 index 72e7110d1b8..00000000000 --- a/changelogs/unreleased/24279-issue-merge-request-sidebar-todo-button-style-improvement.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Removed gray button styling from todo buttons in sidebars -merge_request: 7387 -author: diff --git a/changelogs/unreleased/24369-remove-additional-padding.yml b/changelogs/unreleased/24369-remove-additional-padding.yml deleted file mode 100644 index a6a0b248412..00000000000 --- a/changelogs/unreleased/24369-remove-additional-padding.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove additional padding on right-aligned items in MR widget. -merge_request: 7411 -author: Didem Acet diff --git a/changelogs/unreleased/24492-promise-polyfill.yml b/changelogs/unreleased/24492-promise-polyfill.yml deleted file mode 100644 index d2fddfd83c6..00000000000 --- a/changelogs/unreleased/24492-promise-polyfill.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Adds es6-promise Polyfill -merge_request: 7482 -author: diff --git a/changelogs/unreleased/24496-fix-internal-api-project-lookup.yml b/changelogs/unreleased/24496-fix-internal-api-project-lookup.yml deleted file mode 100644 index a95295c00f3..00000000000 --- a/changelogs/unreleased/24496-fix-internal-api-project-lookup.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix POST /internal/allowed to cope with gitlab-shell v4.0.0 project paths -merge_request: 7480 -author: diff --git a/changelogs/unreleased/24499-fix-activity-autoload-on-large-viewports.yml b/changelogs/unreleased/24499-fix-activity-autoload-on-large-viewports.yml deleted file mode 100644 index 53dcc2a82f1..00000000000 --- a/changelogs/unreleased/24499-fix-activity-autoload-on-large-viewports.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix activity page endless scroll on large viewports -merge_request: 7608 -author: diff --git a/changelogs/unreleased/24616-mr-shows-the-build-for-this-merge-request-failed-although-builds-still-running.yml b/changelogs/unreleased/24616-mr-shows-the-build-for-this-merge-request-failed-although-builds-still-running.yml deleted file mode 100644 index 3773459550f..00000000000 --- a/changelogs/unreleased/24616-mr-shows-the-build-for-this-merge-request-failed-although-builds-still-running.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix a wrong "The build for this merge request failed" message -merge_request: 7579 -author: diff --git a/changelogs/unreleased/24627-fix-bad-mr-error-message.yml b/changelogs/unreleased/24627-fix-bad-mr-error-message.yml deleted file mode 100644 index d6a9818b2ce..00000000000 --- a/changelogs/unreleased/24627-fix-bad-mr-error-message.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix regression causing bad error message to appear on Merge Request form -merge_request: 7599 -author: Alex Sanford diff --git a/changelogs/unreleased/adam-build-missing-services-when-necessary.yml b/changelogs/unreleased/adam-build-missing-services-when-necessary.yml deleted file mode 100644 index 8b157e31e99..00000000000 --- a/changelogs/unreleased/adam-build-missing-services-when-necessary.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Defer saving project services to the database if there are no user changes -merge_request: 6958 -author: diff --git a/changelogs/unreleased/adam-fix-collapsed-diff-symlink-file-conversion.yml b/changelogs/unreleased/adam-fix-collapsed-diff-symlink-file-conversion.yml deleted file mode 100644 index c83558f33d1..00000000000 --- a/changelogs/unreleased/adam-fix-collapsed-diff-symlink-file-conversion.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix expanding a collapsed diff when converting a symlink to a regular file -merge_request: 6953 -author: diff --git a/changelogs/unreleased/add-api-label-id.yml b/changelogs/unreleased/add-api-label-id.yml deleted file mode 100644 index 3af4f5e677d..00000000000 --- a/changelogs/unreleased/add-api-label-id.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Expose label IDs in API -merge_request: 7275 -author: Rares Sfirlogea diff --git a/changelogs/unreleased/add-chat-names.yml b/changelogs/unreleased/add-chat-names.yml deleted file mode 100644 index 6a1e05783a3..00000000000 --- a/changelogs/unreleased/add-chat-names.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow to connect Chat account with GitLab -merge_request: 7450 -author: diff --git a/changelogs/unreleased/add-project-import-data-index.yml b/changelogs/unreleased/add-project-import-data-index.yml deleted file mode 100644 index f5e4005f544..00000000000 --- a/changelogs/unreleased/add-project-import-data-index.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add an index for project_id in project_import_data to improve performance -merge_request: -author: diff --git a/changelogs/unreleased/always-show-download-button.yml b/changelogs/unreleased/always-show-download-button.yml deleted file mode 100644 index 3a625834d01..00000000000 --- a/changelogs/unreleased/always-show-download-button.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Project download buttons always show -merge_request: 7405 -author: Philip Karpiak diff --git a/changelogs/unreleased/api-label-priorities.yml b/changelogs/unreleased/api-label-priorities.yml deleted file mode 100644 index d703f8d32f3..00000000000 --- a/changelogs/unreleased/api-label-priorities.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "API: Ability to retrieve version information" -merge_request: 7286 -author: Robert Schilling diff --git a/changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml b/changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml deleted file mode 100644 index d132d7e79c3..00000000000 --- a/changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Return 400 when creating a system hook fails -merge_request: 7350 -author: Robert Schilling diff --git a/changelogs/unreleased/assignee-dropdown-autocomplete.yml b/changelogs/unreleased/assignee-dropdown-autocomplete.yml deleted file mode 100644 index 9d046b726b7..00000000000 --- a/changelogs/unreleased/assignee-dropdown-autocomplete.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Assignee dropdown now searches author of issue or merge request -merge_request: -author: diff --git a/changelogs/unreleased/broken-link-frontend-dev-guide.yml b/changelogs/unreleased/broken-link-frontend-dev-guide.yml deleted file mode 100644 index d7b6f4a7701..00000000000 --- a/changelogs/unreleased/broken-link-frontend-dev-guide.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix broken link to observatory cli on Frontend Dev Guide -merge_request: -author: Sam Rose diff --git a/changelogs/unreleased/bugfix-html-only-mail.yml b/changelogs/unreleased/bugfix-html-only-mail.yml deleted file mode 100644 index ea0d4e7396f..00000000000 --- a/changelogs/unreleased/bugfix-html-only-mail.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add support for reply-by-email when the email only contains HTML -merge_request: 7397 -author: diff --git a/changelogs/unreleased/changelog-update.yml b/changelogs/unreleased/changelog-update.yml deleted file mode 100644 index 24fa34f4121..00000000000 --- a/changelogs/unreleased/changelog-update.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add environment info to builds page -merge_request: -author: diff --git a/changelogs/unreleased/chatops-deploy-command.yml b/changelogs/unreleased/chatops-deploy-command.yml deleted file mode 100644 index 1e5a3e8df15..00000000000 --- a/changelogs/unreleased/chatops-deploy-command.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add deployment command to ChatOps -merge_request: 7619 -author: diff --git a/changelogs/unreleased/create-pipeline-endpoint.yml b/changelogs/unreleased/create-pipeline-endpoint.yml deleted file mode 100644 index c7638b7b7aa..00000000000 --- a/changelogs/unreleased/create-pipeline-endpoint.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add api endpoint for creating a pipeline -merge_request: 7209 -author: Ido Leibovich diff --git a/changelogs/unreleased/dz-fix-500-group-git.yml b/changelogs/unreleased/dz-fix-500-group-git.yml deleted file mode 100644 index 38e80ad8bde..00000000000 --- a/changelogs/unreleased/dz-fix-500-group-git.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix 500 error when group name ends with git -merge_request: 7630 -author: diff --git a/changelogs/unreleased/dz-fix-group-name-dot.yml b/changelogs/unreleased/dz-fix-group-name-dot.yml deleted file mode 100644 index 439cb229e6b..00000000000 --- a/changelogs/unreleased/dz-fix-group-name-dot.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix 404 on some group pages when name contains dot -merge_request: 7614 -author: diff --git a/changelogs/unreleased/faster_project_search.yml b/changelogs/unreleased/faster_project_search.yml deleted file mode 100644 index e29a9f34ed4..00000000000 --- a/changelogs/unreleased/faster_project_search.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Faster search inside Project -merge_request: -author: diff --git a/changelogs/unreleased/feature-api_owned_resource.yml b/changelogs/unreleased/feature-api_owned_resource.yml deleted file mode 100644 index 9c270e4ecf4..00000000000 --- a/changelogs/unreleased/feature-api_owned_resource.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add api endpoint `/groups/owned` -merge_request: 7103 -author: Borja Aparicio diff --git a/changelogs/unreleased/feature-cycle-analytics-events.yml b/changelogs/unreleased/feature-cycle-analytics-events.yml deleted file mode 100644 index e1211a3c774..00000000000 --- a/changelogs/unreleased/feature-cycle-analytics-events.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add events per stage to cycle analytics -merge_request: -author: diff --git a/changelogs/unreleased/feature-environment-teardown-when-branch-deleted.yml b/changelogs/unreleased/feature-environment-teardown-when-branch-deleted.yml deleted file mode 100644 index 0441b68e45f..00000000000 --- a/changelogs/unreleased/feature-environment-teardown-when-branch-deleted.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Auto-close environment when branch is deleted -merge_request: 7355 -author: diff --git a/changelogs/unreleased/feature-precalculate-authorized-projects.yml b/changelogs/unreleased/feature-precalculate-authorized-projects.yml deleted file mode 100644 index 6e48b710fec..00000000000 --- a/changelogs/unreleased/feature-precalculate-authorized-projects.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Precalculate user's authorized projects in database -merge_request: 6839 -author: diff --git a/changelogs/unreleased/feature-send-registry-address-with-build-payload.yml b/changelogs/unreleased/feature-send-registry-address-with-build-payload.yml deleted file mode 100644 index db9bb2bc31f..00000000000 --- a/changelogs/unreleased/feature-send-registry-address-with-build-payload.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Send credentials (currently for registry only) with build data to GitLab Runner -merge_request: 7474 -author: diff --git a/changelogs/unreleased/feature-subscribe-to-group-level-labels.yml b/changelogs/unreleased/feature-subscribe-to-group-level-labels.yml deleted file mode 100644 index ea336716dce..00000000000 --- a/changelogs/unreleased/feature-subscribe-to-group-level-labels.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow users to subscribe to group labels -merge_request: 7215 -author: diff --git a/changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml b/changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml deleted file mode 100644 index d1bc8ea2eb1..00000000000 --- a/changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix 404 on network page when entering non-existent git revision -merge_request: 7172 -author: Hiroyuki Sato diff --git a/changelogs/unreleased/fix-Build-timeFor.yml b/changelogs/unreleased/fix-Build-timeFor.yml deleted file mode 100644 index ea115f7ee67..00000000000 --- a/changelogs/unreleased/fix-Build-timeFor.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix typo in Build page JavaScript -merge_request: 7563 -author: winniehell diff --git a/changelogs/unreleased/fix-admin-ci-table.yml b/changelogs/unreleased/fix-admin-ci-table.yml deleted file mode 100644 index 9a9e39ee94a..00000000000 --- a/changelogs/unreleased/fix-admin-ci-table.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix misaligned buttons on admin builds page -merge_request: 7424 -author: Didem Acet diff --git a/changelogs/unreleased/fix-ci-linter-undefined-error.yml b/changelogs/unreleased/fix-ci-linter-undefined-error.yml deleted file mode 100644 index 229970b79c0..00000000000 --- a/changelogs/unreleased/fix-ci-linter-undefined-error.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix undefined error in CI linter -merge_request: 7650 -author: diff --git a/changelogs/unreleased/fix-cycle-analytics-permissions.yml b/changelogs/unreleased/fix-cycle-analytics-permissions.yml deleted file mode 100644 index ddcf78d705f..00000000000 --- a/changelogs/unreleased/fix-cycle-analytics-permissions.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Added permissions per stage to cycle analytics endpoint -merge_request: -author: diff --git a/changelogs/unreleased/fix-do-not-add-todo-when-build-allowed-to-fail.yml b/changelogs/unreleased/fix-do-not-add-todo-when-build-allowed-to-fail.yml deleted file mode 100644 index 6402d0c7ece..00000000000 --- a/changelogs/unreleased/fix-do-not-add-todo-when-build-allowed-to-fail.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Do not create a new TODO when failed build is allowed to fail -merge_request: 7618 -author: diff --git a/changelogs/unreleased/fix-error-when-invalid-branch-for-new-pipeline-used.yml b/changelogs/unreleased/fix-error-when-invalid-branch-for-new-pipeline-used.yml deleted file mode 100644 index ad6aa214f0f..00000000000 --- a/changelogs/unreleased/fix-error-when-invalid-branch-for-new-pipeline-used.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix error when using invalid branch name when creating a new pipeline -merge_request: 7324 -author: diff --git a/changelogs/unreleased/fix-help-page-links.yml b/changelogs/unreleased/fix-help-page-links.yml deleted file mode 100644 index 9e5f41c553f..00000000000 --- a/changelogs/unreleased/fix-help-page-links.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix error links in help index page -merge_request: 7396 -author: Fu Xu diff --git a/changelogs/unreleased/fix-invalid-filename-eslint.yml b/changelogs/unreleased/fix-invalid-filename-eslint.yml deleted file mode 100644 index eea21149c90..00000000000 --- a/changelogs/unreleased/fix-invalid-filename-eslint.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix invalid filename validation on eslint -merge_request: 7281 -author: diff --git a/changelogs/unreleased/fix-require-build-script-configuration-entry.yml b/changelogs/unreleased/fix-require-build-script-configuration-entry.yml deleted file mode 100644 index 00b3fd2681f..00000000000 --- a/changelogs/unreleased/fix-require-build-script-configuration-entry.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Make job script a required configuration entry -merge_request: 7566 -author: diff --git a/changelogs/unreleased/fix-search-input-padding.yml b/changelogs/unreleased/fix-search-input-padding.yml deleted file mode 100644 index 5d559d05d73..00000000000 --- a/changelogs/unreleased/fix-search-input-padding.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Give search-input correct padding-right value -merge_request: 7407 -author: Philip Karpiak diff --git a/changelogs/unreleased/fix-shibboleth-auth-with-no-uid.yml b/changelogs/unreleased/fix-shibboleth-auth-with-no-uid.yml deleted file mode 100644 index 56fa2170be3..00000000000 --- a/changelogs/unreleased/fix-shibboleth-auth-with-no-uid.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: fix shibboleth misconfigurations resulting in authentication bypass -merge_request: 7428 -author: diff --git a/changelogs/unreleased/fix-singin-redirect-for-fork-new.yml b/changelogs/unreleased/fix-singin-redirect-for-fork-new.yml deleted file mode 100644 index e4cf8de8699..00000000000 --- a/changelogs/unreleased/fix-singin-redirect-for-fork-new.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fixing the issue of the project fork url giving 500 when not signed instead - of being redirected to sign in page -merge_request: -author: Cagdas Gerede diff --git a/changelogs/unreleased/fix-trace-patch-updated-at.yml b/changelogs/unreleased/fix-trace-patch-updated-at.yml deleted file mode 100644 index 88f400f0a0e..00000000000 --- a/changelogs/unreleased/fix-trace-patch-updated-at.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix trace patching feature - update the updated_at value -merge_request: 7146 -author: diff --git a/changelogs/unreleased/fix_labels_api_adding_missing_parameter.yml b/changelogs/unreleased/fix_labels_api_adding_missing_parameter.yml deleted file mode 100644 index 01b191a8c5a..00000000000 --- a/changelogs/unreleased/fix_labels_api_adding_missing_parameter.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix labels API by adding missing current_user parameter -merge_request: 7458 -author: Francesco Coda Zabetta diff --git a/changelogs/unreleased/fix_navigation_bar_issuables_counters.yml b/changelogs/unreleased/fix_navigation_bar_issuables_counters.yml deleted file mode 100644 index 0f7f8155f91..00000000000 --- a/changelogs/unreleased/fix_navigation_bar_issuables_counters.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Navigation bar issuables counters reflects dashboard issuables counters -merge_request: 7368 -author: Lucas Deschamps diff --git a/changelogs/unreleased/forking-in-progress-title.yml b/changelogs/unreleased/forking-in-progress-title.yml deleted file mode 100644 index 4b9684844b3..00000000000 --- a/changelogs/unreleased/forking-in-progress-title.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use 'Forking in progress' title when appropriate -merge_request: 7394 -author: Philip Karpiak diff --git a/changelogs/unreleased/git-gc-improvements.yml b/changelogs/unreleased/git-gc-improvements.yml deleted file mode 100644 index f15e667ce87..00000000000 --- a/changelogs/unreleased/git-gc-improvements.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Finer-grained Git gargage collection -merge_request: 6588 -author: diff --git a/changelogs/unreleased/hide-empty-merge-request-diffs.yml b/changelogs/unreleased/hide-empty-merge-request-diffs.yml deleted file mode 100644 index e32a51b555a..00000000000 --- a/changelogs/unreleased/hide-empty-merge-request-diffs.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix errors happening when source branch of merge request is removed and then restored -merge_request: 7568 -author: diff --git a/changelogs/unreleased/issue-13823.yml b/changelogs/unreleased/issue-13823.yml deleted file mode 100644 index c1b5760f7df..00000000000 --- a/changelogs/unreleased/issue-13823.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Show random messages when the To Do list is empty -merge_request: 6818 -author: Josep Llaneras diff --git a/changelogs/unreleased/issue-24512.yml b/changelogs/unreleased/issue-24512.yml deleted file mode 100644 index a3a9bd9c3d1..00000000000 --- a/changelogs/unreleased/issue-24512.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add placeholder for the example text for custom hex color on label creation popup -merge_request: -author: Luis Alonso Chavez Armendariz diff --git a/changelogs/unreleased/issue-boards-counter-border-fix.yml b/changelogs/unreleased/issue-boards-counter-border-fix.yml deleted file mode 100644 index c98adb6af7c..00000000000 --- a/changelogs/unreleased/issue-boards-counter-border-fix.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed issue boards counter border when unauthorized -merge_request: -author: diff --git a/changelogs/unreleased/issue_13232.yml b/changelogs/unreleased/issue_13232.yml deleted file mode 100644 index 6dc2de5afe4..00000000000 --- a/changelogs/unreleased/issue_13232.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add JIRA remotelinks and prevent duplicated closing messages -merge_request: -author: diff --git a/changelogs/unreleased/issue_23032.yml b/changelogs/unreleased/issue_23032.yml deleted file mode 100644 index d376cf52112..00000000000 --- a/changelogs/unreleased/issue_23032.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow to test JIRA service settings without having a repository -merge_request: -author: diff --git a/changelogs/unreleased/issue_24303.yml b/changelogs/unreleased/issue_24303.yml deleted file mode 100644 index 1f007712732..00000000000 --- a/changelogs/unreleased/issue_24303.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix JIRA references for project snippets -merge_request: -author: diff --git a/changelogs/unreleased/issue_5541.yml b/changelogs/unreleased/issue_5541.yml deleted file mode 100644 index cf553cf8d80..00000000000 --- a/changelogs/unreleased/issue_5541.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow enabling and disabling commit and MR events for JIRA -merge_request: -author: diff --git a/changelogs/unreleased/jira_service_simplify.yml b/changelogs/unreleased/jira_service_simplify.yml deleted file mode 100644 index 51cedd8ce5e..00000000000 --- a/changelogs/unreleased/jira_service_simplify.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: simplify url generation -merge_request: -author: Jarka Kadlecova diff --git a/changelogs/unreleased/ldap_check_bind.yml b/changelogs/unreleased/ldap_check_bind.yml deleted file mode 100644 index daff8103a07..00000000000 --- a/changelogs/unreleased/ldap_check_bind.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Introduce better credential and error checking to `rake gitlab:ldap:check` -merge_request: 6601 -author: diff --git a/changelogs/unreleased/less-intrusive-system-note.yml b/changelogs/unreleased/less-intrusive-system-note.yml deleted file mode 100644 index 87a5a3be246..00000000000 --- a/changelogs/unreleased/less-intrusive-system-note.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Make system notes less intrusive -merge_request: 6755 -author: diff --git a/changelogs/unreleased/mailroom_idle_timeout.yml b/changelogs/unreleased/mailroom_idle_timeout.yml deleted file mode 100644 index 276b28a56dd..00000000000 --- a/changelogs/unreleased/mailroom_idle_timeout.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow mail_room idle_timeout option to be configurable -merge_request: 7423 -author: diff --git a/changelogs/unreleased/master-recursiveTree.yml b/changelogs/unreleased/master-recursiveTree.yml deleted file mode 100644 index f408f4700f8..00000000000 --- a/changelogs/unreleased/master-recursiveTree.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: allow recursive tree request' -merge_request: 6088 -author: Rebeca Mendez diff --git a/changelogs/unreleased/milestone-project-require.yml b/changelogs/unreleased/milestone-project-require.yml deleted file mode 100644 index e43033541c7..00000000000 --- a/changelogs/unreleased/milestone-project-require.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Require projects before creating milestone. -merge_request: 7301 -author: gfyoung diff --git a/changelogs/unreleased/namespace-validation.yml b/changelogs/unreleased/namespace-validation.yml deleted file mode 100644 index 6ac461bf82e..00000000000 --- a/changelogs/unreleased/namespace-validation.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Check all namespaces on validation of new username. -merge_request: 7537 -author: diff --git a/changelogs/unreleased/new-note-worker-record-not-found-fix.yml b/changelogs/unreleased/new-note-worker-record-not-found-fix.yml deleted file mode 100644 index abfba640cc0..00000000000 --- a/changelogs/unreleased/new-note-worker-record-not-found-fix.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix record not found error on NewNoteWorker processing -merge_request: 6863 -author: Oswaldo Ferreira diff --git a/changelogs/unreleased/optimize-mr-index.yml b/changelogs/unreleased/optimize-mr-index.yml deleted file mode 100644 index 1090b6d4528..00000000000 --- a/changelogs/unreleased/optimize-mr-index.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: More aggressively preload on merge request and issue index pages -merge_request: -author: diff --git a/changelogs/unreleased/pass-correct-tag-target-to-post-receive.yml b/changelogs/unreleased/pass-correct-tag-target-to-post-receive.yml deleted file mode 100644 index 5e868027ed6..00000000000 --- a/changelogs/unreleased/pass-correct-tag-target-to-post-receive.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Pass correct tag target to post-receive hook when creating tag via UI -merge_request: 7556 -author: diff --git a/changelogs/unreleased/pipeline-notifications.yml b/changelogs/unreleased/pipeline-notifications.yml deleted file mode 100644 index b43060674b2..00000000000 --- a/changelogs/unreleased/pipeline-notifications.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Add CI notifications. Who triggered a pipeline would receive an email after - the pipeline is succeeded or failed. Users could also update notification settings - accordingly -merge_request: 6342 -author: diff --git a/changelogs/unreleased/process-commits-using-sidekiq.yml b/changelogs/unreleased/process-commits-using-sidekiq.yml deleted file mode 100644 index 9f596e6a584..00000000000 --- a/changelogs/unreleased/process-commits-using-sidekiq.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Process commits using a dedicated Sidekiq worker -merge_request: 6802 -author: diff --git a/changelogs/unreleased/rack_attack_logging.yml b/changelogs/unreleased/rack_attack_logging.yml deleted file mode 100644 index c0d6c1fd12e..00000000000 --- a/changelogs/unreleased/rack_attack_logging.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add logging for rack attack events to production.log -merge_request: -author: diff --git a/changelogs/unreleased/related-mr-labels.yml b/changelogs/unreleased/related-mr-labels.yml deleted file mode 100644 index 268e0eab870..00000000000 --- a/changelogs/unreleased/related-mr-labels.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Added colored labels to related MR list. -merge_request: 7486 -author: Didem Acet diff --git a/changelogs/unreleased/remove-heading-space-from-diff-content.yml b/changelogs/unreleased/remove-heading-space-from-diff-content.yml deleted file mode 100644 index 1ea85784d29..00000000000 --- a/changelogs/unreleased/remove-heading-space-from-diff-content.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove an extra leading space from diff paste data -merge_request: 7133 -author: Hiroyuki Sato diff --git a/changelogs/unreleased/setter-for-key.yml b/changelogs/unreleased/setter-for-key.yml deleted file mode 100644 index 15167904ed5..00000000000 --- a/changelogs/unreleased/setter-for-key.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use setter for key instead AR callback -merge_request: 7488 -author: Semyon Pupkov diff --git a/changelogs/unreleased/sh-bump-omniauth-gitlab.yml b/changelogs/unreleased/sh-bump-omniauth-gitlab.yml deleted file mode 100644 index 17cd5a993dd..00000000000 --- a/changelogs/unreleased/sh-bump-omniauth-gitlab.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Bump omniauth-gitlab to 1.0.2 to fix incompatibility with omniauth-oauth2 -merge_request: -author: diff --git a/changelogs/unreleased/show-status-from-branch.yml b/changelogs/unreleased/show-status-from-branch.yml deleted file mode 100644 index 1afc230c05c..00000000000 --- a/changelogs/unreleased/show-status-from-branch.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix showing pipeline status for a given commit from correct branch -merge_request: 7034 -author: diff --git a/changelogs/unreleased/sidekiq-job-throttling.yml b/changelogs/unreleased/sidekiq-job-throttling.yml deleted file mode 100644 index ec4e2051c7e..00000000000 --- a/changelogs/unreleased/sidekiq-job-throttling.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Added ability to throttle Sidekiq Jobs -merge_request: 7292 -author: diff --git a/changelogs/unreleased/sidekiq_default_retries.yml b/changelogs/unreleased/sidekiq_default_retries.yml deleted file mode 100644 index 3df2a415dbc..00000000000 --- a/changelogs/unreleased/sidekiq_default_retries.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Set default Sidekiq retries to 3 -merge_request: 7294 -author: diff --git a/changelogs/unreleased/smarter-cache-invalidation.yml b/changelogs/unreleased/smarter-cache-invalidation.yml deleted file mode 100644 index 14a93e26ac4..00000000000 --- a/changelogs/unreleased/smarter-cache-invalidation.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Rework cache invalidation so only changed data is refreshed -merge_request: 7360 -author: diff --git a/changelogs/unreleased/sort-api-groups.yml b/changelogs/unreleased/sort-api-groups.yml deleted file mode 100644 index e3eead8c04f..00000000000 --- a/changelogs/unreleased/sort-api-groups.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow sorting groups in the API -merge_request: -author: diff --git a/changelogs/unreleased/stanhu-gitlab-ce-fix-error-500-with-mr-images.yml b/changelogs/unreleased/stanhu-gitlab-ce-fix-error-500-with-mr-images.yml deleted file mode 100644 index 7ca0d5fb19e..00000000000 --- a/changelogs/unreleased/stanhu-gitlab-ce-fix-error-500-with-mr-images.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix Error 500 when creating a merge request that contains an image that was deleted and added -merge_request: 7457 -author: diff --git a/changelogs/unreleased/upgrade-timeago.yml b/changelogs/unreleased/upgrade-timeago.yml deleted file mode 100644 index ddb266ba558..00000000000 --- a/changelogs/unreleased/upgrade-timeago.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace jQuery.timeago with timeago.js -merge_request: 6274 -author: ClemMakesApps diff --git a/changelogs/unreleased/use-separate-token-for-incoming-email.yml b/changelogs/unreleased/use-separate-token-for-incoming-email.yml deleted file mode 100644 index e498f8dd0a6..00000000000 --- a/changelogs/unreleased/use-separate-token-for-incoming-email.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use separate email-token for incoming email and revert back the inactive feature -merge_request: 5914 -author: diff --git a/changelogs/unreleased/user-dropdown-multiple-requests-fix.yml b/changelogs/unreleased/user-dropdown-multiple-requests-fix.yml deleted file mode 100644 index a83441b852a..00000000000 --- a/changelogs/unreleased/user-dropdown-multiple-requests-fix.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed multiple requests sent when opening dropdowns -merge_request: -author: diff --git a/changelogs/unreleased/user_filter_auth.yml b/changelogs/unreleased/user_filter_auth.yml deleted file mode 100644 index e4071e22e5e..00000000000 --- a/changelogs/unreleased/user_filter_auth.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Centralize LDAP config/filter logic -merge_request: 6606 -author: diff --git a/changelogs/unreleased/zj-mattermost-command-help-message.yml b/changelogs/unreleased/zj-mattermost-command-help-message.yml deleted file mode 100644 index fab238a8d8d..00000000000 --- a/changelogs/unreleased/zj-mattermost-command-help-message.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add help message for configuring Mattermost slash commands -merge_request: 7558 -author: diff --git a/changelogs/unreleased/zj-slash-commands-mattermost.yml b/changelogs/unreleased/zj-slash-commands-mattermost.yml deleted file mode 100644 index 996ffe954f3..00000000000 --- a/changelogs/unreleased/zj-slash-commands-mattermost.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Added Mattermost slash command -merge_request: 7438 -author: -- cgit v1.2.1 From c1710afbd437c557741ff4c7fa185c6ffb89bf1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= <alejorro70@gmail.com> Date: Tue, 22 Nov 2016 15:02:47 -0300 Subject: Update VERSION to 8.15.0-pre --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 919f462addc..d59bc5cbc5c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.14.0-pre +8.15.0-pre -- cgit v1.2.1 From bd0017c6d4e008474fcd87e979b3b4e3f93e39b1 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Mon, 21 Nov 2016 17:19:51 -0600 Subject: clean up globals exemptions within .eslintrc --- .eslintrc | 22 +++++----------------- app/assets/javascripts/activities.js.es6 | 3 ++- .../components/stage_code_component.js.es6 | 2 ++ .../components/stage_issue_component.js.es6 | 2 ++ .../components/stage_plan_component.js.es6 | 2 ++ .../components/stage_production_component.js.es6 | 2 ++ .../components/stage_review_component.js.es6 | 2 ++ .../components/stage_staging_component.js.es6 | 2 ++ .../components/stage_test_component.js.es6 | 2 ++ .../components/total_time_component.js.es6 | 2 ++ .../cycle_analytics/cycle_analytics_bundle.js.es6 | 4 ++++ .../environments/components/environment.js.es6 | 9 +++++---- .../components/environment_item.js.es6 | 3 ++- app/assets/javascripts/logo.js | 2 ++ app/assets/javascripts/tree.js | 1 + spec/javascripts/.eslintrc | 4 ++++ spec/javascripts/build_spec.js.es6 | 4 +++- .../environments/environments_store_spec.js.es6 | 4 +++- 18 files changed, 47 insertions(+), 25 deletions(-) diff --git a/.eslintrc b/.eslintrc index 5850c107a02..6823d0c9f35 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,4 +1,8 @@ { + "env": { + "browser": true, + "es6": true + }, "extends": "airbnb", "plugins": [ "filenames" @@ -9,23 +13,7 @@ "globals": { "$": false, "_": false, - "beforeEach": false, - "d3": false, - "define": false, - "describe": false, - "document": false, - "expect": false, - "fixture": false, "gl": false, - "it": false, - "jQuery": false, - "Mousetrap": false, - "spyOn": false, - "spyOnEvent": false, - "Turbolinks": false, - "window": false, - "Vue": false, - "Flash": false, - "Cookies": false + "jQuery": false } } diff --git a/app/assets/javascripts/activities.js.es6 b/app/assets/javascripts/activities.js.es6 index 19bcfef89fb..648cb4d5d85 100644 --- a/app/assets/javascripts/activities.js.es6 +++ b/app/assets/javascripts/activities.js.es6 @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign, class-methods-use-this */ -/* global Pager, Cookies */ +/* global Pager */ +/* global Cookies */ ((global) => { class Activities { diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 index 520cee7738b..b83a4c63fad 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 @@ -1,4 +1,6 @@ /* eslint-disable no-param-reassign */ +/* global Vue */ + ((global) => { global.cycleAnalytics = global.cycleAnalytics || {}; diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 index 3bb01c67206..cb1687dcc7a 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 @@ -1,4 +1,6 @@ /* eslint-disable no-param-reassign */ +/* global Vue */ + ((global) => { global.cycleAnalytics = global.cycleAnalytics || {}; diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 index b568ab62a69..513298ba4e7 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 @@ -1,4 +1,6 @@ /* eslint-disable no-param-reassign */ +/* global Vue */ + ((global) => { global.cycleAnalytics = global.cycleAnalytics || {}; diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 index a6b6d817a82..73f4205b578 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 @@ -1,4 +1,6 @@ /* eslint-disable no-param-reassign */ +/* global Vue */ + ((global) => { global.cycleAnalytics = global.cycleAnalytics || {}; diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6 index 9e819c1d420..501ffb1fac9 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6 @@ -1,4 +1,6 @@ /* eslint-disable no-param-reassign */ +/* global Vue */ + ((global) => { global.cycleAnalytics = global.cycleAnalytics || {}; diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 index b30c3a31010..82622232f64 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 @@ -1,4 +1,6 @@ /* eslint-disable no-param-reassign */ +/* global Vue */ + ((global) => { global.cycleAnalytics = global.cycleAnalytics || {}; diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 index c54d6b6ee37..4bfd363a1f1 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 @@ -1,4 +1,6 @@ /* eslint-disable no-param-reassign */ +/* global Vue */ + ((global) => { global.cycleAnalytics = global.cycleAnalytics || {}; diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 index 8403fbeaab5..b9675f50e31 100644 --- a/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 @@ -1,4 +1,6 @@ /* eslint-disable no-param-reassign */ +/* global Vue */ + ((global) => { global.cycleAnalytics = global.cycleAnalytics || {}; diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 index f1ddd139c48..2f810a69758 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 @@ -1,3 +1,7 @@ +/* global Vue */ +/* global Cookies */ +/* global Flash */ + //= require vue //= require_tree ./svg //= require_tree . diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index c6b38d8447d..35e183a9086 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -1,12 +1,13 @@ +/* eslint-disable no-param-reassign */ +/* global Vue */ +/* global EnvironmentsService */ + //= require vue //= require vue-resource //= require_tree ../services/ //= require ./environment_item -/* globals Vue, EnvironmentsService */ -/* eslint-disable no-param-reassign */ - -(() => { // eslint-disable-line +(() => { window.gl = window.gl || {}; /** diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index da7db5c05bd..36a0fec3cab 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -6,7 +6,8 @@ /*= require ./environment_stop */ /*= require ./environment_rollback */ -/* globals Vue, timeago */ +/* global Vue */ +/* global timeago */ (() => { /** diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js index 9404b2c3a8c..0ae6df311bb 100644 --- a/app/assets/javascripts/logo.js +++ b/app/assets/javascripts/logo.js @@ -1,4 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, padded-blocks */ +/* global Turbolinks */ + (function() { Turbolinks.enableProgressBar(); diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js index 54c473d936d..f48a7ee0f55 100644 --- a/app/assets/javascripts/tree.js +++ b/app/assets/javascripts/tree.js @@ -1,4 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, padded-blocks, max-len */ +/* global Turbolinks */ (function() { this.TreeView = (function() { function TreeView() { diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc index 90388929612..7792acffac2 100644 --- a/spec/javascripts/.eslintrc +++ b/spec/javascripts/.eslintrc @@ -7,5 +7,9 @@ "rules": { "prefer-arrow-callback": 0, "func-names": 0 + }, + "globals": { + "fixture": false, + "spyOnEvent": false } } diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6 index e21e5844a26..ee192c4f18a 100644 --- a/spec/javascripts/build_spec.js.es6 +++ b/spec/javascripts/build_spec.js.es6 @@ -1,5 +1,7 @@ -/* global Build */ /* eslint-disable no-new */ +/* global Build */ +/* global Turbolinks */ + //= require lib/utils/timeago //= require lib/utils/datetime_utility //= require build diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6 index 82d9599f372..9b0b3cb1c65 100644 --- a/spec/javascripts/environments/environments_store_spec.js.es6 +++ b/spec/javascripts/environments/environments_store_spec.js.es6 @@ -1,7 +1,9 @@ +/* global environmentsList */ + //= require vue //= require environments/stores/environments_store //= require ./mock_data -/* globals environmentsList */ + (() => { beforeEach(() => { gl.environmentsList.EnvironmentsStore.create(); -- cgit v1.2.1 From 46b725aba6054ddf536286e967c84d0afe7d1398 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 22 Nov 2016 10:27:08 -0600 Subject: add "gon" to list of expected globals --- .eslintrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc b/.eslintrc index 6823d0c9f35..67dbf9cffd2 100644 --- a/.eslintrc +++ b/.eslintrc @@ -14,6 +14,7 @@ "$": false, "_": false, "gl": false, + "gon": false, "jQuery": false } } -- cgit v1.2.1 From 6f11ffe8657dfbc0d48f936534436360d23cec9a Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Tue, 22 Nov 2016 11:38:53 -0700 Subject: Fix spacing between icon and word in status badge --- app/assets/stylesheets/pages/pipelines.scss | 8 -------- app/assets/stylesheets/pages/status.scss | 18 +++++++++--------- app/helpers/ci_status_helper.rb | 2 +- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 1b1b056f213..0027d2caf22 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -91,14 +91,6 @@ } } - .ci-status { - - svg { - top: 1px; - margin-right: 0; - } - } - a:hover { text-decoration: none; } diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 1de4674f3e7..4c258bae1f4 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -11,6 +11,15 @@ text-decoration: none; } + svg { + height: 13px; + width: 13px; + position: relative; + top: 1px; + margin-right: 3px; + overflow: visible; + } + &.ci-failed { color: $gl-danger; border-color: $gl-danger; @@ -104,15 +113,6 @@ fill: $table-text-gray; } } - - svg { - height: 13px; - width: 13px; - position: relative; - top: 1px; - margin: 0 3px; - overflow: visible; - } } } diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 895c3d728ad..abcf84b4d15 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -5,7 +5,7 @@ module CiStatusHelper end def ci_status_with_icon(status, target = nil) - content = ci_icon_for_status(status) + ' '.html_safe + ci_label_for_status(status) + content = ci_icon_for_status(status) + ci_label_for_status(status) klass = "ci-status ci-#{status}" if target link_to content, target, class: klass -- cgit v1.2.1 From af1dabe805fa7ad6fcdccfdb792b07e00c3c42d2 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Tue, 22 Nov 2016 19:53:43 +0100 Subject: Reduce size of images from 25MB to 13MB using pngquant Took it from https://gitlab.com/gitlab-com/www-gitlab-com/merge_requests/3232 [ci skip] --- doc/administration/img/custom_hooks_error_msg.png | Bin 159486 -> 44922 bytes .../high_availability/active-active-diagram.png | Bin 29607 -> 14649 bytes .../high_availability/active-passive-diagram.png | Bin 24246 -> 11699 bytes doc/administration/img/housekeeping_settings.png | Bin 27420 -> 12025 bytes .../img/raketasks/check_repos_output.png | Bin 35333 -> 19153 bytes .../img/repository_storages_admin_ui.png | Bin 54043 -> 17760 bytes .../performance/img/grafana_dashboard_dropdown.png | Bin 14368 -> 7761 bytes .../performance/img/grafana_dashboard_import.png | Bin 18267 -> 11836 bytes .../img/grafana_data_source_configuration.png | Bin 26060 -> 14700 bytes .../performance/img/grafana_data_source_empty.png | Bin 21821 -> 11963 bytes .../performance/img/grafana_save_icon.png | Bin 9107 -> 4619 bytes .../img/metrics_gitlab_configuration_settings.png | Bin 61357 -> 26169 bytes .../performance/img/request_profile_result.png | Bin 9720 -> 3236 bytes .../performance/img/request_profiling_token.png | Bin 30076 -> 10229 bytes .../operations/img/sidekiq_job_throttling.png | Bin 114784 -> 32229 bytes doc/ci/img/builds_tab.png | Bin 3047 -> 1956 bytes doc/ci/img/deployments_view.png | Bin 57598 -> 19923 bytes doc/ci/img/environments_available_staging.png | Bin 27398 -> 10098 bytes doc/ci/img/environments_dynamic_groups.png | Bin 134164 -> 45349 bytes doc/ci/img/environments_link_url.png | Bin 33561 -> 12277 bytes doc/ci/img/environments_link_url_deployments.png | Bin 19652 -> 7490 bytes doc/ci/img/environments_link_url_mr.png | Bin 47347 -> 17947 bytes doc/ci/img/environments_manual_action_builds.png | Bin 27170 -> 11137 bytes .../img/environments_manual_action_deployments.png | Bin 34504 -> 12563 bytes .../environments_manual_action_environments.png | Bin 40297 -> 14914 bytes .../img/environments_manual_action_pipelines.png | Bin 42212 -> 16243 bytes .../environments_manual_action_single_pipeline.png | Bin 42233 -> 16576 bytes doc/ci/img/environments_mr_review_app.png | Bin 39780 -> 15366 bytes doc/ci/img/environments_view.png | Bin 57534 -> 21155 bytes doc/ci/img/features_settings.png | Bin 15809 -> 9243 bytes doc/ci/img/pipelines.png | Bin 21647 -> 7516 bytes doc/ci/quick_start/img/build_log.png | Bin 52482 -> 24461 bytes doc/ci/quick_start/img/builds_status.png | Bin 41838 -> 24278 bytes doc/ci/quick_start/img/new_commit.png | Bin 7587 -> 4772 bytes doc/ci/quick_start/img/pipelines_status.png | Bin 89387 -> 25494 bytes doc/ci/quick_start/img/runners_activated.png | Bin 22822 -> 12337 bytes .../img/single_commit_status_pending.png | Bin 29981 -> 15785 bytes doc/ci/quick_start/img/status_pending.png | Bin 16205 -> 9521 bytes .../review_apps/img/review_apps_preview_in_mr.png | Bin 28689 -> 11723 bytes doc/ci/triggers/img/builds_page.png | Bin 76181 -> 29044 bytes doc/ci/triggers/img/trigger_single_build.png | Bin 21152 -> 8233 bytes doc/ci/triggers/img/trigger_variables.png | Bin 9315 -> 3652 bytes doc/ci/triggers/img/triggers_page.png | Bin 12002 -> 5119 bytes .../branded_login_page/appearance.png | Bin 156228 -> 85263 bytes .../branded_login_page/custom_sign_in.png | Bin 166674 -> 79288 bytes .../branded_login_page/default_login_page.png | Bin 150538 -> 73004 bytes doc/development/gitlab_architecture_diagram.png | Bin 23831 -> 20339 bytes doc/development/img/state-model-issue.png | Bin 13256 -> 7713 bytes doc/development/img/state-model-legend.png | Bin 12412 -> 8496 bytes doc/development/img/state-model-merge-request.png | Bin 22484 -> 12459 bytes doc/development/ux_guide/img/button-primary.png | Bin 8410 -> 1550 bytes doc/development/ux_guide/img/button-secondary.png | Bin 11160 -> 2683 bytes doc/development/ux_guide/img/color-blue.png | Bin 3865 -> 2725 bytes doc/development/ux_guide/img/color-green.png | Bin 4127 -> 3008 bytes doc/development/ux_guide/img/color-orange.png | Bin 4698 -> 3470 bytes doc/development/ux_guide/img/color-red.png | Bin 3669 -> 2628 bytes doc/development/ux_guide/img/components-alerts.png | Bin 46785 -> 27342 bytes .../ux_guide/img/components-anchorlinks.png | Bin 36456 -> 19948 bytes .../ux_guide/img/components-contentblock.png | Bin 19841 -> 14190 bytes .../ux_guide/img/components-coverblock.png | Bin 15757 -> 10141 bytes .../ux_guide/img/components-dateexact.png | Bin 5609 -> 4161 bytes .../ux_guide/img/components-daterelative.png | Bin 5843 -> 4189 bytes .../ux_guide/img/components-dropdown.png | Bin 60448 -> 31760 bytes .../ux_guide/img/components-fileholder.png | Bin 4953 -> 3938 bytes .../ux_guide/img/components-horizontalform.png | Bin 5708 -> 4327 bytes .../ux_guide/img/components-listinsidepanel.png | Bin 3962 -> 3449 bytes .../ux_guide/img/components-listwithavatar.png | Bin 7952 -> 5749 bytes .../ux_guide/img/components-listwithhover.png | Bin 3313 -> 2860 bytes doc/development/ux_guide/img/components-panels.png | Bin 32886 -> 21822 bytes .../ux_guide/img/components-referencehover.png | Bin 11519 -> 6948 bytes .../ux_guide/img/components-referenceissues.png | Bin 14587 -> 10009 bytes .../ux_guide/img/components-referencelabels.png | Bin 4643 -> 4108 bytes .../ux_guide/img/components-referencemilestone.png | Bin 2468 -> 2417 bytes .../ux_guide/img/components-referencemrs.png | Bin 12646 -> 8859 bytes .../ux_guide/img/components-referencepeople.png | Bin 7214 -> 5607 bytes .../ux_guide/img/components-rowcontentblock.png | Bin 19730 -> 14315 bytes .../ux_guide/img/components-simplelist.png | Bin 3078 -> 2781 bytes doc/development/ux_guide/img/components-table.png | Bin 7668 -> 6081 bytes .../ux_guide/img/components-verticalform.png | Bin 6541 -> 4964 bytes .../ux_guide/img/copy-form-addissuebutton.png | Bin 26541 -> 16085 bytes .../ux_guide/img/copy-form-addissueform.png | Bin 38242 -> 25978 bytes .../ux_guide/img/copy-form-editissuebutton.png | Bin 16466 -> 11801 bytes .../ux_guide/img/copy-form-editissueform.png | Bin 40660 -> 25621 bytes .../ux_guide/img/features-contextualnav.png | Bin 8051 -> 5912 bytes .../ux_guide/img/features-emptystates.png | Bin 114540 -> 61664 bytes doc/development/ux_guide/img/features-filters.png | Bin 4529 -> 3924 bytes .../ux_guide/img/features-globalnav.png | Bin 8953 -> 5780 bytes .../ux_guide/img/surfaces-contentitemtitle.png | Bin 7463 -> 5142 bytes doc/development/ux_guide/img/surfaces-header.png | Bin 6103 -> 4095 bytes .../img/surfaces-systeminformationblock.png | Bin 15412 -> 10423 bytes doc/development/ux_guide/img/surfaces-ux.png | Bin 7673 -> 4029 bytes doc/development/ux_guide/img/tooltip-placement.png | Bin 2645 -> 2071 bytes doc/development/ux_guide/img/tooltip-usage.png | Bin 9160 -> 5994 bytes doc/gitlab-basics/img/create_new_group_info.png | Bin 53103 -> 20321 bytes doc/gitlab-basics/img/create_new_group_sidebar.png | Bin 5396 -> 2682 bytes .../img/create_new_project_button.png | Bin 10050 -> 4196 bytes .../img/create_new_project_from_group.png | Bin 6545 -> 3194 bytes doc/gitlab-basics/img/create_new_project_info.png | Bin 49451 -> 20385 bytes doc/gitlab-basics/img/fork_choose_namespace.png | Bin 39253 -> 13674 bytes doc/gitlab-basics/img/fork_new.png | Bin 25540 -> 10722 bytes doc/gitlab-basics/img/merge_request_new.png | Bin 3596 -> 2234 bytes doc/gitlab-basics/img/merge_request_page.png | Bin 91432 -> 33801 bytes .../img/merge_request_select_branch.png | Bin 50707 -> 20332 bytes doc/gitlab-basics/img/new_issue_button.png | Bin 3070 -> 2010 bytes doc/gitlab-basics/img/new_issue_page.png | Bin 53268 -> 21386 bytes doc/gitlab-basics/img/profile_settings.png | Bin 5975 -> 3045 bytes .../img/profile_settings_ssh_keys.png | Bin 42977 -> 16531 bytes .../img/profile_settings_ssh_keys_paste_pub.png | Bin 37486 -> 13447 bytes .../img/profile_settings_ssh_keys_single_key.png | Bin 18498 -> 8133 bytes .../img/profile_settings_ssh_keys_title.png | Bin 2362 -> 1872 bytes doc/gitlab-basics/img/project_clone_url.png | Bin 40490 -> 14978 bytes doc/gitlab-basics/img/project_navbar.png | Bin 5745 -> 3259 bytes doc/gitlab-basics/img/select_group_dropdown.png | Bin 8038 -> 3489 bytes doc/integration/img/akismet_settings.png | Bin 26625 -> 16923 bytes doc/integration/img/bitbucket_oauth_keys.png | Bin 12073 -> 5149 bytes .../img/bitbucket_oauth_settings_page.png | Bin 82818 -> 30081 bytes .../img/enabled-oauth-sign-in-sources.png | Bin 21767 -> 13304 bytes doc/integration/img/facebook_api_keys.png | Bin 85832 -> 42308 bytes doc/integration/img/facebook_app_settings.png | Bin 68086 -> 35876 bytes doc/integration/img/facebook_website_url.png | Bin 19823 -> 9620 bytes doc/integration/img/github_app.png | Bin 55591 -> 29330 bytes doc/integration/img/gitlab_app.png | Bin 30963 -> 15402 bytes .../img/gmail_action_buttons_for_gitlab.png | Bin 16020 -> 11573 bytes doc/integration/img/google_app.png | Bin 29154 -> 19168 bytes .../img/oauth_provider_admin_application.png | Bin 33440 -> 17082 bytes .../img/oauth_provider_application_form.png | Bin 23048 -> 12566 bytes .../img/oauth_provider_application_id_secret.png | Bin 27673 -> 15293 bytes .../img/oauth_provider_authorized_application.png | Bin 26622 -> 14668 bytes .../img/oauth_provider_user_wide_applications.png | Bin 33337 -> 17526 bytes doc/integration/img/spam_log.png | Bin 187190 -> 50996 bytes doc/integration/img/submit_issue.png | Bin 174556 -> 45962 bytes doc/integration/img/twitter_app_api_keys.png | Bin 36921 -> 24577 bytes doc/integration/img/twitter_app_details.png | Bin 64686 -> 40392 bytes .../performance/img/grafana_dashboard_dropdown.png | Bin 14368 -> 7761 bytes .../performance/img/grafana_dashboard_import.png | Bin 18267 -> 11836 bytes .../img/grafana_data_source_configuration.png | Bin 26060 -> 14700 bytes .../performance/img/grafana_data_source_empty.png | Bin 21821 -> 11963 bytes .../performance/img/grafana_save_icon.png | Bin 9107 -> 4619 bytes .../img/metrics_gitlab_configuration_settings.png | Bin 37228 -> 21387 bytes doc/profile/2fa_u2f_authenticate.png | Bin 54413 -> 17585 bytes doc/profile/2fa_u2f_register.png | Bin 112414 -> 35186 bytes doc/project_services/img/builds_emails_service.png | Bin 30956 -> 19203 bytes .../img/emails_on_push_service.png | Bin 98160 -> 28535 bytes .../img/jira_add_user_to_group.png | Bin 41994 -> 24838 bytes doc/project_services/img/jira_create_new_group.png | Bin 32934 -> 19127 bytes .../img/jira_create_new_group_name.png | Bin 9054 -> 5168 bytes doc/project_services/img/jira_create_new_user.png | Bin 21081 -> 12625 bytes doc/project_services/img/jira_group_access.png | Bin 32210 -> 19235 bytes doc/project_services/img/jira_issue_reference.png | Bin 49151 -> 18399 bytes .../img/jira_merge_request_close.png | Bin 52556 -> 21172 bytes doc/project_services/img/jira_project_name.png | Bin 41572 -> 26685 bytes doc/project_services/img/jira_service.png | Bin 56834 -> 37869 bytes .../img/jira_service_close_comment.png | Bin 29716 -> 11893 bytes .../img/jira_service_close_issue.png | Bin 89666 -> 30570 bytes doc/project_services/img/jira_service_page.png | Bin 30309 -> 12228 bytes .../img/jira_user_management_link.png | Bin 43095 -> 23921 bytes .../img/jira_workflow_screenshot.png | Bin 111093 -> 66685 bytes .../img/mattermost_add_slash_command.png | Bin 23150 -> 9265 bytes doc/project_services/img/mattermost_bot_auth.png | Bin 21031 -> 8676 bytes .../img/mattermost_bot_available_commands.png | Bin 12746 -> 4647 bytes .../img/mattermost_config_help.png | Bin 176610 -> 63138 bytes .../img/mattermost_console_integrations.png | Bin 114850 -> 41186 bytes .../img/mattermost_gitlab_token.png | Bin 7879 -> 3688 bytes .../img/mattermost_goto_console.png | Bin 22802 -> 7754 bytes .../img/mattermost_slash_command_configuration.png | Bin 60927 -> 24169 bytes .../img/mattermost_slash_command_token.png | Bin 20415 -> 8624 bytes .../img/mattermost_team_integrations.png | Bin 12171 -> 4766 bytes doc/project_services/img/redmine_configuration.png | Bin 16973 -> 10266 bytes .../img/services_templates_redmine_example.png | Bin 13936 -> 8776 bytes doc/project_services/img/slack_configuration.png | Bin 75762 -> 29825 bytes doc/raketasks/backup_hrz.png | Bin 31784 -> 11444 bytes .../img/two_factor_authentication_settings.png | Bin 16807 -> 9941 bytes .../high-availability/aws/img/auto-scaling-det.png | Bin 106157 -> 29970 bytes .../high-availability/aws/img/db-subnet-group.png | Bin 98632 -> 29306 bytes .../high-availability/aws/img/ec-subnet.png | Bin 91922 -> 28405 bytes .../aws/img/elastic-file-system.png | Bin 109719 -> 34582 bytes doc/university/high-availability/aws/img/ig-rt.png | Bin 42022 -> 12547 bytes doc/university/high-availability/aws/img/ig.png | Bin 26220 -> 8149 bytes .../high-availability/aws/img/instance_specs.png | Bin 40938 -> 11525 bytes .../high-availability/aws/img/new_vpc.png | Bin 54072 -> 15696 bytes .../high-availability/aws/img/policies.png | Bin 132366 -> 39845 bytes .../high-availability/aws/img/rds-net-opt.png | Bin 54996 -> 16347 bytes .../high-availability/aws/img/rds-sec-group.png | Bin 43950 -> 11584 bytes .../aws/img/redis-cluster-det.png | Bin 81524 -> 23761 bytes .../high-availability/aws/img/redis-net.png | Bin 100700 -> 27261 bytes .../high-availability/aws/img/route_table.png | Bin 39611 -> 12088 bytes .../high-availability/aws/img/subnet.png | Bin 56466 -> 17077 bytes .../training/gitlab_flow/feature_branches.png | Bin 20600 -> 6202 bytes .../training/gitlab_flow/production_branch.png | Bin 21716 -> 7293 bytes .../training/gitlab_flow/release_branches.png | Bin 44173 -> 12775 bytes doc/university/training/logo.png | Bin 33117 -> 8940 bytes doc/user/admin_area/img/admin_labels.png | Bin 91459 -> 23063 bytes .../monitoring/img/health_check_token.png | Bin 6630 -> 4923 bytes .../settings/img/access_restrictions.png | Bin 7435 -> 3794 bytes .../img/admin_area_maximum_artifacts_size.png | Bin 6227 -> 3447 bytes .../settings/img/admin_area_settings_button.png | Bin 9184 -> 4403 bytes .../admin_area/settings/img/domain_blacklist.png | Bin 34684 -> 13606 bytes .../admin_area/settings/img/restricted_url.png | Bin 47539 -> 18202 bytes doc/user/img/markdown_logo.png | Bin 9509 -> 4421 bytes .../project/builds/img/build_artifacts_browser.png | Bin 8365 -> 3782 bytes .../builds/img/build_artifacts_browser_button.png | Bin 11041 -> 4891 bytes .../builds/img/build_artifacts_builds_page.png | Bin 55625 -> 22022 bytes .../builds/img/build_artifacts_pipelines_page.png | Bin 73038 -> 28339 bytes .../builds/img/build_latest_artifacts_browser.png | Bin 26617 -> 10551 bytes doc/user/project/img/container_registry_enable.png | Bin 5526 -> 3057 bytes doc/user/project/img/container_registry_panel.png | Bin 96315 -> 32310 bytes doc/user/project/img/container_registry_tab.png | Bin 7284 -> 3800 bytes .../project/img/cycle_analytics_landing_page.png | Bin 42122 -> 42117 bytes doc/user/project/img/description_templates.png | Bin 20444 -> 7903 bytes doc/user/project/img/issue_board.png | Bin 275093 -> 81649 bytes doc/user/project/img/issue_board_add_list.png | Bin 22391 -> 9516 bytes .../project/img/issue_board_search_backlog.png | Bin 25948 -> 9769 bytes doc/user/project/img/issue_board_system_notes.png | Bin 20637 -> 4899 bytes .../project/img/issue_board_welcome_message.png | Bin 78694 -> 29956 bytes doc/user/project/img/koding_build-in-progress.png | Bin 70949 -> 21953 bytes doc/user/project/img/koding_build-logs.png | Bin 263623 -> 91364 bytes doc/user/project/img/koding_build-success.png | Bin 304666 -> 73008 bytes doc/user/project/img/koding_commit-koding.yml.png | Bin 302703 -> 86043 bytes .../img/koding_different-stack-on-mr-try.png | Bin 333649 -> 93404 bytes doc/user/project/img/koding_edit-on-ide.png | Bin 330880 -> 90701 bytes doc/user/project/img/koding_enable-koding.png | Bin 73499 -> 20303 bytes doc/user/project/img/koding_landing.png | Bin 268455 -> 81010 bytes .../project/img/koding_open-gitlab-from-koding.png | Bin 32559 -> 10851 bytes doc/user/project/img/koding_run-in-ide.png | Bin 65465 -> 22179 bytes doc/user/project/img/koding_run-mr-in-ide.png | Bin 339759 -> 93780 bytes doc/user/project/img/koding_set-up-ide.png | Bin 207481 -> 54062 bytes doc/user/project/img/koding_stack-import.png | Bin 500352 -> 137608 bytes doc/user/project/img/koding_start-build.png | Bin 105253 -> 27926 bytes .../img/labels_assign_label_in_new_issue.png | Bin 31126 -> 11636 bytes .../project/img/labels_assign_label_sidebar.png | Bin 31537 -> 11767 bytes .../img/labels_assign_label_sidebar_saved.png | Bin 28396 -> 9741 bytes doc/user/project/img/labels_default.png | Bin 80403 -> 32030 bytes .../project/img/labels_description_tooltip.png | Bin 22585 -> 8538 bytes doc/user/project/img/labels_filter.png | Bin 81536 -> 31931 bytes doc/user/project/img/labels_filter_by_priority.png | Bin 60849 -> 23969 bytes doc/user/project/img/labels_generate.png | Bin 31608 -> 13628 bytes doc/user/project/img/labels_new_label.png | Bin 43265 -> 16787 bytes .../project/img/labels_new_label_on_the_fly.png | Bin 10416 -> 4625 bytes .../img/labels_new_label_on_the_fly_create.png | Bin 16151 -> 6389 bytes doc/user/project/img/labels_prioritize.png | Bin 108751 -> 38185 bytes doc/user/project/img/labels_subscribe.png | Bin 11536 -> 5336 bytes doc/user/project/img/mitmproxy-docker.png | Bin 407004 -> 142591 bytes doc/user/project/img/project_settings_list.png | Bin 11404 -> 5919 bytes .../img/protected_branches_choose_branch.png | Bin 20659 -> 7009 bytes .../img/protected_branches_devs_can_push.png | Bin 19312 -> 8302 bytes .../project/img/protected_branches_error_ui.png | Bin 37750 -> 13125 bytes doc/user/project/img/protected_branches_list.png | Bin 16223 -> 6937 bytes .../project/img/protected_branches_matches.png | Bin 32145 -> 12028 bytes doc/user/project/img/protected_branches_page.png | Bin 17839 -> 7205 bytes .../img/cherry_pick_changes_commit.png | Bin 304098 -> 141744 bytes .../img/cherry_pick_changes_commit_modal.png | Bin 264883 -> 111488 bytes .../merge_requests/img/cherry_pick_changes_mr.png | Bin 212267 -> 93870 bytes .../img/cherry_pick_changes_mr_modal.png | Bin 186597 -> 86650 bytes .../project/merge_requests/img/commit_compare.png | Bin 65010 -> 33385 bytes .../merge_requests/img/conflict_section.png | Bin 247537 -> 72815 bytes .../project/merge_requests/img/discussion_view.png | Bin 292754 -> 73821 bytes .../merge_requests/img/discussions_resolved.png | Bin 12840 -> 4152 bytes .../merge_requests/img/merge_request_diff.png | Bin 69394 -> 26650 bytes .../merge_requests/img/merge_request_widget.png | Bin 32292 -> 11039 bytes .../img/merge_when_build_succeeds_enable.png | Bin 68769 -> 39796 bytes ...ge_when_build_succeeds_only_if_succeeds_msg.png | Bin 11136 -> 5251 bytes ...en_build_succeeds_only_if_succeeds_settings.png | Bin 17552 -> 12063 bytes .../img/merge_when_build_succeeds_status.png | Bin 82655 -> 48458 bytes ...allow_merge_if_all_discussions_are_resolved.png | Bin 24693 -> 17888 bytes ...w_merge_if_all_discussions_are_resolved_msg.png | Bin 6940 -> 4962 bytes .../merge_requests/img/resolve_comment_button.png | Bin 14075 -> 4722 bytes .../img/resolve_discussion_button.png | Bin 18405 -> 4683 bytes .../merge_requests/img/revert_changes_commit.png | Bin 233750 -> 95655 bytes .../img/revert_changes_commit_modal.png | Bin 205046 -> 88824 bytes .../merge_requests/img/revert_changes_mr.png | Bin 241051 -> 104972 bytes .../merge_requests/img/revert_changes_mr_modal.png | Bin 211022 -> 93536 bytes doc/user/project/merge_requests/img/versions.png | Bin 171413 -> 55703 bytes .../merge_requests/img/versions_compare.png | Bin 68722 -> 24886 bytes .../merge_requests/img/versions_dropdown.png | Bin 60587 -> 21547 bytes .../merge_requests/img/versions_system_note.png | Bin 18731 -> 7136 bytes .../img/wip_blocked_accept_button.png | Bin 32720 -> 18606 bytes .../project/merge_requests/img/wip_mark_as_wip.png | Bin 21640 -> 11396 bytes .../merge_requests/img/wip_unmark_as_wip.png | Bin 16606 -> 8565 bytes .../pipelines/img/pipelines_settings_badges.png | Bin 56166 -> 21137 bytes .../img/pipelines_settings_test_coverage.png | Bin 4212 -> 2603 bytes .../img/pipelines_test_coverage_build.png | Bin 9953 -> 4481 bytes .../img/pipelines_test_coverage_mr_widget.png | Bin 14502 -> 6391 bytes .../img/web_editor_new_branch_dropdown.png | Bin 20436 -> 10386 bytes .../img/web_editor_new_branch_from_issue.png | Bin 4728 -> 2720 bytes .../repository/img/web_editor_new_branch_page.png | Bin 11245 -> 6034 bytes .../img/web_editor_new_directory_dialog.png | Bin 13339 -> 7323 bytes .../img/web_editor_new_directory_dropdown.png | Bin 20007 -> 9918 bytes .../img/web_editor_new_file_dropdown.png | Bin 20680 -> 10233 bytes .../repository/img/web_editor_new_file_editor.png | Bin 66261 -> 38068 bytes .../repository/img/web_editor_new_push_widget.png | Bin 7076 -> 3395 bytes .../repository/img/web_editor_new_tag_dropdown.png | Bin 20080 -> 9796 bytes .../repository/img/web_editor_new_tag_page.png | Bin 36610 -> 21835 bytes .../img/web_editor_start_new_merge_request.png | Bin 8596 -> 4060 bytes .../img/web_editor_template_dropdown_buttons.png | Bin 14131 -> 5634 bytes .../web_editor_template_dropdown_first_file.png | Bin 25748 -> 8846 bytes .../web_editor_template_dropdown_mit_license.png | Bin 85413 -> 30924 bytes .../img/web_editor_upload_file_dialog.png | Bin 21502 -> 12558 bytes .../img/web_editor_upload_file_dropdown.png | Bin 20651 -> 10291 bytes .../settings/img/import_export_download_export.png | Bin 85600 -> 24482 bytes .../settings/img/import_export_export_button.png | Bin 84637 -> 24122 bytes .../settings/img/import_export_mail_link.png | Bin 44012 -> 13496 bytes .../settings/img/import_export_new_project.png | Bin 43574 -> 13083 bytes .../settings/img/import_export_select_file.png | Bin 46292 -> 13713 bytes .../project/settings/img/settings_edit_button.png | Bin 19392 -> 6901 bytes doc/web_hooks/ssl.png | Bin 39120 -> 23191 bytes .../add-user/img/access_requests_management.png | Bin 15686 -> 11018 bytes .../img/add_new_user_to_project_settings.png | Bin 18149 -> 11046 bytes .../add-user/img/add_user_email_accept.png | Bin 22877 -> 16890 bytes doc/workflow/add-user/img/add_user_email_ready.png | Bin 40207 -> 28171 bytes .../add-user/img/add_user_email_search.png | Bin 45798 -> 29628 bytes .../add-user/img/add_user_give_permissions.png | Bin 56380 -> 36619 bytes ...dd_user_import_members_from_another_project.png | Bin 38778 -> 25343 bytes .../add-user/img/add_user_imported_members.png | Bin 37835 -> 25398 bytes .../add-user/img/add_user_list_members.png | Bin 24337 -> 16916 bytes .../add-user/img/add_user_members_menu.png | Bin 42224 -> 28994 bytes .../add-user/img/add_user_search_people.png | Bin 39844 -> 25368 bytes .../add-user/img/request_access_button.png | Bin 36588 -> 25281 bytes .../img/withdraw_access_request_button.png | Bin 37960 -> 26135 bytes doc/workflow/award_emoji.png | Bin 9939 -> 5268 bytes doc/workflow/ci_mr.png | Bin 29571 -> 12034 bytes doc/workflow/close_issue_mr.png | Bin 82595 -> 42108 bytes doc/workflow/environment_branches.png | Bin 20745 -> 12364 bytes doc/workflow/forking/branch_select.png | Bin 27299 -> 15424 bytes doc/workflow/forking/merge_request.png | Bin 31560 -> 16332 bytes doc/workflow/four_stages.png | Bin 10003 -> 7124 bytes doc/workflow/git_pull.png | Bin 94405 -> 28749 bytes doc/workflow/gitdashflow.png | Bin 131491 -> 68177 bytes doc/workflow/github_flow.png | Bin 10251 -> 6173 bytes doc/workflow/gitlab_flow.png | Bin 70871 -> 47432 bytes doc/workflow/good_commit.png | Bin 13131 -> 8742 bytes doc/workflow/groups/access_requests_management.png | Bin 15829 -> 11186 bytes doc/workflow/groups/add_member_to_group.png | Bin 78060 -> 35724 bytes doc/workflow/groups/group_dashboard.png | Bin 59446 -> 28155 bytes doc/workflow/groups/group_with_two_projects.png | Bin 73101 -> 34462 bytes doc/workflow/groups/max_access_level.png | Bin 74947 -> 34718 bytes doc/workflow/groups/new_group_button.png | Bin 108482 -> 49708 bytes doc/workflow/groups/new_group_form.png | Bin 58860 -> 27263 bytes .../groups/other_group_sees_shared_project.png | Bin 64447 -> 30182 bytes doc/workflow/groups/override_access_level.png | Bin 90122 -> 40993 bytes doc/workflow/groups/project_members_via_group.png | Bin 86260 -> 39532 bytes doc/workflow/groups/request_access_button.png | Bin 49067 -> 35917 bytes doc/workflow/groups/share_project_with_groups.png | Bin 65633 -> 30307 bytes doc/workflow/groups/transfer_project.png | Bin 92115 -> 43502 bytes .../groups/withdraw_access_request_button.png | Bin 49941 -> 36413 bytes doc/workflow/img/award_emoji_comment_awarded.png | Bin 64317 -> 19159 bytes doc/workflow/img/award_emoji_comment_picker.png | Bin 250861 -> 72883 bytes doc/workflow/img/award_emoji_select.png | Bin 49296 -> 23779 bytes .../img/award_emoji_votes_least_popular.png | Bin 116715 -> 50191 bytes .../img/award_emoji_votes_most_popular.png | Bin 108775 -> 48342 bytes .../img/award_emoji_votes_sort_options.png | Bin 131659 -> 57145 bytes doc/workflow/img/file_finder_find_button.png | Bin 25458 -> 14567 bytes doc/workflow/img/file_finder_find_file.png | Bin 35114 -> 19478 bytes .../img/forking_workflow_choose_namespace.png | Bin 59114 -> 26275 bytes doc/workflow/img/forking_workflow_fork_button.png | Bin 20750 -> 12973 bytes .../img/forking_workflow_path_taken_error.png | Bin 17978 -> 10103 bytes doc/workflow/img/new_branch_from_issue.png | Bin 54607 -> 33584 bytes doc/workflow/img/todo_list_item.png | Bin 58912 -> 18777 bytes doc/workflow/img/todos_add_todo_sidebar.png | Bin 120265 -> 33350 bytes doc/workflow/img/todos_icon.png | Bin 3843 -> 1341 bytes doc/workflow/img/todos_index.png | Bin 152040 -> 63372 bytes doc/workflow/img/todos_mark_done_sidebar.png | Bin 121303 -> 33703 bytes .../fogbugz_importer/fogbugz_import_finished.png | Bin 30266 -> 17744 bytes .../fogbugz_importer/fogbugz_import_login.png | Bin 20797 -> 13751 bytes .../fogbugz_import_select_fogbogz.png | Bin 20526 -> 12289 bytes .../fogbugz_import_select_project.png | Bin 34836 -> 20905 bytes .../fogbugz_importer/fogbugz_import_user_map.png | Bin 77208 -> 51238 bytes .../importing/gitlab_importer/importer.png | Bin 18366 -> 12864 bytes .../importing/gitlab_importer/new_project_page.png | Bin 33589 -> 21251 bytes .../img/import_projects_from_github_importer.png | Bin 65288 -> 17953 bytes ...mport_projects_from_github_new_project_page.png | Bin 24911 -> 11047 bytes ...ort_projects_from_github_select_auth_method.png | Bin 42043 -> 17613 bytes doc/workflow/merge_commits.png | Bin 22181 -> 7564 bytes doc/workflow/merge_request.png | Bin 98070 -> 47240 bytes doc/workflow/messy_flow.png | Bin 19314 -> 11665 bytes doc/workflow/milestones/form.png | Bin 84872 -> 40414 bytes doc/workflow/milestones/group_form.png | Bin 74429 -> 35820 bytes doc/workflow/mr_inline_comments.png | Bin 117313 -> 52519 bytes doc/workflow/notifications/settings.png | Bin 59256 -> 37542 bytes doc/workflow/production_branch.png | Bin 10946 -> 7264 bytes doc/workflow/rebase.png | Bin 68976 -> 29009 bytes doc/workflow/release_branches.png | Bin 22163 -> 12746 bytes doc/workflow/releases/new_tag.png | Bin 87330 -> 42456 bytes doc/workflow/releases/tags.png | Bin 93016 -> 44666 bytes doc/workflow/remove_checkbox.png | Bin 12339 -> 6904 bytes 384 files changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/administration/img/custom_hooks_error_msg.png b/doc/administration/img/custom_hooks_error_msg.png index 92e87e15fb3..1b3277bef16 100644 Binary files a/doc/administration/img/custom_hooks_error_msg.png and b/doc/administration/img/custom_hooks_error_msg.png differ diff --git a/doc/administration/img/high_availability/active-active-diagram.png b/doc/administration/img/high_availability/active-active-diagram.png index 81259e0ae93..4f5984b88fe 100644 Binary files a/doc/administration/img/high_availability/active-active-diagram.png and b/doc/administration/img/high_availability/active-active-diagram.png differ diff --git a/doc/administration/img/high_availability/active-passive-diagram.png b/doc/administration/img/high_availability/active-passive-diagram.png index f69ff1d0357..3b42ce5911c 100644 Binary files a/doc/administration/img/high_availability/active-passive-diagram.png and b/doc/administration/img/high_availability/active-passive-diagram.png differ diff --git a/doc/administration/img/housekeeping_settings.png b/doc/administration/img/housekeeping_settings.png index 6ebc6205635..acc4506993a 100644 Binary files a/doc/administration/img/housekeeping_settings.png and b/doc/administration/img/housekeeping_settings.png differ diff --git a/doc/administration/img/raketasks/check_repos_output.png b/doc/administration/img/raketasks/check_repos_output.png index 1f632566b00..7fda2ba0c0f 100644 Binary files a/doc/administration/img/raketasks/check_repos_output.png and b/doc/administration/img/raketasks/check_repos_output.png differ diff --git a/doc/administration/img/repository_storages_admin_ui.png b/doc/administration/img/repository_storages_admin_ui.png index 6481baca1ad..3e76c5b282c 100644 Binary files a/doc/administration/img/repository_storages_admin_ui.png and b/doc/administration/img/repository_storages_admin_ui.png differ diff --git a/doc/administration/monitoring/performance/img/grafana_dashboard_dropdown.png b/doc/administration/monitoring/performance/img/grafana_dashboard_dropdown.png index 7e34fad71ce..51eef90068d 100644 Binary files a/doc/administration/monitoring/performance/img/grafana_dashboard_dropdown.png and b/doc/administration/monitoring/performance/img/grafana_dashboard_dropdown.png differ diff --git a/doc/administration/monitoring/performance/img/grafana_dashboard_import.png b/doc/administration/monitoring/performance/img/grafana_dashboard_import.png index f97624365c7..7761ea00522 100644 Binary files a/doc/administration/monitoring/performance/img/grafana_dashboard_import.png and b/doc/administration/monitoring/performance/img/grafana_dashboard_import.png differ diff --git a/doc/administration/monitoring/performance/img/grafana_data_source_configuration.png b/doc/administration/monitoring/performance/img/grafana_data_source_configuration.png index 7d50e4c88c2..3e749eb8f9d 100644 Binary files a/doc/administration/monitoring/performance/img/grafana_data_source_configuration.png and b/doc/administration/monitoring/performance/img/grafana_data_source_configuration.png differ diff --git a/doc/administration/monitoring/performance/img/grafana_data_source_empty.png b/doc/administration/monitoring/performance/img/grafana_data_source_empty.png index aa39a53acae..33fcaaaef64 100644 Binary files a/doc/administration/monitoring/performance/img/grafana_data_source_empty.png and b/doc/administration/monitoring/performance/img/grafana_data_source_empty.png differ diff --git a/doc/administration/monitoring/performance/img/grafana_save_icon.png b/doc/administration/monitoring/performance/img/grafana_save_icon.png index c740e33cd1c..c18f2147e9d 100644 Binary files a/doc/administration/monitoring/performance/img/grafana_save_icon.png and b/doc/administration/monitoring/performance/img/grafana_save_icon.png differ diff --git a/doc/administration/monitoring/performance/img/metrics_gitlab_configuration_settings.png b/doc/administration/monitoring/performance/img/metrics_gitlab_configuration_settings.png index db396423e30..13bfd097b81 100644 Binary files a/doc/administration/monitoring/performance/img/metrics_gitlab_configuration_settings.png and b/doc/administration/monitoring/performance/img/metrics_gitlab_configuration_settings.png differ diff --git a/doc/administration/monitoring/performance/img/request_profile_result.png b/doc/administration/monitoring/performance/img/request_profile_result.png index 73e2fdcab67..8ebd74c2d3c 100644 Binary files a/doc/administration/monitoring/performance/img/request_profile_result.png and b/doc/administration/monitoring/performance/img/request_profile_result.png differ diff --git a/doc/administration/monitoring/performance/img/request_profiling_token.png b/doc/administration/monitoring/performance/img/request_profiling_token.png index 04d87567816..9160407e028 100644 Binary files a/doc/administration/monitoring/performance/img/request_profiling_token.png and b/doc/administration/monitoring/performance/img/request_profiling_token.png differ diff --git a/doc/administration/operations/img/sidekiq_job_throttling.png b/doc/administration/operations/img/sidekiq_job_throttling.png index 7f29a4d3c46..dcf40b4bf17 100644 Binary files a/doc/administration/operations/img/sidekiq_job_throttling.png and b/doc/administration/operations/img/sidekiq_job_throttling.png differ diff --git a/doc/ci/img/builds_tab.png b/doc/ci/img/builds_tab.png index 35780e277ae..2d7eec8a949 100644 Binary files a/doc/ci/img/builds_tab.png and b/doc/ci/img/builds_tab.png differ diff --git a/doc/ci/img/deployments_view.png b/doc/ci/img/deployments_view.png index ca6097cbea4..7ded0c97b72 100644 Binary files a/doc/ci/img/deployments_view.png and b/doc/ci/img/deployments_view.png differ diff --git a/doc/ci/img/environments_available_staging.png b/doc/ci/img/environments_available_staging.png index 784c4fd944c..5c031ad0d9d 100644 Binary files a/doc/ci/img/environments_available_staging.png and b/doc/ci/img/environments_available_staging.png differ diff --git a/doc/ci/img/environments_dynamic_groups.png b/doc/ci/img/environments_dynamic_groups.png index e89b66c502c..0f42b368c5b 100644 Binary files a/doc/ci/img/environments_dynamic_groups.png and b/doc/ci/img/environments_dynamic_groups.png differ diff --git a/doc/ci/img/environments_link_url.png b/doc/ci/img/environments_link_url.png index 224c21adfb5..44010f6aa6f 100644 Binary files a/doc/ci/img/environments_link_url.png and b/doc/ci/img/environments_link_url.png differ diff --git a/doc/ci/img/environments_link_url_deployments.png b/doc/ci/img/environments_link_url_deployments.png index 9419668a9bd..4f90143527a 100644 Binary files a/doc/ci/img/environments_link_url_deployments.png and b/doc/ci/img/environments_link_url_deployments.png differ diff --git a/doc/ci/img/environments_link_url_mr.png b/doc/ci/img/environments_link_url_mr.png index 3276dfb6096..64f134e0b0d 100644 Binary files a/doc/ci/img/environments_link_url_mr.png and b/doc/ci/img/environments_link_url_mr.png differ diff --git a/doc/ci/img/environments_manual_action_builds.png b/doc/ci/img/environments_manual_action_builds.png index d4bb7ccdbae..e7cf63a1031 100644 Binary files a/doc/ci/img/environments_manual_action_builds.png and b/doc/ci/img/environments_manual_action_builds.png differ diff --git a/doc/ci/img/environments_manual_action_deployments.png b/doc/ci/img/environments_manual_action_deployments.png index c2477381c80..2b3f6f3edad 100644 Binary files a/doc/ci/img/environments_manual_action_deployments.png and b/doc/ci/img/environments_manual_action_deployments.png differ diff --git a/doc/ci/img/environments_manual_action_environments.png b/doc/ci/img/environments_manual_action_environments.png index 56601c0db2d..e0c07604e7f 100644 Binary files a/doc/ci/img/environments_manual_action_environments.png and b/doc/ci/img/environments_manual_action_environments.png differ diff --git a/doc/ci/img/environments_manual_action_pipelines.png b/doc/ci/img/environments_manual_action_pipelines.png index eb6e87cd956..82bbae88027 100644 Binary files a/doc/ci/img/environments_manual_action_pipelines.png and b/doc/ci/img/environments_manual_action_pipelines.png differ diff --git a/doc/ci/img/environments_manual_action_single_pipeline.png b/doc/ci/img/environments_manual_action_single_pipeline.png index 9713ad212e2..36337cb1870 100644 Binary files a/doc/ci/img/environments_manual_action_single_pipeline.png and b/doc/ci/img/environments_manual_action_single_pipeline.png differ diff --git a/doc/ci/img/environments_mr_review_app.png b/doc/ci/img/environments_mr_review_app.png index a2ae25d62fa..7bff84362a3 100644 Binary files a/doc/ci/img/environments_mr_review_app.png and b/doc/ci/img/environments_mr_review_app.png differ diff --git a/doc/ci/img/environments_view.png b/doc/ci/img/environments_view.png index 131a9718cc4..821352188ef 100644 Binary files a/doc/ci/img/environments_view.png and b/doc/ci/img/environments_view.png differ diff --git a/doc/ci/img/features_settings.png b/doc/ci/img/features_settings.png index 38d7036f606..c159253d1c9 100644 Binary files a/doc/ci/img/features_settings.png and b/doc/ci/img/features_settings.png differ diff --git a/doc/ci/img/pipelines.png b/doc/ci/img/pipelines.png index fb3c69353b0..5937e9d99c8 100644 Binary files a/doc/ci/img/pipelines.png and b/doc/ci/img/pipelines.png differ diff --git a/doc/ci/quick_start/img/build_log.png b/doc/ci/quick_start/img/build_log.png index b53a6cd86b0..87643d62d58 100644 Binary files a/doc/ci/quick_start/img/build_log.png and b/doc/ci/quick_start/img/build_log.png differ diff --git a/doc/ci/quick_start/img/builds_status.png b/doc/ci/quick_start/img/builds_status.png index 47862761ffe..d287ae3064f 100644 Binary files a/doc/ci/quick_start/img/builds_status.png and b/doc/ci/quick_start/img/builds_status.png differ diff --git a/doc/ci/quick_start/img/new_commit.png b/doc/ci/quick_start/img/new_commit.png index a53562ce328..29c2fea5d6d 100644 Binary files a/doc/ci/quick_start/img/new_commit.png and b/doc/ci/quick_start/img/new_commit.png differ diff --git a/doc/ci/quick_start/img/pipelines_status.png b/doc/ci/quick_start/img/pipelines_status.png index 6bc97bb739c..53ccc49bd66 100644 Binary files a/doc/ci/quick_start/img/pipelines_status.png and b/doc/ci/quick_start/img/pipelines_status.png differ diff --git a/doc/ci/quick_start/img/runners_activated.png b/doc/ci/quick_start/img/runners_activated.png index 23261123b18..5ce6fe8e17c 100644 Binary files a/doc/ci/quick_start/img/runners_activated.png and b/doc/ci/quick_start/img/runners_activated.png differ diff --git a/doc/ci/quick_start/img/single_commit_status_pending.png b/doc/ci/quick_start/img/single_commit_status_pending.png index ccf3ac957bb..91fc9011847 100644 Binary files a/doc/ci/quick_start/img/single_commit_status_pending.png and b/doc/ci/quick_start/img/single_commit_status_pending.png differ diff --git a/doc/ci/quick_start/img/status_pending.png b/doc/ci/quick_start/img/status_pending.png index 9feacf0c961..cbd44a189d3 100644 Binary files a/doc/ci/quick_start/img/status_pending.png and b/doc/ci/quick_start/img/status_pending.png differ diff --git a/doc/ci/review_apps/img/review_apps_preview_in_mr.png b/doc/ci/review_apps/img/review_apps_preview_in_mr.png index 15bcb90518c..0300392f24b 100644 Binary files a/doc/ci/review_apps/img/review_apps_preview_in_mr.png and b/doc/ci/review_apps/img/review_apps_preview_in_mr.png differ diff --git a/doc/ci/triggers/img/builds_page.png b/doc/ci/triggers/img/builds_page.png index c2cf4b1852c..fded5839f76 100644 Binary files a/doc/ci/triggers/img/builds_page.png and b/doc/ci/triggers/img/builds_page.png differ diff --git a/doc/ci/triggers/img/trigger_single_build.png b/doc/ci/triggers/img/trigger_single_build.png index fa86f0fee3d..c4a5550d640 100644 Binary files a/doc/ci/triggers/img/trigger_single_build.png and b/doc/ci/triggers/img/trigger_single_build.png differ diff --git a/doc/ci/triggers/img/trigger_variables.png b/doc/ci/triggers/img/trigger_variables.png index b2fcc65d304..65fe1ea9ab6 100644 Binary files a/doc/ci/triggers/img/trigger_variables.png and b/doc/ci/triggers/img/trigger_variables.png differ diff --git a/doc/ci/triggers/img/triggers_page.png b/doc/ci/triggers/img/triggers_page.png index 438f285ae2d..56d13905ce6 100644 Binary files a/doc/ci/triggers/img/triggers_page.png and b/doc/ci/triggers/img/triggers_page.png differ diff --git a/doc/customization/branded_login_page/appearance.png b/doc/customization/branded_login_page/appearance.png index 023dc5599b4..31ea4559d37 100644 Binary files a/doc/customization/branded_login_page/appearance.png and b/doc/customization/branded_login_page/appearance.png differ diff --git a/doc/customization/branded_login_page/custom_sign_in.png b/doc/customization/branded_login_page/custom_sign_in.png index 7d99e0a2b3b..c0888fe1f18 100644 Binary files a/doc/customization/branded_login_page/custom_sign_in.png and b/doc/customization/branded_login_page/custom_sign_in.png differ diff --git a/doc/customization/branded_login_page/default_login_page.png b/doc/customization/branded_login_page/default_login_page.png index 0cfa9da202e..9b1233cef45 100644 Binary files a/doc/customization/branded_login_page/default_login_page.png and b/doc/customization/branded_login_page/default_login_page.png differ diff --git a/doc/development/gitlab_architecture_diagram.png b/doc/development/gitlab_architecture_diagram.png index 80e975718e0..cda5ce254ce 100644 Binary files a/doc/development/gitlab_architecture_diagram.png and b/doc/development/gitlab_architecture_diagram.png differ diff --git a/doc/development/img/state-model-issue.png b/doc/development/img/state-model-issue.png index c85fffc2a3a..ee33b6886c6 100644 Binary files a/doc/development/img/state-model-issue.png and b/doc/development/img/state-model-issue.png differ diff --git a/doc/development/img/state-model-legend.png b/doc/development/img/state-model-legend.png index 088230bfc39..1c121f2588c 100644 Binary files a/doc/development/img/state-model-legend.png and b/doc/development/img/state-model-legend.png differ diff --git a/doc/development/img/state-model-merge-request.png b/doc/development/img/state-model-merge-request.png index 0e7556784f4..e00da10cac2 100644 Binary files a/doc/development/img/state-model-merge-request.png and b/doc/development/img/state-model-merge-request.png differ diff --git a/doc/development/ux_guide/img/button-primary.png b/doc/development/ux_guide/img/button-primary.png index f4c673f5b88..eda5ed84aec 100644 Binary files a/doc/development/ux_guide/img/button-primary.png and b/doc/development/ux_guide/img/button-primary.png differ diff --git a/doc/development/ux_guide/img/button-secondary.png b/doc/development/ux_guide/img/button-secondary.png index 57fa65b247c..26d4e8cf43d 100644 Binary files a/doc/development/ux_guide/img/button-secondary.png and b/doc/development/ux_guide/img/button-secondary.png differ diff --git a/doc/development/ux_guide/img/color-blue.png b/doc/development/ux_guide/img/color-blue.png index 6449613eb16..2ca360173eb 100644 Binary files a/doc/development/ux_guide/img/color-blue.png and b/doc/development/ux_guide/img/color-blue.png differ diff --git a/doc/development/ux_guide/img/color-green.png b/doc/development/ux_guide/img/color-green.png index 15475b36f02..489db8f4343 100644 Binary files a/doc/development/ux_guide/img/color-green.png and b/doc/development/ux_guide/img/color-green.png differ diff --git a/doc/development/ux_guide/img/color-orange.png b/doc/development/ux_guide/img/color-orange.png index f4fc09b2d9b..4c4b772d438 100644 Binary files a/doc/development/ux_guide/img/color-orange.png and b/doc/development/ux_guide/img/color-orange.png differ diff --git a/doc/development/ux_guide/img/color-red.png b/doc/development/ux_guide/img/color-red.png index 6fbbf0a885d..3440ad48f05 100644 Binary files a/doc/development/ux_guide/img/color-red.png and b/doc/development/ux_guide/img/color-red.png differ diff --git a/doc/development/ux_guide/img/components-alerts.png b/doc/development/ux_guide/img/components-alerts.png index 0b2ecc16a5f..66a43ac69e1 100644 Binary files a/doc/development/ux_guide/img/components-alerts.png and b/doc/development/ux_guide/img/components-alerts.png differ diff --git a/doc/development/ux_guide/img/components-anchorlinks.png b/doc/development/ux_guide/img/components-anchorlinks.png index 950f348277d..7dd6a8a3876 100644 Binary files a/doc/development/ux_guide/img/components-anchorlinks.png and b/doc/development/ux_guide/img/components-anchorlinks.png differ diff --git a/doc/development/ux_guide/img/components-contentblock.png b/doc/development/ux_guide/img/components-contentblock.png index 31fc1eec9df..58d87729701 100644 Binary files a/doc/development/ux_guide/img/components-contentblock.png and b/doc/development/ux_guide/img/components-contentblock.png differ diff --git a/doc/development/ux_guide/img/components-coverblock.png b/doc/development/ux_guide/img/components-coverblock.png index c8f1f87a108..fb135f9648a 100644 Binary files a/doc/development/ux_guide/img/components-coverblock.png and b/doc/development/ux_guide/img/components-coverblock.png differ diff --git a/doc/development/ux_guide/img/components-dateexact.png b/doc/development/ux_guide/img/components-dateexact.png index 8c0c5c1be40..686ca727293 100644 Binary files a/doc/development/ux_guide/img/components-dateexact.png and b/doc/development/ux_guide/img/components-dateexact.png differ diff --git a/doc/development/ux_guide/img/components-daterelative.png b/doc/development/ux_guide/img/components-daterelative.png index 1dc6d89e4ef..4954dfb51b3 100644 Binary files a/doc/development/ux_guide/img/components-daterelative.png and b/doc/development/ux_guide/img/components-daterelative.png differ diff --git a/doc/development/ux_guide/img/components-dropdown.png b/doc/development/ux_guide/img/components-dropdown.png index 5770a393b37..7f9a701c089 100644 Binary files a/doc/development/ux_guide/img/components-dropdown.png and b/doc/development/ux_guide/img/components-dropdown.png differ diff --git a/doc/development/ux_guide/img/components-fileholder.png b/doc/development/ux_guide/img/components-fileholder.png index 4b8962905d6..ec2911a1232 100644 Binary files a/doc/development/ux_guide/img/components-fileholder.png and b/doc/development/ux_guide/img/components-fileholder.png differ diff --git a/doc/development/ux_guide/img/components-horizontalform.png b/doc/development/ux_guide/img/components-horizontalform.png index 92e28cf9afc..c57dceda43a 100644 Binary files a/doc/development/ux_guide/img/components-horizontalform.png and b/doc/development/ux_guide/img/components-horizontalform.png differ diff --git a/doc/development/ux_guide/img/components-listinsidepanel.png b/doc/development/ux_guide/img/components-listinsidepanel.png index 30ceb3eaa08..3a72d39bb5d 100644 Binary files a/doc/development/ux_guide/img/components-listinsidepanel.png and b/doc/development/ux_guide/img/components-listinsidepanel.png differ diff --git a/doc/development/ux_guide/img/components-listwithavatar.png b/doc/development/ux_guide/img/components-listwithavatar.png index d3cb0ebc02b..f6db575433c 100644 Binary files a/doc/development/ux_guide/img/components-listwithavatar.png and b/doc/development/ux_guide/img/components-listwithavatar.png differ diff --git a/doc/development/ux_guide/img/components-listwithhover.png b/doc/development/ux_guide/img/components-listwithhover.png index 1484ecba6a0..8521a8ad53e 100644 Binary files a/doc/development/ux_guide/img/components-listwithhover.png and b/doc/development/ux_guide/img/components-listwithhover.png differ diff --git a/doc/development/ux_guide/img/components-panels.png b/doc/development/ux_guide/img/components-panels.png index 6e71d0ad9c9..c1391ca07e5 100644 Binary files a/doc/development/ux_guide/img/components-panels.png and b/doc/development/ux_guide/img/components-panels.png differ diff --git a/doc/development/ux_guide/img/components-referencehover.png b/doc/development/ux_guide/img/components-referencehover.png index e9fb27e2aa9..f80564dbb16 100644 Binary files a/doc/development/ux_guide/img/components-referencehover.png and b/doc/development/ux_guide/img/components-referencehover.png differ diff --git a/doc/development/ux_guide/img/components-referenceissues.png b/doc/development/ux_guide/img/components-referenceissues.png index caf9477db38..51fb2cf3e43 100644 Binary files a/doc/development/ux_guide/img/components-referenceissues.png and b/doc/development/ux_guide/img/components-referenceissues.png differ diff --git a/doc/development/ux_guide/img/components-referencelabels.png b/doc/development/ux_guide/img/components-referencelabels.png index a122b45d1f1..aba450cc3ba 100644 Binary files a/doc/development/ux_guide/img/components-referencelabels.png and b/doc/development/ux_guide/img/components-referencelabels.png differ diff --git a/doc/development/ux_guide/img/components-referencemilestone.png b/doc/development/ux_guide/img/components-referencemilestone.png index 5aa9ecd1a78..adf2555ccf8 100644 Binary files a/doc/development/ux_guide/img/components-referencemilestone.png and b/doc/development/ux_guide/img/components-referencemilestone.png differ diff --git a/doc/development/ux_guide/img/components-referencemrs.png b/doc/development/ux_guide/img/components-referencemrs.png index 6280243859a..6c3375f1ea1 100644 Binary files a/doc/development/ux_guide/img/components-referencemrs.png and b/doc/development/ux_guide/img/components-referencemrs.png differ diff --git a/doc/development/ux_guide/img/components-referencepeople.png b/doc/development/ux_guide/img/components-referencepeople.png index 99772a539cf..b8dd431e2e6 100644 Binary files a/doc/development/ux_guide/img/components-referencepeople.png and b/doc/development/ux_guide/img/components-referencepeople.png differ diff --git a/doc/development/ux_guide/img/components-rowcontentblock.png b/doc/development/ux_guide/img/components-rowcontentblock.png index 1c2d7096955..c66a50f9564 100644 Binary files a/doc/development/ux_guide/img/components-rowcontentblock.png and b/doc/development/ux_guide/img/components-rowcontentblock.png differ diff --git a/doc/development/ux_guide/img/components-simplelist.png b/doc/development/ux_guide/img/components-simplelist.png index 892f507cfc2..858e5064c25 100644 Binary files a/doc/development/ux_guide/img/components-simplelist.png and b/doc/development/ux_guide/img/components-simplelist.png differ diff --git a/doc/development/ux_guide/img/components-table.png b/doc/development/ux_guide/img/components-table.png index 7e964c885cf..cedc55758a9 100644 Binary files a/doc/development/ux_guide/img/components-table.png and b/doc/development/ux_guide/img/components-table.png differ diff --git a/doc/development/ux_guide/img/components-verticalform.png b/doc/development/ux_guide/img/components-verticalform.png index 38863ad3c1c..489ae6f862f 100644 Binary files a/doc/development/ux_guide/img/components-verticalform.png and b/doc/development/ux_guide/img/components-verticalform.png differ diff --git a/doc/development/ux_guide/img/copy-form-addissuebutton.png b/doc/development/ux_guide/img/copy-form-addissuebutton.png index 18839d447e8..8457f0ab2ab 100644 Binary files a/doc/development/ux_guide/img/copy-form-addissuebutton.png and b/doc/development/ux_guide/img/copy-form-addissuebutton.png differ diff --git a/doc/development/ux_guide/img/copy-form-addissueform.png b/doc/development/ux_guide/img/copy-form-addissueform.png index e6838c06eca..89c6b4acdfb 100644 Binary files a/doc/development/ux_guide/img/copy-form-addissueform.png and b/doc/development/ux_guide/img/copy-form-addissueform.png differ diff --git a/doc/development/ux_guide/img/copy-form-editissuebutton.png b/doc/development/ux_guide/img/copy-form-editissuebutton.png index 2435820e14f..04bcc2bf831 100644 Binary files a/doc/development/ux_guide/img/copy-form-editissuebutton.png and b/doc/development/ux_guide/img/copy-form-editissuebutton.png differ diff --git a/doc/development/ux_guide/img/copy-form-editissueform.png b/doc/development/ux_guide/img/copy-form-editissueform.png index 5ddeda33e68..126ef34ea7e 100644 Binary files a/doc/development/ux_guide/img/copy-form-editissueform.png and b/doc/development/ux_guide/img/copy-form-editissueform.png differ diff --git a/doc/development/ux_guide/img/features-contextualnav.png b/doc/development/ux_guide/img/features-contextualnav.png index df157f54c84..f8466f28627 100644 Binary files a/doc/development/ux_guide/img/features-contextualnav.png and b/doc/development/ux_guide/img/features-contextualnav.png differ diff --git a/doc/development/ux_guide/img/features-emptystates.png b/doc/development/ux_guide/img/features-emptystates.png index 3befc14588e..51835a7080b 100644 Binary files a/doc/development/ux_guide/img/features-emptystates.png and b/doc/development/ux_guide/img/features-emptystates.png differ diff --git a/doc/development/ux_guide/img/features-filters.png b/doc/development/ux_guide/img/features-filters.png index 281e55d590c..41db76db938 100644 Binary files a/doc/development/ux_guide/img/features-filters.png and b/doc/development/ux_guide/img/features-filters.png differ diff --git a/doc/development/ux_guide/img/features-globalnav.png b/doc/development/ux_guide/img/features-globalnav.png index 3c0db2247ca..73294d1b524 100644 Binary files a/doc/development/ux_guide/img/features-globalnav.png and b/doc/development/ux_guide/img/features-globalnav.png differ diff --git a/doc/development/ux_guide/img/surfaces-contentitemtitle.png b/doc/development/ux_guide/img/surfaces-contentitemtitle.png index 2eb926c1c43..3af0b56c8fb 100644 Binary files a/doc/development/ux_guide/img/surfaces-contentitemtitle.png and b/doc/development/ux_guide/img/surfaces-contentitemtitle.png differ diff --git a/doc/development/ux_guide/img/surfaces-header.png b/doc/development/ux_guide/img/surfaces-header.png index ab44d4de696..ba616388003 100644 Binary files a/doc/development/ux_guide/img/surfaces-header.png and b/doc/development/ux_guide/img/surfaces-header.png differ diff --git a/doc/development/ux_guide/img/surfaces-systeminformationblock.png b/doc/development/ux_guide/img/surfaces-systeminformationblock.png index 5d91e993e24..9f42f1d4dd0 100644 Binary files a/doc/development/ux_guide/img/surfaces-systeminformationblock.png and b/doc/development/ux_guide/img/surfaces-systeminformationblock.png differ diff --git a/doc/development/ux_guide/img/surfaces-ux.png b/doc/development/ux_guide/img/surfaces-ux.png index e692c51e8c0..53208727c64 100644 Binary files a/doc/development/ux_guide/img/surfaces-ux.png and b/doc/development/ux_guide/img/surfaces-ux.png differ diff --git a/doc/development/ux_guide/img/tooltip-placement.png b/doc/development/ux_guide/img/tooltip-placement.png index 29a61c8400a..061f82e4df0 100644 Binary files a/doc/development/ux_guide/img/tooltip-placement.png and b/doc/development/ux_guide/img/tooltip-placement.png differ diff --git a/doc/development/ux_guide/img/tooltip-usage.png b/doc/development/ux_guide/img/tooltip-usage.png index e8e4c6ded91..40c4f051cd0 100644 Binary files a/doc/development/ux_guide/img/tooltip-usage.png and b/doc/development/ux_guide/img/tooltip-usage.png differ diff --git a/doc/gitlab-basics/img/create_new_group_info.png b/doc/gitlab-basics/img/create_new_group_info.png index c8eddfd1bbb..020b4ac00d6 100644 Binary files a/doc/gitlab-basics/img/create_new_group_info.png and b/doc/gitlab-basics/img/create_new_group_info.png differ diff --git a/doc/gitlab-basics/img/create_new_group_sidebar.png b/doc/gitlab-basics/img/create_new_group_sidebar.png index 28017ee02e0..fa88d1d51c0 100644 Binary files a/doc/gitlab-basics/img/create_new_group_sidebar.png and b/doc/gitlab-basics/img/create_new_group_sidebar.png differ diff --git a/doc/gitlab-basics/img/create_new_project_button.png b/doc/gitlab-basics/img/create_new_project_button.png index e7c794d943f..a19f0e57b56 100644 Binary files a/doc/gitlab-basics/img/create_new_project_button.png and b/doc/gitlab-basics/img/create_new_project_button.png differ diff --git a/doc/gitlab-basics/img/create_new_project_from_group.png b/doc/gitlab-basics/img/create_new_project_from_group.png index 6d41d17f9ca..c35234660db 100644 Binary files a/doc/gitlab-basics/img/create_new_project_from_group.png and b/doc/gitlab-basics/img/create_new_project_from_group.png differ diff --git a/doc/gitlab-basics/img/create_new_project_info.png b/doc/gitlab-basics/img/create_new_project_info.png index 16d56f0707f..fcfbca87b91 100644 Binary files a/doc/gitlab-basics/img/create_new_project_info.png and b/doc/gitlab-basics/img/create_new_project_info.png differ diff --git a/doc/gitlab-basics/img/fork_choose_namespace.png b/doc/gitlab-basics/img/fork_choose_namespace.png index 82c9c3bd39e..4c50276d5ad 100644 Binary files a/doc/gitlab-basics/img/fork_choose_namespace.png and b/doc/gitlab-basics/img/fork_choose_namespace.png differ diff --git a/doc/gitlab-basics/img/fork_new.png b/doc/gitlab-basics/img/fork_new.png index 41885223286..fa185fdaca1 100644 Binary files a/doc/gitlab-basics/img/fork_new.png and b/doc/gitlab-basics/img/fork_new.png differ diff --git a/doc/gitlab-basics/img/merge_request_new.png b/doc/gitlab-basics/img/merge_request_new.png index 0aba5743f01..6fcd7bebada 100644 Binary files a/doc/gitlab-basics/img/merge_request_new.png and b/doc/gitlab-basics/img/merge_request_new.png differ diff --git a/doc/gitlab-basics/img/merge_request_page.png b/doc/gitlab-basics/img/merge_request_page.png index 68c3bbf9444..f6087294e22 100644 Binary files a/doc/gitlab-basics/img/merge_request_page.png and b/doc/gitlab-basics/img/merge_request_page.png differ diff --git a/doc/gitlab-basics/img/merge_request_select_branch.png b/doc/gitlab-basics/img/merge_request_select_branch.png index 516436ff6cc..9f6b93943a9 100644 Binary files a/doc/gitlab-basics/img/merge_request_select_branch.png and b/doc/gitlab-basics/img/merge_request_select_branch.png differ diff --git a/doc/gitlab-basics/img/new_issue_button.png b/doc/gitlab-basics/img/new_issue_button.png index 46b626bed65..3b113471f0c 100644 Binary files a/doc/gitlab-basics/img/new_issue_button.png and b/doc/gitlab-basics/img/new_issue_button.png differ diff --git a/doc/gitlab-basics/img/new_issue_page.png b/doc/gitlab-basics/img/new_issue_page.png index 843504130b7..ce3e60df276 100644 Binary files a/doc/gitlab-basics/img/new_issue_page.png and b/doc/gitlab-basics/img/new_issue_page.png differ diff --git a/doc/gitlab-basics/img/profile_settings.png b/doc/gitlab-basics/img/profile_settings.png index f0abd478849..26df4c0a734 100644 Binary files a/doc/gitlab-basics/img/profile_settings.png and b/doc/gitlab-basics/img/profile_settings.png differ diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys.png b/doc/gitlab-basics/img/profile_settings_ssh_keys.png index 2c9a42fe10c..8ac603a2af9 100644 Binary files a/doc/gitlab-basics/img/profile_settings_ssh_keys.png and b/doc/gitlab-basics/img/profile_settings_ssh_keys.png differ diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.png b/doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.png index cd7add6937f..5e501ec86ef 100644 Binary files a/doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.png and b/doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.png differ diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png b/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png index 095beb02be8..6a1430d9663 100644 Binary files a/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png and b/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png differ diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys_title.png b/doc/gitlab-basics/img/profile_settings_ssh_keys_title.png index 4b998a7f948..89a04c17fed 100644 Binary files a/doc/gitlab-basics/img/profile_settings_ssh_keys_title.png and b/doc/gitlab-basics/img/profile_settings_ssh_keys_title.png differ diff --git a/doc/gitlab-basics/img/project_clone_url.png b/doc/gitlab-basics/img/project_clone_url.png index eed430e1036..bdd7d011db3 100644 Binary files a/doc/gitlab-basics/img/project_clone_url.png and b/doc/gitlab-basics/img/project_clone_url.png differ diff --git a/doc/gitlab-basics/img/project_navbar.png b/doc/gitlab-basics/img/project_navbar.png index 97cf3cd9702..be6f38ede32 100644 Binary files a/doc/gitlab-basics/img/project_navbar.png and b/doc/gitlab-basics/img/project_navbar.png differ diff --git a/doc/gitlab-basics/img/select_group_dropdown.png b/doc/gitlab-basics/img/select_group_dropdown.png index 7d8b89c2df9..68fc950304c 100644 Binary files a/doc/gitlab-basics/img/select_group_dropdown.png and b/doc/gitlab-basics/img/select_group_dropdown.png differ diff --git a/doc/integration/img/akismet_settings.png b/doc/integration/img/akismet_settings.png index c2aa97b132e..689654bf960 100644 Binary files a/doc/integration/img/akismet_settings.png and b/doc/integration/img/akismet_settings.png differ diff --git a/doc/integration/img/bitbucket_oauth_keys.png b/doc/integration/img/bitbucket_oauth_keys.png index 3fb2f7524a3..6dd2c7d744e 100644 Binary files a/doc/integration/img/bitbucket_oauth_keys.png and b/doc/integration/img/bitbucket_oauth_keys.png differ diff --git a/doc/integration/img/bitbucket_oauth_settings_page.png b/doc/integration/img/bitbucket_oauth_settings_page.png index a3047712d8c..8dbee9762d7 100644 Binary files a/doc/integration/img/bitbucket_oauth_settings_page.png and b/doc/integration/img/bitbucket_oauth_settings_page.png differ diff --git a/doc/integration/img/enabled-oauth-sign-in-sources.png b/doc/integration/img/enabled-oauth-sign-in-sources.png index b23d6dcc595..f145aeae75c 100644 Binary files a/doc/integration/img/enabled-oauth-sign-in-sources.png and b/doc/integration/img/enabled-oauth-sign-in-sources.png differ diff --git a/doc/integration/img/facebook_api_keys.png b/doc/integration/img/facebook_api_keys.png index 995845d5a69..9463ec1e7a3 100644 Binary files a/doc/integration/img/facebook_api_keys.png and b/doc/integration/img/facebook_api_keys.png differ diff --git a/doc/integration/img/facebook_app_settings.png b/doc/integration/img/facebook_app_settings.png index 1cd586ecd7c..81f38cab16e 100644 Binary files a/doc/integration/img/facebook_app_settings.png and b/doc/integration/img/facebook_app_settings.png differ diff --git a/doc/integration/img/facebook_website_url.png b/doc/integration/img/facebook_website_url.png index 10e1bd5d5a6..67d78d13951 100644 Binary files a/doc/integration/img/facebook_website_url.png and b/doc/integration/img/facebook_website_url.png differ diff --git a/doc/integration/img/github_app.png b/doc/integration/img/github_app.png index de31242679a..d6c289a1de1 100644 Binary files a/doc/integration/img/github_app.png and b/doc/integration/img/github_app.png differ diff --git a/doc/integration/img/gitlab_app.png b/doc/integration/img/gitlab_app.png index 065316fd3c7..b4958581a9b 100644 Binary files a/doc/integration/img/gitlab_app.png and b/doc/integration/img/gitlab_app.png differ diff --git a/doc/integration/img/gmail_action_buttons_for_gitlab.png b/doc/integration/img/gmail_action_buttons_for_gitlab.png index a6704139091..0e3e24d6ffc 100644 Binary files a/doc/integration/img/gmail_action_buttons_for_gitlab.png and b/doc/integration/img/gmail_action_buttons_for_gitlab.png differ diff --git a/doc/integration/img/google_app.png b/doc/integration/img/google_app.png index 08f7f714553..9fda06dabb1 100644 Binary files a/doc/integration/img/google_app.png and b/doc/integration/img/google_app.png differ diff --git a/doc/integration/img/oauth_provider_admin_application.png b/doc/integration/img/oauth_provider_admin_application.png index fc5f7596fcc..c8ecce129c8 100644 Binary files a/doc/integration/img/oauth_provider_admin_application.png and b/doc/integration/img/oauth_provider_admin_application.png differ diff --git a/doc/integration/img/oauth_provider_application_form.png b/doc/integration/img/oauth_provider_application_form.png index 606ab3e3467..954681e054e 100644 Binary files a/doc/integration/img/oauth_provider_application_form.png and b/doc/integration/img/oauth_provider_application_form.png differ diff --git a/doc/integration/img/oauth_provider_application_id_secret.png b/doc/integration/img/oauth_provider_application_id_secret.png index cbedcef8376..65cca5f1e1b 100644 Binary files a/doc/integration/img/oauth_provider_application_id_secret.png and b/doc/integration/img/oauth_provider_application_id_secret.png differ diff --git a/doc/integration/img/oauth_provider_authorized_application.png b/doc/integration/img/oauth_provider_authorized_application.png index 6a2ea09073c..ed99db3476d 100644 Binary files a/doc/integration/img/oauth_provider_authorized_application.png and b/doc/integration/img/oauth_provider_authorized_application.png differ diff --git a/doc/integration/img/oauth_provider_user_wide_applications.png b/doc/integration/img/oauth_provider_user_wide_applications.png index 0c7b095a2dd..9cc12555574 100644 Binary files a/doc/integration/img/oauth_provider_user_wide_applications.png and b/doc/integration/img/oauth_provider_user_wide_applications.png differ diff --git a/doc/integration/img/spam_log.png b/doc/integration/img/spam_log.png index 8d574448690..43e267daff4 100644 Binary files a/doc/integration/img/spam_log.png and b/doc/integration/img/spam_log.png differ diff --git a/doc/integration/img/submit_issue.png b/doc/integration/img/submit_issue.png index 5c7896a7eec..8accb78faf3 100644 Binary files a/doc/integration/img/submit_issue.png and b/doc/integration/img/submit_issue.png differ diff --git a/doc/integration/img/twitter_app_api_keys.png b/doc/integration/img/twitter_app_api_keys.png index 15b29ac7d16..34e3c3ba001 100644 Binary files a/doc/integration/img/twitter_app_api_keys.png and b/doc/integration/img/twitter_app_api_keys.png differ diff --git a/doc/integration/img/twitter_app_details.png b/doc/integration/img/twitter_app_details.png index 323112a88bb..b53f4eb3202 100644 Binary files a/doc/integration/img/twitter_app_details.png and b/doc/integration/img/twitter_app_details.png differ diff --git a/doc/monitoring/performance/img/grafana_dashboard_dropdown.png b/doc/monitoring/performance/img/grafana_dashboard_dropdown.png index 7e34fad71ce..51eef90068d 100644 Binary files a/doc/monitoring/performance/img/grafana_dashboard_dropdown.png and b/doc/monitoring/performance/img/grafana_dashboard_dropdown.png differ diff --git a/doc/monitoring/performance/img/grafana_dashboard_import.png b/doc/monitoring/performance/img/grafana_dashboard_import.png index f97624365c7..7761ea00522 100644 Binary files a/doc/monitoring/performance/img/grafana_dashboard_import.png and b/doc/monitoring/performance/img/grafana_dashboard_import.png differ diff --git a/doc/monitoring/performance/img/grafana_data_source_configuration.png b/doc/monitoring/performance/img/grafana_data_source_configuration.png index 7d50e4c88c2..3e749eb8f9d 100644 Binary files a/doc/monitoring/performance/img/grafana_data_source_configuration.png and b/doc/monitoring/performance/img/grafana_data_source_configuration.png differ diff --git a/doc/monitoring/performance/img/grafana_data_source_empty.png b/doc/monitoring/performance/img/grafana_data_source_empty.png index aa39a53acae..33fcaaaef64 100644 Binary files a/doc/monitoring/performance/img/grafana_data_source_empty.png and b/doc/monitoring/performance/img/grafana_data_source_empty.png differ diff --git a/doc/monitoring/performance/img/grafana_save_icon.png b/doc/monitoring/performance/img/grafana_save_icon.png index c740e33cd1c..c18f2147e9d 100644 Binary files a/doc/monitoring/performance/img/grafana_save_icon.png and b/doc/monitoring/performance/img/grafana_save_icon.png differ diff --git a/doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png b/doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png index e6ed45a0386..d96a18ebc04 100644 Binary files a/doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png and b/doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png differ diff --git a/doc/profile/2fa_u2f_authenticate.png b/doc/profile/2fa_u2f_authenticate.png index b9138ff60db..b224ab14195 100644 Binary files a/doc/profile/2fa_u2f_authenticate.png and b/doc/profile/2fa_u2f_authenticate.png differ diff --git a/doc/profile/2fa_u2f_register.png b/doc/profile/2fa_u2f_register.png index 15b3683ef73..1cc142aa851 100644 Binary files a/doc/profile/2fa_u2f_register.png and b/doc/profile/2fa_u2f_register.png differ diff --git a/doc/project_services/img/builds_emails_service.png b/doc/project_services/img/builds_emails_service.png index 440728795be..9dbbed03833 100644 Binary files a/doc/project_services/img/builds_emails_service.png and b/doc/project_services/img/builds_emails_service.png differ diff --git a/doc/project_services/img/emails_on_push_service.png b/doc/project_services/img/emails_on_push_service.png index cd6f79ad1eb..df301aa1eeb 100644 Binary files a/doc/project_services/img/emails_on_push_service.png and b/doc/project_services/img/emails_on_push_service.png differ diff --git a/doc/project_services/img/jira_add_user_to_group.png b/doc/project_services/img/jira_add_user_to_group.png index 0ba737bda9a..27dac49260c 100644 Binary files a/doc/project_services/img/jira_add_user_to_group.png and b/doc/project_services/img/jira_add_user_to_group.png differ diff --git a/doc/project_services/img/jira_create_new_group.png b/doc/project_services/img/jira_create_new_group.png index 0609060cb05..06c4e84fc61 100644 Binary files a/doc/project_services/img/jira_create_new_group.png and b/doc/project_services/img/jira_create_new_group.png differ diff --git a/doc/project_services/img/jira_create_new_group_name.png b/doc/project_services/img/jira_create_new_group_name.png index 53d77b17df0..bfc0dc6b2e9 100644 Binary files a/doc/project_services/img/jira_create_new_group_name.png and b/doc/project_services/img/jira_create_new_group_name.png differ diff --git a/doc/project_services/img/jira_create_new_user.png b/doc/project_services/img/jira_create_new_user.png index 9eaa444ed25..e9c03ed770d 100644 Binary files a/doc/project_services/img/jira_create_new_user.png and b/doc/project_services/img/jira_create_new_user.png differ diff --git a/doc/project_services/img/jira_group_access.png b/doc/project_services/img/jira_group_access.png index 8d4657427ae..9d64cc57269 100644 Binary files a/doc/project_services/img/jira_group_access.png and b/doc/project_services/img/jira_group_access.png differ diff --git a/doc/project_services/img/jira_issue_reference.png b/doc/project_services/img/jira_issue_reference.png index 463200da6aa..72c81460df7 100644 Binary files a/doc/project_services/img/jira_issue_reference.png and b/doc/project_services/img/jira_issue_reference.png differ diff --git a/doc/project_services/img/jira_merge_request_close.png b/doc/project_services/img/jira_merge_request_close.png index b8f6058a514..0f82ceba557 100644 Binary files a/doc/project_services/img/jira_merge_request_close.png and b/doc/project_services/img/jira_merge_request_close.png differ diff --git a/doc/project_services/img/jira_project_name.png b/doc/project_services/img/jira_project_name.png index e785ec6140d..8540a427461 100644 Binary files a/doc/project_services/img/jira_project_name.png and b/doc/project_services/img/jira_project_name.png differ diff --git a/doc/project_services/img/jira_service.png b/doc/project_services/img/jira_service.png index 13aefce6f84..8e073b84ff9 100644 Binary files a/doc/project_services/img/jira_service.png and b/doc/project_services/img/jira_service.png differ diff --git a/doc/project_services/img/jira_service_close_comment.png b/doc/project_services/img/jira_service_close_comment.png index 84a71c692b1..bb9cd7e3d13 100644 Binary files a/doc/project_services/img/jira_service_close_comment.png and b/doc/project_services/img/jira_service_close_comment.png differ diff --git a/doc/project_services/img/jira_service_close_issue.png b/doc/project_services/img/jira_service_close_issue.png index b033b210469..c85b1d1dd97 100644 Binary files a/doc/project_services/img/jira_service_close_issue.png and b/doc/project_services/img/jira_service_close_issue.png differ diff --git a/doc/project_services/img/jira_service_page.png b/doc/project_services/img/jira_service_page.png index 1cda73be83d..c74351b57b8 100644 Binary files a/doc/project_services/img/jira_service_page.png and b/doc/project_services/img/jira_service_page.png differ diff --git a/doc/project_services/img/jira_user_management_link.png b/doc/project_services/img/jira_user_management_link.png index 5f002b59bac..f81c5b5fc87 100644 Binary files a/doc/project_services/img/jira_user_management_link.png and b/doc/project_services/img/jira_user_management_link.png differ diff --git a/doc/project_services/img/jira_workflow_screenshot.png b/doc/project_services/img/jira_workflow_screenshot.png index 937a50a77d9..e62fb202613 100644 Binary files a/doc/project_services/img/jira_workflow_screenshot.png and b/doc/project_services/img/jira_workflow_screenshot.png differ diff --git a/doc/project_services/img/mattermost_add_slash_command.png b/doc/project_services/img/mattermost_add_slash_command.png index 6d45bce8004..7759efa183c 100644 Binary files a/doc/project_services/img/mattermost_add_slash_command.png and b/doc/project_services/img/mattermost_add_slash_command.png differ diff --git a/doc/project_services/img/mattermost_bot_auth.png b/doc/project_services/img/mattermost_bot_auth.png index 19c4735194f..830b7849f3d 100644 Binary files a/doc/project_services/img/mattermost_bot_auth.png and b/doc/project_services/img/mattermost_bot_auth.png differ diff --git a/doc/project_services/img/mattermost_bot_available_commands.png b/doc/project_services/img/mattermost_bot_available_commands.png index f912a639cc5..b51798cf10d 100644 Binary files a/doc/project_services/img/mattermost_bot_available_commands.png and b/doc/project_services/img/mattermost_bot_available_commands.png differ diff --git a/doc/project_services/img/mattermost_config_help.png b/doc/project_services/img/mattermost_config_help.png index 3e38bf0abc6..a62e4b792f9 100644 Binary files a/doc/project_services/img/mattermost_config_help.png and b/doc/project_services/img/mattermost_config_help.png differ diff --git a/doc/project_services/img/mattermost_console_integrations.png b/doc/project_services/img/mattermost_console_integrations.png index eecec0950a8..b3b8c20d7bf 100644 Binary files a/doc/project_services/img/mattermost_console_integrations.png and b/doc/project_services/img/mattermost_console_integrations.png differ diff --git a/doc/project_services/img/mattermost_gitlab_token.png b/doc/project_services/img/mattermost_gitlab_token.png index 3f4f26aab35..257018914d2 100644 Binary files a/doc/project_services/img/mattermost_gitlab_token.png and b/doc/project_services/img/mattermost_gitlab_token.png differ diff --git a/doc/project_services/img/mattermost_goto_console.png b/doc/project_services/img/mattermost_goto_console.png index 3576758b331..3354c2a24b4 100644 Binary files a/doc/project_services/img/mattermost_goto_console.png and b/doc/project_services/img/mattermost_goto_console.png differ diff --git a/doc/project_services/img/mattermost_slash_command_configuration.png b/doc/project_services/img/mattermost_slash_command_configuration.png index 06416b0d068..12766ab2b34 100644 Binary files a/doc/project_services/img/mattermost_slash_command_configuration.png and b/doc/project_services/img/mattermost_slash_command_configuration.png differ diff --git a/doc/project_services/img/mattermost_slash_command_token.png b/doc/project_services/img/mattermost_slash_command_token.png index 320e263026a..c38f37c203c 100644 Binary files a/doc/project_services/img/mattermost_slash_command_token.png and b/doc/project_services/img/mattermost_slash_command_token.png differ diff --git a/doc/project_services/img/mattermost_team_integrations.png b/doc/project_services/img/mattermost_team_integrations.png index 9086cf1c136..69d4a231e5a 100644 Binary files a/doc/project_services/img/mattermost_team_integrations.png and b/doc/project_services/img/mattermost_team_integrations.png differ diff --git a/doc/project_services/img/redmine_configuration.png b/doc/project_services/img/redmine_configuration.png index e9d8c0d2da8..7b6dd271401 100644 Binary files a/doc/project_services/img/redmine_configuration.png and b/doc/project_services/img/redmine_configuration.png differ diff --git a/doc/project_services/img/services_templates_redmine_example.png b/doc/project_services/img/services_templates_redmine_example.png index 77c2b98e5d0..50d20510daf 100644 Binary files a/doc/project_services/img/services_templates_redmine_example.png and b/doc/project_services/img/services_templates_redmine_example.png differ diff --git a/doc/project_services/img/slack_configuration.png b/doc/project_services/img/slack_configuration.png index b8de8a56db7..fc8e58e686b 100644 Binary files a/doc/project_services/img/slack_configuration.png and b/doc/project_services/img/slack_configuration.png differ diff --git a/doc/raketasks/backup_hrz.png b/doc/raketasks/backup_hrz.png index 287587609a1..c9595b236ee 100644 Binary files a/doc/raketasks/backup_hrz.png and b/doc/raketasks/backup_hrz.png differ diff --git a/doc/security/img/two_factor_authentication_settings.png b/doc/security/img/two_factor_authentication_settings.png index 6af5feabb13..6d89be1eb04 100644 Binary files a/doc/security/img/two_factor_authentication_settings.png and b/doc/security/img/two_factor_authentication_settings.png differ diff --git a/doc/university/high-availability/aws/img/auto-scaling-det.png b/doc/university/high-availability/aws/img/auto-scaling-det.png index e9b65529495..1e125f301bc 100644 Binary files a/doc/university/high-availability/aws/img/auto-scaling-det.png and b/doc/university/high-availability/aws/img/auto-scaling-det.png differ diff --git a/doc/university/high-availability/aws/img/db-subnet-group.png b/doc/university/high-availability/aws/img/db-subnet-group.png index 0768aa73c45..590a02b8dbe 100644 Binary files a/doc/university/high-availability/aws/img/db-subnet-group.png and b/doc/university/high-availability/aws/img/db-subnet-group.png differ diff --git a/doc/university/high-availability/aws/img/ec-subnet.png b/doc/university/high-availability/aws/img/ec-subnet.png index f41d78b271d..43ef76b62d3 100644 Binary files a/doc/university/high-availability/aws/img/ec-subnet.png and b/doc/university/high-availability/aws/img/ec-subnet.png differ diff --git a/doc/university/high-availability/aws/img/elastic-file-system.png b/doc/university/high-availability/aws/img/elastic-file-system.png index 7de866d1e89..5bcfb8d0588 100644 Binary files a/doc/university/high-availability/aws/img/elastic-file-system.png and b/doc/university/high-availability/aws/img/elastic-file-system.png differ diff --git a/doc/university/high-availability/aws/img/ig-rt.png b/doc/university/high-availability/aws/img/ig-rt.png index 93bb0c2ae02..62cca074a1e 100644 Binary files a/doc/university/high-availability/aws/img/ig-rt.png and b/doc/university/high-availability/aws/img/ig-rt.png differ diff --git a/doc/university/high-availability/aws/img/ig.png b/doc/university/high-availability/aws/img/ig.png index cc50456370f..d4fc2d12de8 100644 Binary files a/doc/university/high-availability/aws/img/ig.png and b/doc/university/high-availability/aws/img/ig.png differ diff --git a/doc/university/high-availability/aws/img/instance_specs.png b/doc/university/high-availability/aws/img/instance_specs.png index ef31dc41dae..650f375ab3c 100644 Binary files a/doc/university/high-availability/aws/img/instance_specs.png and b/doc/university/high-availability/aws/img/instance_specs.png differ diff --git a/doc/university/high-availability/aws/img/new_vpc.png b/doc/university/high-availability/aws/img/new_vpc.png index 4aac6af7c7a..e51c066cee2 100644 Binary files a/doc/university/high-availability/aws/img/new_vpc.png and b/doc/university/high-availability/aws/img/new_vpc.png differ diff --git a/doc/university/high-availability/aws/img/policies.png b/doc/university/high-availability/aws/img/policies.png index 8c58117e4fa..afcd9e4af9b 100644 Binary files a/doc/university/high-availability/aws/img/policies.png and b/doc/university/high-availability/aws/img/policies.png differ diff --git a/doc/university/high-availability/aws/img/rds-net-opt.png b/doc/university/high-availability/aws/img/rds-net-opt.png index bc204de2474..651cc23b1ab 100644 Binary files a/doc/university/high-availability/aws/img/rds-net-opt.png and b/doc/university/high-availability/aws/img/rds-net-opt.png differ diff --git a/doc/university/high-availability/aws/img/rds-sec-group.png b/doc/university/high-availability/aws/img/rds-sec-group.png index 8864dc3e463..c6d1bc350e4 100644 Binary files a/doc/university/high-availability/aws/img/rds-sec-group.png and b/doc/university/high-availability/aws/img/rds-sec-group.png differ diff --git a/doc/university/high-availability/aws/img/redis-cluster-det.png b/doc/university/high-availability/aws/img/redis-cluster-det.png index 9e9a81283c5..51d3a08eab6 100644 Binary files a/doc/university/high-availability/aws/img/redis-cluster-det.png and b/doc/university/high-availability/aws/img/redis-cluster-det.png differ diff --git a/doc/university/high-availability/aws/img/redis-net.png b/doc/university/high-availability/aws/img/redis-net.png index 037bd6d6897..9022a9ada78 100644 Binary files a/doc/university/high-availability/aws/img/redis-net.png and b/doc/university/high-availability/aws/img/redis-net.png differ diff --git a/doc/university/high-availability/aws/img/route_table.png b/doc/university/high-availability/aws/img/route_table.png index 1dea322474d..c8bef75f01a 100644 Binary files a/doc/university/high-availability/aws/img/route_table.png and b/doc/university/high-availability/aws/img/route_table.png differ diff --git a/doc/university/high-availability/aws/img/subnet.png b/doc/university/high-availability/aws/img/subnet.png index dbc71201992..de910edc948 100644 Binary files a/doc/university/high-availability/aws/img/subnet.png and b/doc/university/high-availability/aws/img/subnet.png differ diff --git a/doc/university/training/gitlab_flow/feature_branches.png b/doc/university/training/gitlab_flow/feature_branches.png index 88addb623ee..612e0248222 100644 Binary files a/doc/university/training/gitlab_flow/feature_branches.png and b/doc/university/training/gitlab_flow/feature_branches.png differ diff --git a/doc/university/training/gitlab_flow/production_branch.png b/doc/university/training/gitlab_flow/production_branch.png index 33fb26dd621..66456cc51af 100644 Binary files a/doc/university/training/gitlab_flow/production_branch.png and b/doc/university/training/gitlab_flow/production_branch.png differ diff --git a/doc/university/training/gitlab_flow/release_branches.png b/doc/university/training/gitlab_flow/release_branches.png index da7ae53413a..5661e36c4e2 100644 Binary files a/doc/university/training/gitlab_flow/release_branches.png and b/doc/university/training/gitlab_flow/release_branches.png differ diff --git a/doc/university/training/logo.png b/doc/university/training/logo.png index cc831790405..c80f65c053e 100644 Binary files a/doc/university/training/logo.png and b/doc/university/training/logo.png differ diff --git a/doc/user/admin_area/img/admin_labels.png b/doc/user/admin_area/img/admin_labels.png index 1ee33a534ab..a9ea059ccf9 100644 Binary files a/doc/user/admin_area/img/admin_labels.png and b/doc/user/admin_area/img/admin_labels.png differ diff --git a/doc/user/admin_area/monitoring/img/health_check_token.png b/doc/user/admin_area/monitoring/img/health_check_token.png index 2d7c82a65a8..182549fc484 100644 Binary files a/doc/user/admin_area/monitoring/img/health_check_token.png and b/doc/user/admin_area/monitoring/img/health_check_token.png differ diff --git a/doc/user/admin_area/settings/img/access_restrictions.png b/doc/user/admin_area/settings/img/access_restrictions.png index 8eea84320d7..8c5336c7835 100644 Binary files a/doc/user/admin_area/settings/img/access_restrictions.png and b/doc/user/admin_area/settings/img/access_restrictions.png differ diff --git a/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png b/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png index 53f7e76033e..b7d6671902a 100644 Binary files a/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png and b/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png differ diff --git a/doc/user/admin_area/settings/img/admin_area_settings_button.png b/doc/user/admin_area/settings/img/admin_area_settings_button.png index 509708b627f..1d2c0ac04bc 100644 Binary files a/doc/user/admin_area/settings/img/admin_area_settings_button.png and b/doc/user/admin_area/settings/img/admin_area_settings_button.png differ diff --git a/doc/user/admin_area/settings/img/domain_blacklist.png b/doc/user/admin_area/settings/img/domain_blacklist.png index bd87b73cf9e..dedd3be1e8f 100644 Binary files a/doc/user/admin_area/settings/img/domain_blacklist.png and b/doc/user/admin_area/settings/img/domain_blacklist.png differ diff --git a/doc/user/admin_area/settings/img/restricted_url.png b/doc/user/admin_area/settings/img/restricted_url.png index 8b00a18320b..67abd13f741 100644 Binary files a/doc/user/admin_area/settings/img/restricted_url.png and b/doc/user/admin_area/settings/img/restricted_url.png differ diff --git a/doc/user/img/markdown_logo.png b/doc/user/img/markdown_logo.png index 05c8b0d0ccf..bb3faaaec76 100644 Binary files a/doc/user/img/markdown_logo.png and b/doc/user/img/markdown_logo.png differ diff --git a/doc/user/project/builds/img/build_artifacts_browser.png b/doc/user/project/builds/img/build_artifacts_browser.png index d95e2800c0f..686273948d6 100644 Binary files a/doc/user/project/builds/img/build_artifacts_browser.png and b/doc/user/project/builds/img/build_artifacts_browser.png differ diff --git a/doc/user/project/builds/img/build_artifacts_browser_button.png b/doc/user/project/builds/img/build_artifacts_browser_button.png index 463540634e3..33ef7de0415 100644 Binary files a/doc/user/project/builds/img/build_artifacts_browser_button.png and b/doc/user/project/builds/img/build_artifacts_browser_button.png differ diff --git a/doc/user/project/builds/img/build_artifacts_builds_page.png b/doc/user/project/builds/img/build_artifacts_builds_page.png index db78386ba7b..8f75602d592 100644 Binary files a/doc/user/project/builds/img/build_artifacts_builds_page.png and b/doc/user/project/builds/img/build_artifacts_builds_page.png differ diff --git a/doc/user/project/builds/img/build_artifacts_pipelines_page.png b/doc/user/project/builds/img/build_artifacts_pipelines_page.png index 6c2d1a4bdc7..4bbd00ddaa0 100644 Binary files a/doc/user/project/builds/img/build_artifacts_pipelines_page.png and b/doc/user/project/builds/img/build_artifacts_pipelines_page.png differ diff --git a/doc/user/project/builds/img/build_latest_artifacts_browser.png b/doc/user/project/builds/img/build_latest_artifacts_browser.png index d8e9071958c..c6d8856078b 100644 Binary files a/doc/user/project/builds/img/build_latest_artifacts_browser.png and b/doc/user/project/builds/img/build_latest_artifacts_browser.png differ diff --git a/doc/user/project/img/container_registry_enable.png b/doc/user/project/img/container_registry_enable.png index 6fffa2a91d8..d067a8be1ca 100644 Binary files a/doc/user/project/img/container_registry_enable.png and b/doc/user/project/img/container_registry_enable.png differ diff --git a/doc/user/project/img/container_registry_panel.png b/doc/user/project/img/container_registry_panel.png index 60fd76192b7..e4c9ecbb25b 100644 Binary files a/doc/user/project/img/container_registry_panel.png and b/doc/user/project/img/container_registry_panel.png differ diff --git a/doc/user/project/img/container_registry_tab.png b/doc/user/project/img/container_registry_tab.png index 36b883aaa97..a85237271d9 100644 Binary files a/doc/user/project/img/container_registry_tab.png and b/doc/user/project/img/container_registry_tab.png differ diff --git a/doc/user/project/img/cycle_analytics_landing_page.png b/doc/user/project/img/cycle_analytics_landing_page.png index aab060ad0ed..316612c0da0 100644 Binary files a/doc/user/project/img/cycle_analytics_landing_page.png and b/doc/user/project/img/cycle_analytics_landing_page.png differ diff --git a/doc/user/project/img/description_templates.png b/doc/user/project/img/description_templates.png index c41cc77a94c..e9d45029532 100644 Binary files a/doc/user/project/img/description_templates.png and b/doc/user/project/img/description_templates.png differ diff --git a/doc/user/project/img/issue_board.png b/doc/user/project/img/issue_board.png index 63c269f6dbc..2a35a615d70 100644 Binary files a/doc/user/project/img/issue_board.png and b/doc/user/project/img/issue_board.png differ diff --git a/doc/user/project/img/issue_board_add_list.png b/doc/user/project/img/issue_board_add_list.png index 2b8c10eaa0a..aa1a4ca4cfa 100644 Binary files a/doc/user/project/img/issue_board_add_list.png and b/doc/user/project/img/issue_board_add_list.png differ diff --git a/doc/user/project/img/issue_board_search_backlog.png b/doc/user/project/img/issue_board_search_backlog.png index 112ea171539..fbb67b9c18f 100644 Binary files a/doc/user/project/img/issue_board_search_backlog.png and b/doc/user/project/img/issue_board_search_backlog.png differ diff --git a/doc/user/project/img/issue_board_system_notes.png b/doc/user/project/img/issue_board_system_notes.png index b69ef034954..bd0f5f54095 100644 Binary files a/doc/user/project/img/issue_board_system_notes.png and b/doc/user/project/img/issue_board_system_notes.png differ diff --git a/doc/user/project/img/issue_board_welcome_message.png b/doc/user/project/img/issue_board_welcome_message.png index b757faeb230..aa25cfb5b37 100644 Binary files a/doc/user/project/img/issue_board_welcome_message.png and b/doc/user/project/img/issue_board_welcome_message.png differ diff --git a/doc/user/project/img/koding_build-in-progress.png b/doc/user/project/img/koding_build-in-progress.png index f8cc81834c4..79b7b2f10a2 100644 Binary files a/doc/user/project/img/koding_build-in-progress.png and b/doc/user/project/img/koding_build-in-progress.png differ diff --git a/doc/user/project/img/koding_build-logs.png b/doc/user/project/img/koding_build-logs.png index a04cd5aff99..b30c8375b20 100644 Binary files a/doc/user/project/img/koding_build-logs.png and b/doc/user/project/img/koding_build-logs.png differ diff --git a/doc/user/project/img/koding_build-success.png b/doc/user/project/img/koding_build-success.png index 2a0dd296480..a2342cfd324 100644 Binary files a/doc/user/project/img/koding_build-success.png and b/doc/user/project/img/koding_build-success.png differ diff --git a/doc/user/project/img/koding_commit-koding.yml.png b/doc/user/project/img/koding_commit-koding.yml.png index 3e133c50327..16842410ae2 100644 Binary files a/doc/user/project/img/koding_commit-koding.yml.png and b/doc/user/project/img/koding_commit-koding.yml.png differ diff --git a/doc/user/project/img/koding_different-stack-on-mr-try.png b/doc/user/project/img/koding_different-stack-on-mr-try.png index fd25e32f648..10c7c51d2e6 100644 Binary files a/doc/user/project/img/koding_different-stack-on-mr-try.png and b/doc/user/project/img/koding_different-stack-on-mr-try.png differ diff --git a/doc/user/project/img/koding_edit-on-ide.png b/doc/user/project/img/koding_edit-on-ide.png index fd5aaff75f5..ab861281d3e 100644 Binary files a/doc/user/project/img/koding_edit-on-ide.png and b/doc/user/project/img/koding_edit-on-ide.png differ diff --git a/doc/user/project/img/koding_enable-koding.png b/doc/user/project/img/koding_enable-koding.png index c0ae0ee9918..0b6fcfadcc5 100644 Binary files a/doc/user/project/img/koding_enable-koding.png and b/doc/user/project/img/koding_enable-koding.png differ diff --git a/doc/user/project/img/koding_landing.png b/doc/user/project/img/koding_landing.png index 7c629d9b05e..1eeddcd3813 100644 Binary files a/doc/user/project/img/koding_landing.png and b/doc/user/project/img/koding_landing.png differ diff --git a/doc/user/project/img/koding_open-gitlab-from-koding.png b/doc/user/project/img/koding_open-gitlab-from-koding.png index c958cf8f224..4235a72b36f 100644 Binary files a/doc/user/project/img/koding_open-gitlab-from-koding.png and b/doc/user/project/img/koding_open-gitlab-from-koding.png differ diff --git a/doc/user/project/img/koding_run-in-ide.png b/doc/user/project/img/koding_run-in-ide.png index f91ee0f74cc..d22e5023c59 100644 Binary files a/doc/user/project/img/koding_run-in-ide.png and b/doc/user/project/img/koding_run-in-ide.png differ diff --git a/doc/user/project/img/koding_run-mr-in-ide.png b/doc/user/project/img/koding_run-mr-in-ide.png index 502817a2a46..cb1112c4034 100644 Binary files a/doc/user/project/img/koding_run-mr-in-ide.png and b/doc/user/project/img/koding_run-mr-in-ide.png differ diff --git a/doc/user/project/img/koding_set-up-ide.png b/doc/user/project/img/koding_set-up-ide.png index 7f408c980b5..033d41729a2 100644 Binary files a/doc/user/project/img/koding_set-up-ide.png and b/doc/user/project/img/koding_set-up-ide.png differ diff --git a/doc/user/project/img/koding_stack-import.png b/doc/user/project/img/koding_stack-import.png index 2a4e3c87fc8..245ccb07ba3 100644 Binary files a/doc/user/project/img/koding_stack-import.png and b/doc/user/project/img/koding_stack-import.png differ diff --git a/doc/user/project/img/koding_start-build.png b/doc/user/project/img/koding_start-build.png index 52159440f62..3f5c16d5d2f 100644 Binary files a/doc/user/project/img/koding_start-build.png and b/doc/user/project/img/koding_start-build.png differ diff --git a/doc/user/project/img/labels_assign_label_in_new_issue.png b/doc/user/project/img/labels_assign_label_in_new_issue.png index e32a35f7cda..badfbed0bbe 100644 Binary files a/doc/user/project/img/labels_assign_label_in_new_issue.png and b/doc/user/project/img/labels_assign_label_in_new_issue.png differ diff --git a/doc/user/project/img/labels_assign_label_sidebar.png b/doc/user/project/img/labels_assign_label_sidebar.png index 799443af889..d74796fdb4d 100644 Binary files a/doc/user/project/img/labels_assign_label_sidebar.png and b/doc/user/project/img/labels_assign_label_sidebar.png differ diff --git a/doc/user/project/img/labels_assign_label_sidebar_saved.png b/doc/user/project/img/labels_assign_label_sidebar_saved.png index e7d8d69e60e..dabffe956dc 100644 Binary files a/doc/user/project/img/labels_assign_label_sidebar_saved.png and b/doc/user/project/img/labels_assign_label_sidebar_saved.png differ diff --git a/doc/user/project/img/labels_default.png b/doc/user/project/img/labels_default.png index ee0c9f889ad..474953d565b 100644 Binary files a/doc/user/project/img/labels_default.png and b/doc/user/project/img/labels_default.png differ diff --git a/doc/user/project/img/labels_description_tooltip.png b/doc/user/project/img/labels_description_tooltip.png index 0d1e3e091fb..eea4f8cf0f4 100644 Binary files a/doc/user/project/img/labels_description_tooltip.png and b/doc/user/project/img/labels_description_tooltip.png differ diff --git a/doc/user/project/img/labels_filter.png b/doc/user/project/img/labels_filter.png index ed622be2d93..3aca77f0070 100644 Binary files a/doc/user/project/img/labels_filter.png and b/doc/user/project/img/labels_filter.png differ diff --git a/doc/user/project/img/labels_filter_by_priority.png b/doc/user/project/img/labels_filter_by_priority.png index c5a9e20919b..5609a1f6d7f 100644 Binary files a/doc/user/project/img/labels_filter_by_priority.png and b/doc/user/project/img/labels_filter_by_priority.png differ diff --git a/doc/user/project/img/labels_generate.png b/doc/user/project/img/labels_generate.png index 9579be4e231..987f4b5be71 100644 Binary files a/doc/user/project/img/labels_generate.png and b/doc/user/project/img/labels_generate.png differ diff --git a/doc/user/project/img/labels_new_label.png b/doc/user/project/img/labels_new_label.png index a916d3dceb5..b44b4bd296d 100644 Binary files a/doc/user/project/img/labels_new_label.png and b/doc/user/project/img/labels_new_label.png differ diff --git a/doc/user/project/img/labels_new_label_on_the_fly.png b/doc/user/project/img/labels_new_label_on_the_fly.png index 80cc434239e..2ac9805b1ab 100644 Binary files a/doc/user/project/img/labels_new_label_on_the_fly.png and b/doc/user/project/img/labels_new_label_on_the_fly.png differ diff --git a/doc/user/project/img/labels_new_label_on_the_fly_create.png b/doc/user/project/img/labels_new_label_on_the_fly_create.png index c41090945eb..02ccf68553b 100644 Binary files a/doc/user/project/img/labels_new_label_on_the_fly_create.png and b/doc/user/project/img/labels_new_label_on_the_fly_create.png differ diff --git a/doc/user/project/img/labels_prioritize.png b/doc/user/project/img/labels_prioritize.png index 8dfe72cf826..3e888f36364 100644 Binary files a/doc/user/project/img/labels_prioritize.png and b/doc/user/project/img/labels_prioritize.png differ diff --git a/doc/user/project/img/labels_subscribe.png b/doc/user/project/img/labels_subscribe.png index ea3db2bc0cf..56f24ae7bc8 100644 Binary files a/doc/user/project/img/labels_subscribe.png and b/doc/user/project/img/labels_subscribe.png differ diff --git a/doc/user/project/img/mitmproxy-docker.png b/doc/user/project/img/mitmproxy-docker.png index 4e3e37b413d..aa3b6a0b830 100644 Binary files a/doc/user/project/img/mitmproxy-docker.png and b/doc/user/project/img/mitmproxy-docker.png differ diff --git a/doc/user/project/img/project_settings_list.png b/doc/user/project/img/project_settings_list.png index cd9f5c00eea..0bb761b45c9 100644 Binary files a/doc/user/project/img/project_settings_list.png and b/doc/user/project/img/project_settings_list.png differ diff --git a/doc/user/project/img/protected_branches_choose_branch.png b/doc/user/project/img/protected_branches_choose_branch.png index 26328143717..c2848db9c96 100644 Binary files a/doc/user/project/img/protected_branches_choose_branch.png and b/doc/user/project/img/protected_branches_choose_branch.png differ diff --git a/doc/user/project/img/protected_branches_devs_can_push.png b/doc/user/project/img/protected_branches_devs_can_push.png index 812cc8767b7..1c05cb8fd36 100644 Binary files a/doc/user/project/img/protected_branches_devs_can_push.png and b/doc/user/project/img/protected_branches_devs_can_push.png differ diff --git a/doc/user/project/img/protected_branches_error_ui.png b/doc/user/project/img/protected_branches_error_ui.png index cc61df7ca97..3f8e462d3ad 100644 Binary files a/doc/user/project/img/protected_branches_error_ui.png and b/doc/user/project/img/protected_branches_error_ui.png differ diff --git a/doc/user/project/img/protected_branches_list.png b/doc/user/project/img/protected_branches_list.png index f33f1b2bdb6..1b2936cb711 100644 Binary files a/doc/user/project/img/protected_branches_list.png and b/doc/user/project/img/protected_branches_list.png differ diff --git a/doc/user/project/img/protected_branches_matches.png b/doc/user/project/img/protected_branches_matches.png index 30ce53f704e..d7f2c8582fc 100644 Binary files a/doc/user/project/img/protected_branches_matches.png and b/doc/user/project/img/protected_branches_matches.png differ diff --git a/doc/user/project/img/protected_branches_page.png b/doc/user/project/img/protected_branches_page.png index 1585dde5b29..4e5afff3bae 100644 Binary files a/doc/user/project/img/protected_branches_page.png and b/doc/user/project/img/protected_branches_page.png differ diff --git a/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png b/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png index 7fb68cc9e9b..5ab094ab367 100644 Binary files a/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png and b/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png differ diff --git a/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png b/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png index 5267e04562f..42dcb9203ec 100644 Binary files a/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png and b/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png differ diff --git a/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png b/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png index 975fb13e463..71227747182 100644 Binary files a/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png and b/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png differ diff --git a/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png b/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png index 6c003bacbe3..604eb22f51c 100644 Binary files a/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png and b/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png differ diff --git a/doc/user/project/merge_requests/img/commit_compare.png b/doc/user/project/merge_requests/img/commit_compare.png index 0e4a2b23c04..e612a39716e 100644 Binary files a/doc/user/project/merge_requests/img/commit_compare.png and b/doc/user/project/merge_requests/img/commit_compare.png differ diff --git a/doc/user/project/merge_requests/img/conflict_section.png b/doc/user/project/merge_requests/img/conflict_section.png index 842e50b14b2..cfc17013218 100644 Binary files a/doc/user/project/merge_requests/img/conflict_section.png and b/doc/user/project/merge_requests/img/conflict_section.png differ diff --git a/doc/user/project/merge_requests/img/discussion_view.png b/doc/user/project/merge_requests/img/discussion_view.png index 83bb60acce2..2ee1db2eab3 100644 Binary files a/doc/user/project/merge_requests/img/discussion_view.png and b/doc/user/project/merge_requests/img/discussion_view.png differ diff --git a/doc/user/project/merge_requests/img/discussions_resolved.png b/doc/user/project/merge_requests/img/discussions_resolved.png index 85428129ac8..3fd496f6da5 100644 Binary files a/doc/user/project/merge_requests/img/discussions_resolved.png and b/doc/user/project/merge_requests/img/discussions_resolved.png differ diff --git a/doc/user/project/merge_requests/img/merge_request_diff.png b/doc/user/project/merge_requests/img/merge_request_diff.png index 06ee4908edc..9c5488cb207 100644 Binary files a/doc/user/project/merge_requests/img/merge_request_diff.png and b/doc/user/project/merge_requests/img/merge_request_diff.png differ diff --git a/doc/user/project/merge_requests/img/merge_request_widget.png b/doc/user/project/merge_requests/img/merge_request_widget.png index ffb96b17b07..43a945c74d9 100644 Binary files a/doc/user/project/merge_requests/img/merge_request_widget.png and b/doc/user/project/merge_requests/img/merge_request_widget.png differ diff --git a/doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png index b86e6d7b3fd..f50a1be24f2 100644 Binary files a/doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png and b/doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png differ diff --git a/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png index 6b9756b7418..c43f76b058c 100644 Binary files a/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png and b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png differ diff --git a/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png index 18bebf5fe92..ddc58ff2630 100644 Binary files a/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png and b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png differ diff --git a/doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png index f3ea61d8147..a98636ee359 100644 Binary files a/doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png and b/doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png differ diff --git a/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved.png b/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved.png index 52c8acf15e0..928c7d33898 100644 Binary files a/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved.png and b/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved.png differ diff --git a/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved_msg.png b/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved_msg.png index 79ba5c362c7..bcdc0250d7c 100644 Binary files a/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved_msg.png and b/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved_msg.png differ diff --git a/doc/user/project/merge_requests/img/resolve_comment_button.png b/doc/user/project/merge_requests/img/resolve_comment_button.png index 2c4ab2f5d53..70340108874 100644 Binary files a/doc/user/project/merge_requests/img/resolve_comment_button.png and b/doc/user/project/merge_requests/img/resolve_comment_button.png differ diff --git a/doc/user/project/merge_requests/img/resolve_discussion_button.png b/doc/user/project/merge_requests/img/resolve_discussion_button.png index 73f265bb101..ab454f661e0 100644 Binary files a/doc/user/project/merge_requests/img/resolve_discussion_button.png and b/doc/user/project/merge_requests/img/resolve_discussion_button.png differ diff --git a/doc/user/project/merge_requests/img/revert_changes_commit.png b/doc/user/project/merge_requests/img/revert_changes_commit.png index e7194fc3504..a0663e130e9 100644 Binary files a/doc/user/project/merge_requests/img/revert_changes_commit.png and b/doc/user/project/merge_requests/img/revert_changes_commit.png differ diff --git a/doc/user/project/merge_requests/img/revert_changes_commit_modal.png b/doc/user/project/merge_requests/img/revert_changes_commit_modal.png index c660ec7eaec..ef7b6dae553 100644 Binary files a/doc/user/project/merge_requests/img/revert_changes_commit_modal.png and b/doc/user/project/merge_requests/img/revert_changes_commit_modal.png differ diff --git a/doc/user/project/merge_requests/img/revert_changes_mr.png b/doc/user/project/merge_requests/img/revert_changes_mr.png index 3002f0ac1c5..8792018ee53 100644 Binary files a/doc/user/project/merge_requests/img/revert_changes_mr.png and b/doc/user/project/merge_requests/img/revert_changes_mr.png differ diff --git a/doc/user/project/merge_requests/img/revert_changes_mr_modal.png b/doc/user/project/merge_requests/img/revert_changes_mr_modal.png index c6aaeecc8a6..f6540c9dd33 100644 Binary files a/doc/user/project/merge_requests/img/revert_changes_mr_modal.png and b/doc/user/project/merge_requests/img/revert_changes_mr_modal.png differ diff --git a/doc/user/project/merge_requests/img/versions.png b/doc/user/project/merge_requests/img/versions.png index 6c86f2c68ac..33c58d2abff 100644 Binary files a/doc/user/project/merge_requests/img/versions.png and b/doc/user/project/merge_requests/img/versions.png differ diff --git a/doc/user/project/merge_requests/img/versions_compare.png b/doc/user/project/merge_requests/img/versions_compare.png index 890cae7768c..db978ea7b1d 100644 Binary files a/doc/user/project/merge_requests/img/versions_compare.png and b/doc/user/project/merge_requests/img/versions_compare.png differ diff --git a/doc/user/project/merge_requests/img/versions_dropdown.png b/doc/user/project/merge_requests/img/versions_dropdown.png index 9bab9304e14..889a2d93e6c 100644 Binary files a/doc/user/project/merge_requests/img/versions_dropdown.png and b/doc/user/project/merge_requests/img/versions_dropdown.png differ diff --git a/doc/user/project/merge_requests/img/versions_system_note.png b/doc/user/project/merge_requests/img/versions_system_note.png index 7c9d7715745..90be6298d15 100644 Binary files a/doc/user/project/merge_requests/img/versions_system_note.png and b/doc/user/project/merge_requests/img/versions_system_note.png differ diff --git a/doc/user/project/merge_requests/img/wip_blocked_accept_button.png b/doc/user/project/merge_requests/img/wip_blocked_accept_button.png index 89c458aa8d9..047b0b4620f 100644 Binary files a/doc/user/project/merge_requests/img/wip_blocked_accept_button.png and b/doc/user/project/merge_requests/img/wip_blocked_accept_button.png differ diff --git a/doc/user/project/merge_requests/img/wip_mark_as_wip.png b/doc/user/project/merge_requests/img/wip_mark_as_wip.png index 9c37354a653..8bd206bc24a 100644 Binary files a/doc/user/project/merge_requests/img/wip_mark_as_wip.png and b/doc/user/project/merge_requests/img/wip_mark_as_wip.png differ diff --git a/doc/user/project/merge_requests/img/wip_unmark_as_wip.png b/doc/user/project/merge_requests/img/wip_unmark_as_wip.png index 31f7326beb0..c0bfa6a35a2 100644 Binary files a/doc/user/project/merge_requests/img/wip_unmark_as_wip.png and b/doc/user/project/merge_requests/img/wip_unmark_as_wip.png differ diff --git a/doc/user/project/pipelines/img/pipelines_settings_badges.png b/doc/user/project/pipelines/img/pipelines_settings_badges.png index d0c4640791d..3bdc6374c15 100644 Binary files a/doc/user/project/pipelines/img/pipelines_settings_badges.png and b/doc/user/project/pipelines/img/pipelines_settings_badges.png differ diff --git a/doc/user/project/pipelines/img/pipelines_settings_test_coverage.png b/doc/user/project/pipelines/img/pipelines_settings_test_coverage.png index d2a5568521f..2a99201e014 100644 Binary files a/doc/user/project/pipelines/img/pipelines_settings_test_coverage.png and b/doc/user/project/pipelines/img/pipelines_settings_test_coverage.png differ diff --git a/doc/user/project/pipelines/img/pipelines_test_coverage_build.png b/doc/user/project/pipelines/img/pipelines_test_coverage_build.png index 3823100daf2..7eaba1a256f 100644 Binary files a/doc/user/project/pipelines/img/pipelines_test_coverage_build.png and b/doc/user/project/pipelines/img/pipelines_test_coverage_build.png differ diff --git a/doc/user/project/pipelines/img/pipelines_test_coverage_mr_widget.png b/doc/user/project/pipelines/img/pipelines_test_coverage_mr_widget.png index c4f78803e69..c166bb8bec8 100644 Binary files a/doc/user/project/pipelines/img/pipelines_test_coverage_mr_widget.png and b/doc/user/project/pipelines/img/pipelines_test_coverage_mr_widget.png differ diff --git a/doc/user/project/repository/img/web_editor_new_branch_dropdown.png b/doc/user/project/repository/img/web_editor_new_branch_dropdown.png index a8e635d2faf..31edb6bde3a 100644 Binary files a/doc/user/project/repository/img/web_editor_new_branch_dropdown.png and b/doc/user/project/repository/img/web_editor_new_branch_dropdown.png differ diff --git a/doc/user/project/repository/img/web_editor_new_branch_from_issue.png b/doc/user/project/repository/img/web_editor_new_branch_from_issue.png index b0a63ddf0ab..4729f5383c0 100644 Binary files a/doc/user/project/repository/img/web_editor_new_branch_from_issue.png and b/doc/user/project/repository/img/web_editor_new_branch_from_issue.png differ diff --git a/doc/user/project/repository/img/web_editor_new_branch_page.png b/doc/user/project/repository/img/web_editor_new_branch_page.png index 7f36b7faf63..8d82f981527 100644 Binary files a/doc/user/project/repository/img/web_editor_new_branch_page.png and b/doc/user/project/repository/img/web_editor_new_branch_page.png differ diff --git a/doc/user/project/repository/img/web_editor_new_directory_dialog.png b/doc/user/project/repository/img/web_editor_new_directory_dialog.png index d16e3c67116..1c9beff8849 100644 Binary files a/doc/user/project/repository/img/web_editor_new_directory_dialog.png and b/doc/user/project/repository/img/web_editor_new_directory_dialog.png differ diff --git a/doc/user/project/repository/img/web_editor_new_directory_dropdown.png b/doc/user/project/repository/img/web_editor_new_directory_dropdown.png index c8d77b16ee8..ede691f6f74 100644 Binary files a/doc/user/project/repository/img/web_editor_new_directory_dropdown.png and b/doc/user/project/repository/img/web_editor_new_directory_dropdown.png differ diff --git a/doc/user/project/repository/img/web_editor_new_file_dropdown.png b/doc/user/project/repository/img/web_editor_new_file_dropdown.png index 3fcb91c9b93..13a4d721039 100644 Binary files a/doc/user/project/repository/img/web_editor_new_file_dropdown.png and b/doc/user/project/repository/img/web_editor_new_file_dropdown.png differ diff --git a/doc/user/project/repository/img/web_editor_new_file_editor.png b/doc/user/project/repository/img/web_editor_new_file_editor.png index 21c340b9288..d0bcc69bf63 100644 Binary files a/doc/user/project/repository/img/web_editor_new_file_editor.png and b/doc/user/project/repository/img/web_editor_new_file_editor.png differ diff --git a/doc/user/project/repository/img/web_editor_new_push_widget.png b/doc/user/project/repository/img/web_editor_new_push_widget.png index c7738a4c930..77756876d4f 100644 Binary files a/doc/user/project/repository/img/web_editor_new_push_widget.png and b/doc/user/project/repository/img/web_editor_new_push_widget.png differ diff --git a/doc/user/project/repository/img/web_editor_new_tag_dropdown.png b/doc/user/project/repository/img/web_editor_new_tag_dropdown.png index ac7415009b3..b52d5cabdf2 100644 Binary files a/doc/user/project/repository/img/web_editor_new_tag_dropdown.png and b/doc/user/project/repository/img/web_editor_new_tag_dropdown.png differ diff --git a/doc/user/project/repository/img/web_editor_new_tag_page.png b/doc/user/project/repository/img/web_editor_new_tag_page.png index 231e1a13fc0..d6d9945397c 100644 Binary files a/doc/user/project/repository/img/web_editor_new_tag_page.png and b/doc/user/project/repository/img/web_editor_new_tag_page.png differ diff --git a/doc/user/project/repository/img/web_editor_start_new_merge_request.png b/doc/user/project/repository/img/web_editor_start_new_merge_request.png index 2755501dfd1..384e8320f15 100644 Binary files a/doc/user/project/repository/img/web_editor_start_new_merge_request.png and b/doc/user/project/repository/img/web_editor_start_new_merge_request.png differ diff --git a/doc/user/project/repository/img/web_editor_template_dropdown_buttons.png b/doc/user/project/repository/img/web_editor_template_dropdown_buttons.png index 4efc51cc423..f21183125f6 100644 Binary files a/doc/user/project/repository/img/web_editor_template_dropdown_buttons.png and b/doc/user/project/repository/img/web_editor_template_dropdown_buttons.png differ diff --git a/doc/user/project/repository/img/web_editor_template_dropdown_first_file.png b/doc/user/project/repository/img/web_editor_template_dropdown_first_file.png index 67190c58823..7f31c2a8887 100644 Binary files a/doc/user/project/repository/img/web_editor_template_dropdown_first_file.png and b/doc/user/project/repository/img/web_editor_template_dropdown_first_file.png differ diff --git a/doc/user/project/repository/img/web_editor_template_dropdown_mit_license.png b/doc/user/project/repository/img/web_editor_template_dropdown_mit_license.png index 47719113805..afd44d78959 100644 Binary files a/doc/user/project/repository/img/web_editor_template_dropdown_mit_license.png and b/doc/user/project/repository/img/web_editor_template_dropdown_mit_license.png differ diff --git a/doc/user/project/repository/img/web_editor_upload_file_dialog.png b/doc/user/project/repository/img/web_editor_upload_file_dialog.png index 9d6d8250bbe..04e951406ad 100644 Binary files a/doc/user/project/repository/img/web_editor_upload_file_dialog.png and b/doc/user/project/repository/img/web_editor_upload_file_dialog.png differ diff --git a/doc/user/project/repository/img/web_editor_upload_file_dropdown.png b/doc/user/project/repository/img/web_editor_upload_file_dropdown.png index 6b5205b05ec..b8c766d4b99 100644 Binary files a/doc/user/project/repository/img/web_editor_upload_file_dropdown.png and b/doc/user/project/repository/img/web_editor_upload_file_dropdown.png differ diff --git a/doc/user/project/settings/img/import_export_download_export.png b/doc/user/project/settings/img/import_export_download_export.png index a2f7f0085c1..4945590e3e8 100644 Binary files a/doc/user/project/settings/img/import_export_download_export.png and b/doc/user/project/settings/img/import_export_download_export.png differ diff --git a/doc/user/project/settings/img/import_export_export_button.png b/doc/user/project/settings/img/import_export_export_button.png index 1f7bdd21b0d..eef79821f8b 100644 Binary files a/doc/user/project/settings/img/import_export_export_button.png and b/doc/user/project/settings/img/import_export_export_button.png differ diff --git a/doc/user/project/settings/img/import_export_mail_link.png b/doc/user/project/settings/img/import_export_mail_link.png index c123f83eb8e..48ef42855bc 100644 Binary files a/doc/user/project/settings/img/import_export_mail_link.png and b/doc/user/project/settings/img/import_export_mail_link.png differ diff --git a/doc/user/project/settings/img/import_export_new_project.png b/doc/user/project/settings/img/import_export_new_project.png index b3a7f201018..9dd509dc4a0 100644 Binary files a/doc/user/project/settings/img/import_export_new_project.png and b/doc/user/project/settings/img/import_export_new_project.png differ diff --git a/doc/user/project/settings/img/import_export_select_file.png b/doc/user/project/settings/img/import_export_select_file.png index f31832af3e1..fb831dca32b 100644 Binary files a/doc/user/project/settings/img/import_export_select_file.png and b/doc/user/project/settings/img/import_export_select_file.png differ diff --git a/doc/user/project/settings/img/settings_edit_button.png b/doc/user/project/settings/img/settings_edit_button.png index 3c0cee536de..9f3a8330e3a 100644 Binary files a/doc/user/project/settings/img/settings_edit_button.png and b/doc/user/project/settings/img/settings_edit_button.png differ diff --git a/doc/web_hooks/ssl.png b/doc/web_hooks/ssl.png index 8c4f08d1825..a552888ed96 100644 Binary files a/doc/web_hooks/ssl.png and b/doc/web_hooks/ssl.png differ diff --git a/doc/workflow/add-user/img/access_requests_management.png b/doc/workflow/add-user/img/access_requests_management.png index 5c9b510ba9d..3693bed869b 100644 Binary files a/doc/workflow/add-user/img/access_requests_management.png and b/doc/workflow/add-user/img/access_requests_management.png differ diff --git a/doc/workflow/add-user/img/add_new_user_to_project_settings.png b/doc/workflow/add-user/img/add_new_user_to_project_settings.png index 5da0552f9d6..40db600455f 100644 Binary files a/doc/workflow/add-user/img/add_new_user_to_project_settings.png and b/doc/workflow/add-user/img/add_new_user_to_project_settings.png differ diff --git a/doc/workflow/add-user/img/add_user_email_accept.png b/doc/workflow/add-user/img/add_user_email_accept.png index a2954ad7c37..763b3ff463d 100644 Binary files a/doc/workflow/add-user/img/add_user_email_accept.png and b/doc/workflow/add-user/img/add_user_email_accept.png differ diff --git a/doc/workflow/add-user/img/add_user_email_ready.png b/doc/workflow/add-user/img/add_user_email_ready.png index 19d91bc0999..0066eb3427b 100644 Binary files a/doc/workflow/add-user/img/add_user_email_ready.png and b/doc/workflow/add-user/img/add_user_email_ready.png differ diff --git a/doc/workflow/add-user/img/add_user_email_search.png b/doc/workflow/add-user/img/add_user_email_search.png index cb31b77d941..66bcd6aad80 100644 Binary files a/doc/workflow/add-user/img/add_user_email_search.png and b/doc/workflow/add-user/img/add_user_email_search.png differ diff --git a/doc/workflow/add-user/img/add_user_give_permissions.png b/doc/workflow/add-user/img/add_user_give_permissions.png index e6b77022f06..376a3eefccc 100644 Binary files a/doc/workflow/add-user/img/add_user_give_permissions.png and b/doc/workflow/add-user/img/add_user_give_permissions.png differ diff --git a/doc/workflow/add-user/img/add_user_import_members_from_another_project.png b/doc/workflow/add-user/img/add_user_import_members_from_another_project.png index 1068589c5ff..0c32001098e 100644 Binary files a/doc/workflow/add-user/img/add_user_import_members_from_another_project.png and b/doc/workflow/add-user/img/add_user_import_members_from_another_project.png differ diff --git a/doc/workflow/add-user/img/add_user_imported_members.png b/doc/workflow/add-user/img/add_user_imported_members.png index 5cd120a4245..51fd7688890 100644 Binary files a/doc/workflow/add-user/img/add_user_imported_members.png and b/doc/workflow/add-user/img/add_user_imported_members.png differ diff --git a/doc/workflow/add-user/img/add_user_list_members.png b/doc/workflow/add-user/img/add_user_list_members.png index 5fe3482192e..e0fa404288d 100644 Binary files a/doc/workflow/add-user/img/add_user_list_members.png and b/doc/workflow/add-user/img/add_user_list_members.png differ diff --git a/doc/workflow/add-user/img/add_user_members_menu.png b/doc/workflow/add-user/img/add_user_members_menu.png index 340d15c9830..8e61d15fe65 100644 Binary files a/doc/workflow/add-user/img/add_user_members_menu.png and b/doc/workflow/add-user/img/add_user_members_menu.png differ diff --git a/doc/workflow/add-user/img/add_user_search_people.png b/doc/workflow/add-user/img/add_user_search_people.png index 1c05d70ca31..41767a9167c 100644 Binary files a/doc/workflow/add-user/img/add_user_search_people.png and b/doc/workflow/add-user/img/add_user_search_people.png differ diff --git a/doc/workflow/add-user/img/request_access_button.png b/doc/workflow/add-user/img/request_access_button.png index 984d640b0f0..608baccb0ca 100644 Binary files a/doc/workflow/add-user/img/request_access_button.png and b/doc/workflow/add-user/img/request_access_button.png differ diff --git a/doc/workflow/add-user/img/withdraw_access_request_button.png b/doc/workflow/add-user/img/withdraw_access_request_button.png index ff54a0e4384..6edd786b151 100644 Binary files a/doc/workflow/add-user/img/withdraw_access_request_button.png and b/doc/workflow/add-user/img/withdraw_access_request_button.png differ diff --git a/doc/workflow/award_emoji.png b/doc/workflow/award_emoji.png index 481680af80c..1ad634a343e 100644 Binary files a/doc/workflow/award_emoji.png and b/doc/workflow/award_emoji.png differ diff --git a/doc/workflow/ci_mr.png b/doc/workflow/ci_mr.png index f8a7708643e..77423c68190 100644 Binary files a/doc/workflow/ci_mr.png and b/doc/workflow/ci_mr.png differ diff --git a/doc/workflow/close_issue_mr.png b/doc/workflow/close_issue_mr.png index 5e520240233..70de2fb6cee 100644 Binary files a/doc/workflow/close_issue_mr.png and b/doc/workflow/close_issue_mr.png differ diff --git a/doc/workflow/environment_branches.png b/doc/workflow/environment_branches.png index 13fb0478eaa..0941a4cad9c 100644 Binary files a/doc/workflow/environment_branches.png and b/doc/workflow/environment_branches.png differ diff --git a/doc/workflow/forking/branch_select.png b/doc/workflow/forking/branch_select.png index 7f19414f3a9..3e82afca75b 100644 Binary files a/doc/workflow/forking/branch_select.png and b/doc/workflow/forking/branch_select.png differ diff --git a/doc/workflow/forking/merge_request.png b/doc/workflow/forking/merge_request.png index e2da42a2be7..294775e1fdd 100644 Binary files a/doc/workflow/forking/merge_request.png and b/doc/workflow/forking/merge_request.png differ diff --git a/doc/workflow/four_stages.png b/doc/workflow/four_stages.png index 49413087dca..3ef6a33d2d4 100644 Binary files a/doc/workflow/four_stages.png and b/doc/workflow/four_stages.png differ diff --git a/doc/workflow/git_pull.png b/doc/workflow/git_pull.png index 9a1fdf899bf..2dd06b56c56 100644 Binary files a/doc/workflow/git_pull.png and b/doc/workflow/git_pull.png differ diff --git a/doc/workflow/gitdashflow.png b/doc/workflow/gitdashflow.png index e456cf9309d..65900853d84 100644 Binary files a/doc/workflow/gitdashflow.png and b/doc/workflow/gitdashflow.png differ diff --git a/doc/workflow/github_flow.png b/doc/workflow/github_flow.png index b3fca97cc2d..21a22becdb6 100644 Binary files a/doc/workflow/github_flow.png and b/doc/workflow/github_flow.png differ diff --git a/doc/workflow/gitlab_flow.png b/doc/workflow/gitlab_flow.png index d85d4ff374e..c3562cc69a8 100644 Binary files a/doc/workflow/gitlab_flow.png and b/doc/workflow/gitlab_flow.png differ diff --git a/doc/workflow/good_commit.png b/doc/workflow/good_commit.png index 7958feea4d9..c3664aa97f2 100644 Binary files a/doc/workflow/good_commit.png and b/doc/workflow/good_commit.png differ diff --git a/doc/workflow/groups/access_requests_management.png b/doc/workflow/groups/access_requests_management.png index 5202434f00f..36deaa89a70 100644 Binary files a/doc/workflow/groups/access_requests_management.png and b/doc/workflow/groups/access_requests_management.png differ diff --git a/doc/workflow/groups/add_member_to_group.png b/doc/workflow/groups/add_member_to_group.png index 6e3f660d2e4..a10d5032bb0 100644 Binary files a/doc/workflow/groups/add_member_to_group.png and b/doc/workflow/groups/add_member_to_group.png differ diff --git a/doc/workflow/groups/group_dashboard.png b/doc/workflow/groups/group_dashboard.png index 662c932e536..a5829f25808 100644 Binary files a/doc/workflow/groups/group_dashboard.png and b/doc/workflow/groups/group_dashboard.png differ diff --git a/doc/workflow/groups/group_with_two_projects.png b/doc/workflow/groups/group_with_two_projects.png index dc3475949f5..76d0a1b8ab2 100644 Binary files a/doc/workflow/groups/group_with_two_projects.png and b/doc/workflow/groups/group_with_two_projects.png differ diff --git a/doc/workflow/groups/max_access_level.png b/doc/workflow/groups/max_access_level.png index 2855a514013..63f33f9d91d 100644 Binary files a/doc/workflow/groups/max_access_level.png and b/doc/workflow/groups/max_access_level.png differ diff --git a/doc/workflow/groups/new_group_button.png b/doc/workflow/groups/new_group_button.png index 26136312c8f..7155d6280bd 100644 Binary files a/doc/workflow/groups/new_group_button.png and b/doc/workflow/groups/new_group_button.png differ diff --git a/doc/workflow/groups/new_group_form.png b/doc/workflow/groups/new_group_form.png index dc50a069ef2..0d798cd4b84 100644 Binary files a/doc/workflow/groups/new_group_form.png and b/doc/workflow/groups/new_group_form.png differ diff --git a/doc/workflow/groups/other_group_sees_shared_project.png b/doc/workflow/groups/other_group_sees_shared_project.png index 2230720cecd..67af27043eb 100644 Binary files a/doc/workflow/groups/other_group_sees_shared_project.png and b/doc/workflow/groups/other_group_sees_shared_project.png differ diff --git a/doc/workflow/groups/override_access_level.png b/doc/workflow/groups/override_access_level.png index 9d6aaf4c363..2b3e9a49842 100644 Binary files a/doc/workflow/groups/override_access_level.png and b/doc/workflow/groups/override_access_level.png differ diff --git a/doc/workflow/groups/project_members_via_group.png b/doc/workflow/groups/project_members_via_group.png index 58270936a0b..878c9a03ac9 100644 Binary files a/doc/workflow/groups/project_members_via_group.png and b/doc/workflow/groups/project_members_via_group.png differ diff --git a/doc/workflow/groups/request_access_button.png b/doc/workflow/groups/request_access_button.png index 0eec5cb937d..f1aae6afed7 100644 Binary files a/doc/workflow/groups/request_access_button.png and b/doc/workflow/groups/request_access_button.png differ diff --git a/doc/workflow/groups/share_project_with_groups.png b/doc/workflow/groups/share_project_with_groups.png index 5772d4deced..3cb4796f9f7 100644 Binary files a/doc/workflow/groups/share_project_with_groups.png and b/doc/workflow/groups/share_project_with_groups.png differ diff --git a/doc/workflow/groups/transfer_project.png b/doc/workflow/groups/transfer_project.png index 0aef3ab3f0f..52161817f11 100644 Binary files a/doc/workflow/groups/transfer_project.png and b/doc/workflow/groups/transfer_project.png differ diff --git a/doc/workflow/groups/withdraw_access_request_button.png b/doc/workflow/groups/withdraw_access_request_button.png index b7de830a780..c5d8ef6c04f 100644 Binary files a/doc/workflow/groups/withdraw_access_request_button.png and b/doc/workflow/groups/withdraw_access_request_button.png differ diff --git a/doc/workflow/img/award_emoji_comment_awarded.png b/doc/workflow/img/award_emoji_comment_awarded.png index 67697831869..111793ebf8a 100644 Binary files a/doc/workflow/img/award_emoji_comment_awarded.png and b/doc/workflow/img/award_emoji_comment_awarded.png differ diff --git a/doc/workflow/img/award_emoji_comment_picker.png b/doc/workflow/img/award_emoji_comment_picker.png index d9c3faecdca..3ad1bab3119 100644 Binary files a/doc/workflow/img/award_emoji_comment_picker.png and b/doc/workflow/img/award_emoji_comment_picker.png differ diff --git a/doc/workflow/img/award_emoji_select.png b/doc/workflow/img/award_emoji_select.png index ad664c0aeff..e1b37beaf62 100644 Binary files a/doc/workflow/img/award_emoji_select.png and b/doc/workflow/img/award_emoji_select.png differ diff --git a/doc/workflow/img/award_emoji_votes_least_popular.png b/doc/workflow/img/award_emoji_votes_least_popular.png index 57d595d9602..86ede4b0c10 100644 Binary files a/doc/workflow/img/award_emoji_votes_least_popular.png and b/doc/workflow/img/award_emoji_votes_least_popular.png differ diff --git a/doc/workflow/img/award_emoji_votes_most_popular.png b/doc/workflow/img/award_emoji_votes_most_popular.png index 432bd09b8a7..1d3e2e57aa0 100644 Binary files a/doc/workflow/img/award_emoji_votes_most_popular.png and b/doc/workflow/img/award_emoji_votes_most_popular.png differ diff --git a/doc/workflow/img/award_emoji_votes_sort_options.png b/doc/workflow/img/award_emoji_votes_sort_options.png index ae6e224b317..c6dc1b939c1 100644 Binary files a/doc/workflow/img/award_emoji_votes_sort_options.png and b/doc/workflow/img/award_emoji_votes_sort_options.png differ diff --git a/doc/workflow/img/file_finder_find_button.png b/doc/workflow/img/file_finder_find_button.png index 96e383f0213..23139cc00c5 100644 Binary files a/doc/workflow/img/file_finder_find_button.png and b/doc/workflow/img/file_finder_find_button.png differ diff --git a/doc/workflow/img/file_finder_find_file.png b/doc/workflow/img/file_finder_find_file.png index c6508514c76..c2212c7cd9e 100644 Binary files a/doc/workflow/img/file_finder_find_file.png and b/doc/workflow/img/file_finder_find_file.png differ diff --git a/doc/workflow/img/forking_workflow_choose_namespace.png b/doc/workflow/img/forking_workflow_choose_namespace.png index 1839d5e8be2..b34b12090a1 100644 Binary files a/doc/workflow/img/forking_workflow_choose_namespace.png and b/doc/workflow/img/forking_workflow_choose_namespace.png differ diff --git a/doc/workflow/img/forking_workflow_fork_button.png b/doc/workflow/img/forking_workflow_fork_button.png index cc79d6fd40c..29854e6c516 100644 Binary files a/doc/workflow/img/forking_workflow_fork_button.png and b/doc/workflow/img/forking_workflow_fork_button.png differ diff --git a/doc/workflow/img/forking_workflow_path_taken_error.png b/doc/workflow/img/forking_workflow_path_taken_error.png index a859155aef0..9365fd13200 100644 Binary files a/doc/workflow/img/forking_workflow_path_taken_error.png and b/doc/workflow/img/forking_workflow_path_taken_error.png differ diff --git a/doc/workflow/img/new_branch_from_issue.png b/doc/workflow/img/new_branch_from_issue.png index 61acdd30ae9..286d775bb9e 100644 Binary files a/doc/workflow/img/new_branch_from_issue.png and b/doc/workflow/img/new_branch_from_issue.png differ diff --git a/doc/workflow/img/todo_list_item.png b/doc/workflow/img/todo_list_item.png index 884ba1d22a3..076069b651e 100644 Binary files a/doc/workflow/img/todo_list_item.png and b/doc/workflow/img/todo_list_item.png differ diff --git a/doc/workflow/img/todos_add_todo_sidebar.png b/doc/workflow/img/todos_add_todo_sidebar.png index 126ecc2c82f..59175ae44c5 100644 Binary files a/doc/workflow/img/todos_add_todo_sidebar.png and b/doc/workflow/img/todos_add_todo_sidebar.png differ diff --git a/doc/workflow/img/todos_icon.png b/doc/workflow/img/todos_icon.png index bba77f88913..1ed16b09669 100644 Binary files a/doc/workflow/img/todos_icon.png and b/doc/workflow/img/todos_icon.png differ diff --git a/doc/workflow/img/todos_index.png b/doc/workflow/img/todos_index.png index f1438ef7355..902a5aa6bd3 100644 Binary files a/doc/workflow/img/todos_index.png and b/doc/workflow/img/todos_index.png differ diff --git a/doc/workflow/img/todos_mark_done_sidebar.png b/doc/workflow/img/todos_mark_done_sidebar.png index f449f977dd6..aa35bb672ea 100644 Binary files a/doc/workflow/img/todos_mark_done_sidebar.png and b/doc/workflow/img/todos_mark_done_sidebar.png differ diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.png index fd7a4d3fabf..62c5c86c9b3 100644 Binary files a/doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.png and b/doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.png differ diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_login.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_login.png index fd1ba6f5884..96bce70b74d 100644 Binary files a/doc/workflow/importing/fogbugz_importer/fogbugz_import_login.png and b/doc/workflow/importing/fogbugz_importer/fogbugz_import_login.png differ diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.png index 186c1563951..b26c652e382 100644 Binary files a/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.png and b/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.png differ diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.png index 2f84d3232f2..ccc82f9d4cd 100644 Binary files a/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.png and b/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.png differ diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.png index 652ca20b9ab..28ff55a8d89 100644 Binary files a/doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.png and b/doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.png differ diff --git a/doc/workflow/importing/gitlab_importer/importer.png b/doc/workflow/importing/gitlab_importer/importer.png index 35a7ddc8318..27d42eb492e 100644 Binary files a/doc/workflow/importing/gitlab_importer/importer.png and b/doc/workflow/importing/gitlab_importer/importer.png differ diff --git a/doc/workflow/importing/gitlab_importer/new_project_page.png b/doc/workflow/importing/gitlab_importer/new_project_page.png index 81074d2d016..c673724f436 100644 Binary files a/doc/workflow/importing/gitlab_importer/new_project_page.png and b/doc/workflow/importing/gitlab_importer/new_project_page.png differ diff --git a/doc/workflow/importing/img/import_projects_from_github_importer.png b/doc/workflow/importing/img/import_projects_from_github_importer.png index eadd33c695f..d8effaf6075 100644 Binary files a/doc/workflow/importing/img/import_projects_from_github_importer.png and b/doc/workflow/importing/img/import_projects_from_github_importer.png differ diff --git a/doc/workflow/importing/img/import_projects_from_github_new_project_page.png b/doc/workflow/importing/img/import_projects_from_github_new_project_page.png index 6e91c430a33..b23ade4480c 100644 Binary files a/doc/workflow/importing/img/import_projects_from_github_new_project_page.png and b/doc/workflow/importing/img/import_projects_from_github_new_project_page.png differ diff --git a/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png b/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png index c11863ab10c..f50d9266991 100644 Binary files a/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png and b/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png differ diff --git a/doc/workflow/merge_commits.png b/doc/workflow/merge_commits.png index 8aa1587cde6..4a80811c6e3 100644 Binary files a/doc/workflow/merge_commits.png and b/doc/workflow/merge_commits.png differ diff --git a/doc/workflow/merge_request.png b/doc/workflow/merge_request.png index 6aad1d82f6e..08dfc7f2468 100644 Binary files a/doc/workflow/merge_request.png and b/doc/workflow/merge_request.png differ diff --git a/doc/workflow/messy_flow.png b/doc/workflow/messy_flow.png index 8d2c0dae8c2..7e72e2a3be6 100644 Binary files a/doc/workflow/messy_flow.png and b/doc/workflow/messy_flow.png differ diff --git a/doc/workflow/milestones/form.png b/doc/workflow/milestones/form.png index 3965ca4d083..c4731d88543 100644 Binary files a/doc/workflow/milestones/form.png and b/doc/workflow/milestones/form.png differ diff --git a/doc/workflow/milestones/group_form.png b/doc/workflow/milestones/group_form.png index ff20df8081f..dccdb019703 100644 Binary files a/doc/workflow/milestones/group_form.png and b/doc/workflow/milestones/group_form.png differ diff --git a/doc/workflow/mr_inline_comments.png b/doc/workflow/mr_inline_comments.png index af7df3100d0..6a2e66a01ba 100644 Binary files a/doc/workflow/mr_inline_comments.png and b/doc/workflow/mr_inline_comments.png differ diff --git a/doc/workflow/notifications/settings.png b/doc/workflow/notifications/settings.png index d50757beffc..8a5494d16a8 100644 Binary files a/doc/workflow/notifications/settings.png and b/doc/workflow/notifications/settings.png differ diff --git a/doc/workflow/production_branch.png b/doc/workflow/production_branch.png index d88a3687151..648d5d5c92e 100644 Binary files a/doc/workflow/production_branch.png and b/doc/workflow/production_branch.png differ diff --git a/doc/workflow/rebase.png b/doc/workflow/rebase.png index df353311fa0..8b9bb61a5cc 100644 Binary files a/doc/workflow/rebase.png and b/doc/workflow/rebase.png differ diff --git a/doc/workflow/release_branches.png b/doc/workflow/release_branches.png index c2162248d25..5194d75a667 100644 Binary files a/doc/workflow/release_branches.png and b/doc/workflow/release_branches.png differ diff --git a/doc/workflow/releases/new_tag.png b/doc/workflow/releases/new_tag.png index 2456a8500f4..97519e5808f 100644 Binary files a/doc/workflow/releases/new_tag.png and b/doc/workflow/releases/new_tag.png differ diff --git a/doc/workflow/releases/tags.png b/doc/workflow/releases/tags.png index eeda967afd6..4c032f96125 100644 Binary files a/doc/workflow/releases/tags.png and b/doc/workflow/releases/tags.png differ diff --git a/doc/workflow/remove_checkbox.png b/doc/workflow/remove_checkbox.png index 3b0393deb0f..fb0e792b37b 100644 Binary files a/doc/workflow/remove_checkbox.png and b/doc/workflow/remove_checkbox.png differ -- cgit v1.2.1 From a571a61f0a1e381b438034c44ddc240ddb965b83 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 22 Nov 2016 12:56:03 -0600 Subject: keep json alphabetized --- .eslintrc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.eslintrc b/.eslintrc index 67dbf9cffd2..788a88487d8 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,17 +4,17 @@ "es6": true }, "extends": "airbnb", - "plugins": [ - "filenames" - ], - "rules": { - "filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"] - }, "globals": { "$": false, "_": false, "gl": false, "gon": false, "jQuery": false + }, + "plugins": [ + "filenames" + ], + "rules": { + "filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"] } } -- cgit v1.2.1 From 7e0e5bc3786bbe8ce2a58e28cc166f9e7cbc8193 Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller <bbodenmiller@hotmail.com> Date: Tue, 22 Nov 2016 19:25:20 +0000 Subject: add details that Git LFS must be enabled in project --- doc/workflow/lfs/manage_large_binaries_with_git_lfs.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md index 28e6a21a229..6a7098e79d0 100644 --- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md +++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md @@ -24,6 +24,7 @@ Documentation for GitLab instance administrators is under [LFS administration do ## Requirements * Git LFS is supported in GitLab starting with version 8.2 +* Git LFS must be enabled under project settings * [Git LFS client](https://git-lfs.github.com) version 1.0.1 and up ## Known limitations @@ -31,10 +32,10 @@ Documentation for GitLab instance administrators is under [LFS administration do * Git LFS v1 original API is not supported since it was deprecated early in LFS development * When SSH is set as a remote, Git LFS objects still go through HTTPS -* Any Git LFS request will ask for HTTPS credentials to be provided so good Git +* Any Git LFS request will ask for HTTPS credentials to be provided so a good Git credentials store is recommended * Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have - to add the URL to Git config manually (see #troubleshooting) + to add the URL to Git config manually (see [troubleshooting](#troubleshooting)) >**Note**: With 8.12 GitLab added LFS support to SSH. The Git LFS communication still goes over HTTP, but now the SSH client passes the correct credentials @@ -95,7 +96,7 @@ available to the project anymore. Probably the object was removed from the serve * Local git repository is using deprecated LFS API -### Invalid status for <url> : 501 +### Invalid status for `<url>` : 501 Git LFS will log the failures into a log file. To view this log file, while in project directory: @@ -106,6 +107,9 @@ git lfs logs last If the status `error 501` is shown, it is because: +* Git LFS is not enabled in project settings. Check your project settings and + enable Git LFS. + * Git LFS support is not enabled on the GitLab server. Check with your GitLab administrator why Git LFS is not enabled on the server. See [LFS administration documentation](lfs_administration.md) for instructions -- cgit v1.2.1 From 459cd939e7a31c833858c86db6003327524800de Mon Sep 17 00:00:00 2001 From: winniehell <git@winniehell.de> Date: Wed, 9 Nov 2016 15:26:03 +0100 Subject: Generate ESLint HTML report (!7374) --- .gitignore | 1 + .gitlab-ci.yml | 23 +++++++++++++++++++++-- package.json | 3 ++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 6a1002621f4..0b602d613c7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ .chef .directory /.envrc +eslint-report.html /.gitlab_shell_secret .idea /.rbenv-version diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ab45ea57aed..2b65bc4182b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -349,7 +349,7 @@ coverage: - coverage/index.html - coverage/assets/ -lint-javascript: +lint:javascript: cache: paths: - node_modules/ @@ -358,7 +358,24 @@ lint-javascript: before_script: - npm install script: - - npm run eslint + - npm --silent run eslint + +lint:javascript:report: + cache: + paths: + - node_modules/ + stage: post-test + image: "node:7.1" + before_script: + - npm install + script: + - find app/ spec/ -name '*.js' -or -name '*.js.es6' -exec sed --in-place 's|/\* eslint-disable .*\*/||' {} \; # run report over all files + - npm --silent run eslint-report || true # ignore exit code + artifacts: + name: eslint-report + expire_in: 31d + paths: + - eslint-report.html # Trigger docs build # https://gitlab.com/gitlab-com/doc-gitlab-com/blob/master/README.md#deployment-process @@ -398,11 +415,13 @@ pages: dependencies: - coverage - teaspoon + - lint:javascript:report script: - mv public/ .public/ - mkdir public/ - mv coverage public/coverage-ruby - mv coverage-javascript/default/ public/coverage-javascript/ + - mv eslint-report.html public/ artifacts: paths: - public diff --git a/package.json b/package.json index 2a9fb808eef..93e23d63c36 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,8 @@ "private": true, "scripts": { "eslint": "eslint --ext .js,.js.es6 .", - "eslint-fix": "eslint --fix --ext .js,.js.es6 ." + "eslint-fix": "npm run eslint -- --fix", + "eslint-report": "npm run eslint -- --format html --output-file ./eslint-report.html" }, "devDependencies": { "eslint": "^3.1.1", -- cgit v1.2.1 From 9e87eaf83f6f75e013ef3a7aa565caada70a1390 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 22 Nov 2016 14:47:02 -0600 Subject: sort pipeline graph builds and build groups by name --- app/views/projects/commit/_pipeline_stage.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/commit/_pipeline_stage.html.haml b/app/views/projects/commit/_pipeline_stage.html.haml index 289aa5178b1..f9a9c8707f5 100644 --- a/app/views/projects/commit/_pipeline_stage.html.haml +++ b/app/views/projects/commit/_pipeline_stage.html.haml @@ -1,4 +1,4 @@ -- status_groups = statuses.group_by(&:group_name) +- status_groups = statuses.sort_by(&:name).group_by(&:group_name) - status_groups.each do |group_name, grouped_statuses| - if grouped_statuses.one? - status = grouped_statuses.first -- cgit v1.2.1 From aba7f7dd87affaaf8c957a4bf1ce874ccc57f1a4 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 22 Nov 2016 14:55:56 -0600 Subject: add CHANGELOG entry for !7681 --- changelogs/unreleased/24739-collapsed-build-list-sorting.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/24739-collapsed-build-list-sorting.yml diff --git a/changelogs/unreleased/24739-collapsed-build-list-sorting.yml b/changelogs/unreleased/24739-collapsed-build-list-sorting.yml new file mode 100644 index 00000000000..036e606318f --- /dev/null +++ b/changelogs/unreleased/24739-collapsed-build-list-sorting.yml @@ -0,0 +1,4 @@ +--- +title: Sort builds by name within pipeline graph +merge_request: 7681 +author: -- cgit v1.2.1 From 40822b618576f06098f3f61cc5e11a29d656619e Mon Sep 17 00:00:00 2001 From: blackst0ne <blackst0ne.ru@gmail.com> Date: Tue, 22 Nov 2016 21:31:01 +1100 Subject: Fix sidekiq stats in admin area Added tests Added changelog entry Resolved all issues in code --- app/helpers/sidekiq_helper.rb | 10 +++------- .../unreleased/fix_sidekiq_stats_in_admin_area.yml | 4 ++++ spec/helpers/sidekiq_helper_spec.rb | 23 ++++++++++++++++++++++ 3 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 changelogs/unreleased/fix_sidekiq_stats_in_admin_area.yml diff --git a/app/helpers/sidekiq_helper.rb b/app/helpers/sidekiq_helper.rb index 56749d80bd3..b5017080cfb 100644 --- a/app/helpers/sidekiq_helper.rb +++ b/app/helpers/sidekiq_helper.rb @@ -5,15 +5,11 @@ module SidekiqHelper (?<mem>[\d\.,]+)\s+ (?<state>[DRSTWXZNLsl\+<]+)\s+ (?<start>.+)\s+ - (?<command>sidekiq.*\])\s* + (?<command>sidekiq.*\]) \z/x def parse_sidekiq_ps(line) - match = line.match(SIDEKIQ_PS_REGEXP) - if match - match[1..6] - else - %w[? ? ? ? ? ?] - end + match = line.strip.match(SIDEKIQ_PS_REGEXP) + match ? match[1..6] : Array.new(6, '?') end end diff --git a/changelogs/unreleased/fix_sidekiq_stats_in_admin_area.yml b/changelogs/unreleased/fix_sidekiq_stats_in_admin_area.yml new file mode 100644 index 00000000000..4f007be8624 --- /dev/null +++ b/changelogs/unreleased/fix_sidekiq_stats_in_admin_area.yml @@ -0,0 +1,4 @@ +--- +title: Sidekiq stats in the admin area will now show correctly on different platforms +merge_request: +author: blackst0ne diff --git a/spec/helpers/sidekiq_helper_spec.rb b/spec/helpers/sidekiq_helper_spec.rb index d60839b78ec..f86e496740a 100644 --- a/spec/helpers/sidekiq_helper_spec.rb +++ b/spec/helpers/sidekiq_helper_spec.rb @@ -30,6 +30,29 @@ describe SidekiqHelper do expect(parts).to eq(['55137', '10.0', '2.1', 'S+', '2:30pm', 'sidekiq 4.1.4 gitlab [0 of 25 busy]']) end + it 'parses OSX output' do + line = ' 1641 1.5 3.8 S+ 4:04PM sidekiq 4.2.1 gitlab [0 of 25 busy]' + parts = helper.parse_sidekiq_ps(line) + + expect(parts).to eq(['1641', '1.5', '3.8', 'S+', '4:04PM', 'sidekiq 4.2.1 gitlab [0 of 25 busy]']) + end + + it 'parses Ubuntu output' do + # Ubuntu Linux 16.04 LTS / procps-3.3.10-4ubuntu2 + line = ' 938 1.4 2.5 Sl+ 21:23:21 sidekiq 4.2.1 gitlab [0 of 25 busy] ' + parts = helper.parse_sidekiq_ps(line) + + expect(parts).to eq(['938', '1.4', '2.5', 'Sl+', '21:23:21', 'sidekiq 4.2.1 gitlab [0 of 25 busy]']) + end + + it 'parses Debian output' do + # Debian Linux Wheezy/Jessie + line = '17725 1.0 12.1 Ssl 19:20:15 sidekiq 4.2.1 gitlab-rails [0 of 25 busy] ' + parts = helper.parse_sidekiq_ps(line) + + expect(parts).to eq(['17725', '1.0', '12.1', 'Ssl', '19:20:15', 'sidekiq 4.2.1 gitlab-rails [0 of 25 busy]']) + end + it 'does fail gracefully on line not matching the format' do line = '55137 10.0 2.1 S+ 2:30pm something' parts = helper.parse_sidekiq_ps(line) -- cgit v1.2.1 From 11785719cd6ba246551af188549a158154c2c9bf Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Tue, 22 Nov 2016 14:37:48 -0700 Subject: Use btn-primary for running builds btn --- app/assets/javascripts/merge_request_widget.js.es6 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index 56c87af3226..ef27a9c91c0 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -67,7 +67,7 @@ MergeRequestWidget.prototype.addEventListeners = function() { var allowedPages; allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes']; - return $(document).on('page:change.merge_request', (function(_this) { + $(document).on('page:change.merge_request', (function(_this) { return function() { var page; page = $('body').data('page').split(':').last(); @@ -245,7 +245,7 @@ case "not_found": return this.setMergeButtonClass('btn-danger'); case "running": - return this.setMergeButtonClass('btn-warning'); + return this.setMergeButtonClass('btn-info'); case "success": case "success_with_warnings": return this.setMergeButtonClass('btn-create'); @@ -263,7 +263,7 @@ }; MergeRequestWidget.prototype.setMergeButtonClass = function(css_class) { - return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-warning btn-create').addClass(css_class); + return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-info btn-create').addClass(css_class); }; return MergeRequestWidget; -- cgit v1.2.1 From 31a4894a098e5ca0230628677045c2de90961520 Mon Sep 17 00:00:00 2001 From: winniehell <git@winniehell.de> Date: Wed, 23 Nov 2016 01:08:54 +0100 Subject: Replace static fixture for shortcuts_issuable_spec (!7685) --- changelogs/unreleased/shortcuts-issuable-fixture.yml | 4 ++++ spec/javascripts/fixtures/issuable.html.haml | 2 -- spec/javascripts/shortcuts_issuable_spec.js | 6 ++++-- 3 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/shortcuts-issuable-fixture.yml delete mode 100644 spec/javascripts/fixtures/issuable.html.haml diff --git a/changelogs/unreleased/shortcuts-issuable-fixture.yml b/changelogs/unreleased/shortcuts-issuable-fixture.yml new file mode 100644 index 00000000000..88945600886 --- /dev/null +++ b/changelogs/unreleased/shortcuts-issuable-fixture.yml @@ -0,0 +1,4 @@ +--- +title: Replace static fixture for shortcuts_issuable_spec +merge_request: 7685 +author: winniehell diff --git a/spec/javascripts/fixtures/issuable.html.haml b/spec/javascripts/fixtures/issuable.html.haml deleted file mode 100644 index 42ab4aa68b1..00000000000 --- a/spec/javascripts/fixtures/issuable.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -%form.js-main-target-form - %textarea#note_note diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index 7d36d79b687..e37816b0a8c 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -4,9 +4,11 @@ (function() { describe('ShortcutsIssuable', function() { - fixture.preload('issuable.html'); + var fixtureName = 'issues/open-issue.html.raw'; + fixture.preload(fixtureName); beforeEach(function() { - fixture.load('issuable.html'); + fixture.load(fixtureName); + document.querySelector('.js-new-note-form').classList.add('js-main-target-form'); return this.shortcut = new ShortcutsIssuable(); }); return describe('#replyWithSelectedText', function() { -- cgit v1.2.1 From 4b4fd743c35aad51e42d074336191035e7303cf0 Mon Sep 17 00:00:00 2001 From: winniehell <git@winniehell.de> Date: Wed, 23 Nov 2016 01:25:11 +0100 Subject: Replace static fixture for zen_mode_spec (!7686) --- changelogs/unreleased/zen-mode-fixture.yml | 4 ++++ spec/javascripts/fixtures/zen_mode.html.haml | 8 -------- spec/javascripts/zen_mode_spec.js | 9 +++++---- 3 files changed, 9 insertions(+), 12 deletions(-) create mode 100644 changelogs/unreleased/zen-mode-fixture.yml delete mode 100644 spec/javascripts/fixtures/zen_mode.html.haml diff --git a/changelogs/unreleased/zen-mode-fixture.yml b/changelogs/unreleased/zen-mode-fixture.yml new file mode 100644 index 00000000000..bec6f6e6dba --- /dev/null +++ b/changelogs/unreleased/zen-mode-fixture.yml @@ -0,0 +1,4 @@ +--- +title: Replace static fixture for zen_mode_spec +merge_request: 7686 +author: winniehell diff --git a/spec/javascripts/fixtures/zen_mode.html.haml b/spec/javascripts/fixtures/zen_mode.html.haml deleted file mode 100644 index cb906a7feaa..00000000000 --- a/spec/javascripts/fixtures/zen_mode.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -.md-area - .zen-backdrop - %textarea#note_note.js-gfm-input.markdown-area - %a.js-zen-enter(tabindex="-1" href="#") - %i.fa.fa-expand - Edit in fullscreen - %a.js-zen-leave(tabindex="-1" href="#") - %i.fa.fa-compress diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index a18e8aee9b1..b9acaaa5a0d 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -6,9 +6,10 @@ var enterZen, escapeKeydown, exitZen; describe('ZenMode', function() { - fixture.preload('zen_mode.html'); + var fixtureName = 'issues/open-issue.html.raw'; + fixture.preload(fixtureName); beforeEach(function() { - fixture.load('zen_mode.html'); + fixture.load(fixtureName); spyOn(Dropzone, 'forElement').and.callFake(function() { return { enable: function() { @@ -60,11 +61,11 @@ }); enterZen = function() { - return $('a.js-zen-enter').click(); + return $('.js-zen-enter').click(); }; exitZen = function() { // Ohmmmmmmm - return $('a.js-zen-leave').click(); + return $('.js-zen-leave').click(); }; escapeKeydown = function() { -- cgit v1.2.1 From e5a968c4f1485c8890d515bb5bcc958546916fc3 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Fri, 18 Nov 2016 13:23:52 -0600 Subject: move timeago.js to vendor directory --- app/assets/javascripts/application.js | 1 + app/assets/javascripts/lib/utils/timeago.js | 239 ---------------------------- vendor/assets/javascripts/timeago.js | 237 +++++++++++++++++++++++++++ 3 files changed, 238 insertions(+), 239 deletions(-) delete mode 100644 app/assets/javascripts/lib/utils/timeago.js create mode 100644 vendor/assets/javascripts/timeago.js diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 76f3c6506ed..9333c6e8586 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -19,6 +19,7 @@ /*= require jquery.scrollTo */ /*= require jquery.turbolinks */ /*= require js.cookie */ +/*= require timeago */ /*= require turbolinks */ /*= require autosave */ /*= require bootstrap/affix */ diff --git a/app/assets/javascripts/lib/utils/timeago.js b/app/assets/javascripts/lib/utils/timeago.js deleted file mode 100644 index edf0a612374..00000000000 --- a/app/assets/javascripts/lib/utils/timeago.js +++ /dev/null @@ -1,239 +0,0 @@ -/* eslint-disable no-unused-expressions, wrap-iife, func-names, curly, no-param-reassign, no-trailing-spaces, prefer-arrow-callback, no-var, one-var, quote-props, space-before-function-paren, vars-on-top, radix, prefer-template, space-infix-ops, no-use-before-define, newline-per-chained-call, no-useless-escape, no-nested-ternary, indent, no-undef, no-plusplus, one-var-declaration-per-line, operator-assignment, consistent-return, keyword-spacing, max-len, space-unary-ops, no-shadow, no-restricted-syntax, guard-for-in, eol-last, max-len */ - -/** - * Copyright (c) 2016 hustcc - * License: MIT - * Version: v2.0.2 - * https://github.com/hustcc/timeago.js - * This is a forked from (https://gitlab.com/ClemMakesApps/timeago.js) -**/ -/* eslint-disable */ -/* jshint expr: true */ -!function (root, factory) { - if (typeof module === 'object' && module.exports) - module.exports = factory(root); - else - root.timeago = factory(root); -}(typeof window !== 'undefined' ? window : this, -function () { - var cnt = 0, // the timer counter, for timer key - indexMapEn = 'second_minute_hour_day_week_month_year'.split('_'), - - // build-in locales: en & zh_CN - locales = { - 'en': function(number, index) { - if (index === 0) return ['just now', 'right now']; - var unit = indexMapEn[parseInt(index / 2)]; - if (number > 1) unit += 's'; - return [number + ' ' + unit + ' ago', 'in ' + number + ' ' + unit]; - }, - }, - // second, minute, hour, day, week, month, year(365 days) - SEC_ARRAY = [60, 60, 24, 7, 365/7/12, 12], - SEC_ARRAY_LEN = 6, - ATTR_DATETIME = 'datetime'; - - // format Date / string / timestamp to Date instance. - function toDate(input) { - if (input instanceof Date) return input; - if (!isNaN(input)) return new Date(toInt(input)); - if (/^\d+$/.test(input)) return new Date(toInt(input, 10)); - input = (input || '').trim().replace(/\.\d+/, '') // remove milliseconds - .replace(/-/, '/').replace(/-/, '/') - .replace(/T/, ' ').replace(/Z/, ' UTC') - .replace(/([\+\-]\d\d)\:?(\d\d)/, ' $1$2'); // -04:00 -> -0400 - return new Date(input); - } - // change f into int, remove Decimal. just for code compression - function toInt(f) { - return parseInt(f); - } - // format the diff second to *** time ago, with setting locale - function formatDiff(diff, locale, defaultLocale) { - // if locale is not exist, use defaultLocale. - // if defaultLocale is not exist, use build-in `en`. - // be sure of no error when locale is not exist. - locale = locales[locale] ? locale : (locales[defaultLocale] ? defaultLocale : 'en'); - // if (! locales[locale]) locale = defaultLocale; - var i = 0; - agoin = diff < 0 ? 1 : 0; // timein or timeago - diff = Math.abs(diff); - - for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) { - diff /= SEC_ARRAY[i]; - } - diff = toInt(diff); - i *= 2; - - if (diff > (i === 0 ? 9 : 1)) i += 1; - return locales[locale](diff, i)[agoin].replace('%s', diff); - } - // calculate the diff second between date to be formated an now date. - function diffSec(date, nowDate) { - nowDate = nowDate ? toDate(nowDate) : new Date(); - return (nowDate - toDate(date)) / 1000; - } - /** - * nextInterval: calculate the next interval time. - * - diff: the diff sec between now and date to be formated. - * - * What's the meaning? - * diff = 61 then return 59 - * diff = 3601 (an hour + 1 second), then return 3599 - * make the interval with high performace. - **/ - function nextInterval(diff) { - var rst = 1, i = 0, d = Math.abs(diff); - for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) { - diff /= SEC_ARRAY[i]; - rst *= SEC_ARRAY[i]; - } - // return leftSec(d, rst); - d = d % rst; - d = d ? rst - d : rst; - return Math.ceil(d); - } - // get the datetime attribute, jQuery and DOM - function getDateAttr(node) { - if (node.getAttribute) return node.getAttribute(ATTR_DATETIME); - if(node.attr) return node.attr(ATTR_DATETIME); - } - /** - * timeago: the function to get `timeago` instance. - * - nowDate: the relative date, default is new Date(). - * - defaultLocale: the default locale, default is en. if your set it, then the `locale` parameter of format is not needed of you. - * - * How to use it? - * var timeagoLib = require('timeago.js'); - * var timeago = timeagoLib(); // all use default. - * var timeago = timeagoLib('2016-09-10'); // the relative date is 2016-09-10, so the 2016-09-11 will be 1 day ago. - * var timeago = timeagoLib(null, 'zh_CN'); // set default locale is `zh_CN`. - * var timeago = timeagoLib('2016-09-10', 'zh_CN'); // the relative date is 2016-09-10, and locale is zh_CN, so the 2016-09-11 will be 1天前. - **/ - function Timeago(nowDate, defaultLocale) { - var timers = {}; // real-time render timers - // if do not set the defaultLocale, set it with `en` - if (! defaultLocale) defaultLocale = 'en'; // use default build-in locale - // what the timer will do - function doRender(node, date, locale, cnt) { - var diff = diffSec(date, nowDate); - node.innerHTML = formatDiff(diff, locale, defaultLocale); - // waiting %s seconds, do the next render - timers['k' + cnt] = setTimeout(function() { - doRender(node, date, locale, cnt); - }, nextInterval(diff) * 1000); - } - /** - * nextInterval: calculate the next interval time. - * - diff: the diff sec between now and date to be formated. - * - * What's the meaning? - * diff = 61 then return 59 - * diff = 3601 (an hour + 1 second), then return 3599 - * make the interval with high performace. - **/ - // this.nextInterval = function(diff) { // for dev test - // var rst = 1, i = 0, d = Math.abs(diff); - // for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) { - // diff /= SEC_ARRAY[i]; - // rst *= SEC_ARRAY[i]; - // } - // // return leftSec(d, rst); - // d = d % rst; - // d = d ? rst - d : rst; - // return Math.ceil(d); - // }; // for dev test - /** - * format: format the date to *** time ago, with setting or default locale - * - date: the date / string / timestamp to be formated - * - locale: the formated string's locale name, e.g. en / zh_CN - * - * How to use it? - * var timeago = require('timeago.js')(); - * timeago.format(new Date(), 'pl'); // Date instance - * timeago.format('2016-09-10', 'fr'); // formated date string - * timeago.format(1473473400269); // timestamp with ms - **/ - this.format = function(date, locale) { - return formatDiff(diffSec(date, nowDate), locale, defaultLocale); - }; - /** - * render: render the DOM real-time. - * - nodes: which nodes will be rendered. - * - locale: the locale name used to format date. - * - * How to use it? - * var timeago = new require('timeago.js')(); - * // 1. javascript selector - * timeago.render(document.querySelectorAll('.need_to_be_rendered')); - * // 2. use jQuery selector - * timeago.render($('.need_to_be_rendered'), 'pl'); - * - * Notice: please be sure the dom has attribute `datetime`. - **/ - this.render = function(nodes, locale) { - if (nodes.length === undefined) nodes = [nodes]; - for (var i = 0; i < nodes.length; i++) { - doRender(nodes[i], getDateAttr(nodes[i]), locale, ++ cnt); // render item - } - }; - /** - * cancel: cancel all the timers which are doing real-time render. - * - * How to use it? - * var timeago = new require('timeago.js')(); - * timeago.render(document.querySelectorAll('.need_to_be_rendered')); - * timeago.cancel(); // will stop all the timer, stop render in real time. - **/ - this.cancel = function() { - for (var key in timers) { - clearTimeout(timers[key]); - } - timers = {}; - }; - /** - * setLocale: set the default locale name. - * - * How to use it? - * var timeago = require('timeago.js'); - * timeago = new timeago(); - * timeago.setLocale('fr'); - **/ - this.setLocale = function(locale) { - defaultLocale = locale; - }; - return this; - } - /** - * timeago: the function to get `timeago` instance. - * - nowDate: the relative date, default is new Date(). - * - defaultLocale: the default locale, default is en. if your set it, then the `locale` parameter of format is not needed of you. - * - * How to use it? - * var timeagoLib = require('timeago.js'); - * var timeago = timeagoLib(); // all use default. - * var timeago = timeagoLib('2016-09-10'); // the relative date is 2016-09-10, so the 2016-09-11 will be 1 day ago. - * var timeago = timeagoLib(null, 'zh_CN'); // set default locale is `zh_CN`. - * var timeago = timeagoLib('2016-09-10', 'zh_CN'); // the relative date is 2016-09-10, and locale is zh_CN, so the 2016-09-11 will be 1天前. - **/ - function timeagoFactory(nowDate, defaultLocale) { - return new Timeago(nowDate, defaultLocale); - } - /** - * register: register a new language locale - * - locale: locale name, e.g. en / zh_CN, notice the standard. - * - localeFunc: the locale process function - * - * How to use it? - * var timeagoLib = require('timeago.js'); - * - * timeagoLib.register('the locale name', the_locale_func); - * // or - * timeagoLib.register('pl', require('timeago.js/locales/pl')); - **/ - timeagoFactory.register = function(locale, localeFunc) { - locales[locale] = localeFunc; - }; - - return timeagoFactory; -}); \ No newline at end of file diff --git a/vendor/assets/javascripts/timeago.js b/vendor/assets/javascripts/timeago.js new file mode 100644 index 00000000000..0eb6f7967a5 --- /dev/null +++ b/vendor/assets/javascripts/timeago.js @@ -0,0 +1,237 @@ +/** + * Copyright (c) 2016 hustcc + * License: MIT + * Version: v2.0.2 + * https://github.com/hustcc/timeago.js + * This is a forked from (https://gitlab.com/ClemMakesApps/timeago.js) +**/ +/* eslint-disable */ +/* jshint expr: true */ +!function (root, factory) { + if (typeof module === 'object' && module.exports) + module.exports = factory(root); + else + root.timeago = factory(root); +}(typeof window !== 'undefined' ? window : this, +function () { + var cnt = 0, // the timer counter, for timer key + indexMapEn = 'second_minute_hour_day_week_month_year'.split('_'), + + // build-in locales: en & zh_CN + locales = { + 'en': function(number, index) { + if (index === 0) return ['just now', 'right now']; + var unit = indexMapEn[parseInt(index / 2)]; + if (number > 1) unit += 's'; + return [number + ' ' + unit + ' ago', 'in ' + number + ' ' + unit]; + }, + }, + // second, minute, hour, day, week, month, year(365 days) + SEC_ARRAY = [60, 60, 24, 7, 365/7/12, 12], + SEC_ARRAY_LEN = 6, + ATTR_DATETIME = 'datetime'; + + // format Date / string / timestamp to Date instance. + function toDate(input) { + if (input instanceof Date) return input; + if (!isNaN(input)) return new Date(toInt(input)); + if (/^\d+$/.test(input)) return new Date(toInt(input, 10)); + input = (input || '').trim().replace(/\.\d+/, '') // remove milliseconds + .replace(/-/, '/').replace(/-/, '/') + .replace(/T/, ' ').replace(/Z/, ' UTC') + .replace(/([\+\-]\d\d)\:?(\d\d)/, ' $1$2'); // -04:00 -> -0400 + return new Date(input); + } + // change f into int, remove Decimal. just for code compression + function toInt(f) { + return parseInt(f); + } + // format the diff second to *** time ago, with setting locale + function formatDiff(diff, locale, defaultLocale) { + // if locale is not exist, use defaultLocale. + // if defaultLocale is not exist, use build-in `en`. + // be sure of no error when locale is not exist. + locale = locales[locale] ? locale : (locales[defaultLocale] ? defaultLocale : 'en'); + // if (! locales[locale]) locale = defaultLocale; + var i = 0; + agoin = diff < 0 ? 1 : 0; // timein or timeago + diff = Math.abs(diff); + + for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) { + diff /= SEC_ARRAY[i]; + } + diff = toInt(diff); + i *= 2; + + if (diff > (i === 0 ? 9 : 1)) i += 1; + return locales[locale](diff, i)[agoin].replace('%s', diff); + } + // calculate the diff second between date to be formated an now date. + function diffSec(date, nowDate) { + nowDate = nowDate ? toDate(nowDate) : new Date(); + return (nowDate - toDate(date)) / 1000; + } + /** + * nextInterval: calculate the next interval time. + * - diff: the diff sec between now and date to be formated. + * + * What's the meaning? + * diff = 61 then return 59 + * diff = 3601 (an hour + 1 second), then return 3599 + * make the interval with high performace. + **/ + function nextInterval(diff) { + var rst = 1, i = 0, d = Math.abs(diff); + for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) { + diff /= SEC_ARRAY[i]; + rst *= SEC_ARRAY[i]; + } + // return leftSec(d, rst); + d = d % rst; + d = d ? rst - d : rst; + return Math.ceil(d); + } + // get the datetime attribute, jQuery and DOM + function getDateAttr(node) { + if (node.getAttribute) return node.getAttribute(ATTR_DATETIME); + if(node.attr) return node.attr(ATTR_DATETIME); + } + /** + * timeago: the function to get `timeago` instance. + * - nowDate: the relative date, default is new Date(). + * - defaultLocale: the default locale, default is en. if your set it, then the `locale` parameter of format is not needed of you. + * + * How to use it? + * var timeagoLib = require('timeago.js'); + * var timeago = timeagoLib(); // all use default. + * var timeago = timeagoLib('2016-09-10'); // the relative date is 2016-09-10, so the 2016-09-11 will be 1 day ago. + * var timeago = timeagoLib(null, 'zh_CN'); // set default locale is `zh_CN`. + * var timeago = timeagoLib('2016-09-10', 'zh_CN'); // the relative date is 2016-09-10, and locale is zh_CN, so the 2016-09-11 will be 1天前. + **/ + function Timeago(nowDate, defaultLocale) { + var timers = {}; // real-time render timers + // if do not set the defaultLocale, set it with `en` + if (! defaultLocale) defaultLocale = 'en'; // use default build-in locale + // what the timer will do + function doRender(node, date, locale, cnt) { + var diff = diffSec(date, nowDate); + node.innerHTML = formatDiff(diff, locale, defaultLocale); + // waiting %s seconds, do the next render + timers['k' + cnt] = setTimeout(function() { + doRender(node, date, locale, cnt); + }, nextInterval(diff) * 1000); + } + /** + * nextInterval: calculate the next interval time. + * - diff: the diff sec between now and date to be formated. + * + * What's the meaning? + * diff = 61 then return 59 + * diff = 3601 (an hour + 1 second), then return 3599 + * make the interval with high performace. + **/ + // this.nextInterval = function(diff) { // for dev test + // var rst = 1, i = 0, d = Math.abs(diff); + // for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) { + // diff /= SEC_ARRAY[i]; + // rst *= SEC_ARRAY[i]; + // } + // // return leftSec(d, rst); + // d = d % rst; + // d = d ? rst - d : rst; + // return Math.ceil(d); + // }; // for dev test + /** + * format: format the date to *** time ago, with setting or default locale + * - date: the date / string / timestamp to be formated + * - locale: the formated string's locale name, e.g. en / zh_CN + * + * How to use it? + * var timeago = require('timeago.js')(); + * timeago.format(new Date(), 'pl'); // Date instance + * timeago.format('2016-09-10', 'fr'); // formated date string + * timeago.format(1473473400269); // timestamp with ms + **/ + this.format = function(date, locale) { + return formatDiff(diffSec(date, nowDate), locale, defaultLocale); + }; + /** + * render: render the DOM real-time. + * - nodes: which nodes will be rendered. + * - locale: the locale name used to format date. + * + * How to use it? + * var timeago = new require('timeago.js')(); + * // 1. javascript selector + * timeago.render(document.querySelectorAll('.need_to_be_rendered')); + * // 2. use jQuery selector + * timeago.render($('.need_to_be_rendered'), 'pl'); + * + * Notice: please be sure the dom has attribute `datetime`. + **/ + this.render = function(nodes, locale) { + if (nodes.length === undefined) nodes = [nodes]; + for (var i = 0; i < nodes.length; i++) { + doRender(nodes[i], getDateAttr(nodes[i]), locale, ++ cnt); // render item + } + }; + /** + * cancel: cancel all the timers which are doing real-time render. + * + * How to use it? + * var timeago = new require('timeago.js')(); + * timeago.render(document.querySelectorAll('.need_to_be_rendered')); + * timeago.cancel(); // will stop all the timer, stop render in real time. + **/ + this.cancel = function() { + for (var key in timers) { + clearTimeout(timers[key]); + } + timers = {}; + }; + /** + * setLocale: set the default locale name. + * + * How to use it? + * var timeago = require('timeago.js'); + * timeago = new timeago(); + * timeago.setLocale('fr'); + **/ + this.setLocale = function(locale) { + defaultLocale = locale; + }; + return this; + } + /** + * timeago: the function to get `timeago` instance. + * - nowDate: the relative date, default is new Date(). + * - defaultLocale: the default locale, default is en. if your set it, then the `locale` parameter of format is not needed of you. + * + * How to use it? + * var timeagoLib = require('timeago.js'); + * var timeago = timeagoLib(); // all use default. + * var timeago = timeagoLib('2016-09-10'); // the relative date is 2016-09-10, so the 2016-09-11 will be 1 day ago. + * var timeago = timeagoLib(null, 'zh_CN'); // set default locale is `zh_CN`. + * var timeago = timeagoLib('2016-09-10', 'zh_CN'); // the relative date is 2016-09-10, and locale is zh_CN, so the 2016-09-11 will be 1天前. + **/ + function timeagoFactory(nowDate, defaultLocale) { + return new Timeago(nowDate, defaultLocale); + } + /** + * register: register a new language locale + * - locale: locale name, e.g. en / zh_CN, notice the standard. + * - localeFunc: the locale process function + * + * How to use it? + * var timeagoLib = require('timeago.js'); + * + * timeagoLib.register('the locale name', the_locale_func); + * // or + * timeagoLib.register('pl', require('timeago.js/locales/pl')); + **/ + timeagoFactory.register = function(locale, localeFunc) { + locales[locale] = localeFunc; + }; + + return timeagoFactory; +}); -- cgit v1.2.1 From 8ede8603b0f6910886219ab1594eff1c0bfa3252 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 22 Nov 2016 23:47:52 -0600 Subject: timeago should be a dependency of datetime_utility --- app/assets/javascripts/application.js | 1 - .../javascripts/environments/components/environment_item.js.es6 | 8 ++++---- app/assets/javascripts/lib/utils/datetime_utility.js | 8 +++++++- spec/javascripts/build_spec.js.es6 | 1 - spec/javascripts/merge_request_widget_spec.js | 2 +- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 9333c6e8586..76f3c6506ed 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -19,7 +19,6 @@ /*= require jquery.scrollTo */ /*= require jquery.turbolinks */ /*= require js.cookie */ -/*= require timeago */ /*= require turbolinks */ /*= require autosave */ /*= require bootstrap/affix */ diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 36a0fec3cab..07f49cce3dc 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -1,4 +1,7 @@ -/*= require lib/utils/timeago */ +/* global Vue */ +/* global timeago */ + +/*= require timeago */ /*= require lib/utils/text_utility */ /*= require vue_common_component/commit */ /*= require ./environment_actions */ @@ -6,9 +9,6 @@ /*= require ./environment_stop */ /*= require ./environment_rollback */ -/* global Vue */ -/* global timeago */ - (() => { /** * Envrionment Item Component diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index d480fdc882b..963d2851e5f 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -1,4 +1,10 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, no-undef, comma-dangle, no-unused-expressions, prefer-template, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, comma-dangle, no-unused-expressions, prefer-template, padded-blocks, max-len */ +/* global timeago */ +/* global dateFormat */ + +/*= require timeago */ +/*= require date.format */ + (function() { (function(w) { var base; diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6 index ee192c4f18a..4208e076e96 100644 --- a/spec/javascripts/build_spec.js.es6 +++ b/spec/javascripts/build_spec.js.es6 @@ -2,7 +2,6 @@ /* global Build */ /* global Turbolinks */ -//= require lib/utils/timeago //= require lib/utils/datetime_utility //= require build //= require breakpoints diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js index f38e9cb8ef5..62890f1ca96 100644 --- a/spec/javascripts/merge_request_widget_spec.js +++ b/spec/javascripts/merge_request_widget_spec.js @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, indent, quote-props, no-var, padded-blocks, max-len */ + /*= require merge_request_widget */ -/*= require lib/utils/timeago */ /*= require lib/utils/datetime_utility */ (function() { -- cgit v1.2.1 From 2f8fa73b40193954e97ae580bab8df45f3c10c5a Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Wed, 23 Nov 2016 09:02:40 +0100 Subject: Fix BASH usage in the .gitlab-ci.yml --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2b65bc4182b..d7a1df383ee 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,7 +20,7 @@ before_script: - source ./scripts/prepare_build.sh - cp config/gitlab.yml.example config/gitlab.yml - bundle --version - - '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"' + - '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) $FLAGS' - retry gem install knapsack - '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate add_limits_mysql' @@ -328,7 +328,7 @@ migration paths: - git checkout -f FETCH_HEAD - cp config/resque.yml.example config/resque.yml - sed -i 's/localhost/redis/g' config/resque.yml - - bundle install --without postgres production --jobs $(nproc) ${FLAGS[@]} --retry=3 + - bundle install --without postgres production --jobs $(nproc) $FLAGS --retry=3 - rake db:drop db:create db:schema:load db:seed_fu - git checkout $CI_BUILD_REF - source scripts/prepare_build.sh -- cgit v1.2.1 From 125cabd335222b61affc74264ee7e88b61324df0 Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Tue, 22 Nov 2016 15:53:15 +0600 Subject: resolves updated and resolved status is not showin --- app/views/discussions/_discussion.html.haml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml index e4b4ea675d2..2bce2780484 100644 --- a/app/views/discussions/_discussion.html.haml +++ b/app/views/discussions/_discussion.html.haml @@ -32,6 +32,7 @@ an outdated diff = time_ago_with_tooltip(discussion.created_at, placement: "bottom", html_class: "note-created-ago") + = render "discussions/headline", discussion: discussion .discussion-body.js-toggle-content{ class: ("hide" unless expanded) } - if discussion.diff_discussion? && discussion.diff_file -- cgit v1.2.1 From b938aa5cc83ffb51b516c7abfaaf6fe5e37031a6 Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Wed, 23 Nov 2016 09:10:04 +0100 Subject: Fix and relevant spec for plan stage breaking with nil commits --- changelogs/unreleased/fix-cycle-analytics-plan-issue.yml | 4 ++++ lib/gitlab/cycle_analytics/base_event.rb | 2 +- lib/gitlab/cycle_analytics/plan_event.rb | 2 ++ spec/lib/gitlab/cycle_analytics/plan_event_spec.rb | 8 ++++++++ 4 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/fix-cycle-analytics-plan-issue.yml diff --git a/changelogs/unreleased/fix-cycle-analytics-plan-issue.yml b/changelogs/unreleased/fix-cycle-analytics-plan-issue.yml new file mode 100644 index 00000000000..6ed16c6d722 --- /dev/null +++ b/changelogs/unreleased/fix-cycle-analytics-plan-issue.yml @@ -0,0 +1,4 @@ +--- +title: Fix cycle analytics plan stage when commits are missing +merge_request: +author: diff --git a/lib/gitlab/cycle_analytics/base_event.rb b/lib/gitlab/cycle_analytics/base_event.rb index 486139b1687..53a148ad703 100644 --- a/lib/gitlab/cycle_analytics/base_event.rb +++ b/lib/gitlab/cycle_analytics/base_event.rb @@ -16,7 +16,7 @@ module Gitlab event_result.map do |event| serialize(event) if has_permission?(event['id']) - end + end.compact end def custom_query(_base_query); end diff --git a/lib/gitlab/cycle_analytics/plan_event.rb b/lib/gitlab/cycle_analytics/plan_event.rb index b1ae215f348..7c3f0e9989f 100644 --- a/lib/gitlab/cycle_analytics/plan_event.rb +++ b/lib/gitlab/cycle_analytics/plan_event.rb @@ -27,6 +27,8 @@ module Gitlab end def first_time_reference_commit(commits, event) + return nil if commits.blank? + YAML.load(commits).find do |commit| next unless commit[:committed_date] && event['first_mentioned_in_commit_at'] diff --git a/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb index d76a255acf5..4a5604115ec 100644 --- a/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb @@ -6,5 +6,13 @@ describe Gitlab::CycleAnalytics::PlanEvent do it 'has the default order' do expect(event.order).to eq(event.start_time_attrs) end + + context 'no commits' do + it 'does not blow up if there are no commits' do + allow_any_instance_of(Gitlab::CycleAnalytics::EventsQuery).to receive(:execute).and_return([{}]) + + expect { event.fetch }.not_to raise_error + end + end end end -- cgit v1.2.1 From 2a7f4c48c5c98bb007768d8f13e0081be0e133e3 Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Wed, 23 Nov 2016 14:31:37 +0600 Subject: resloves failed rspec test --- spec/features/merge_requests/diff_notes_resolve_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb index eab64bd4b54..d5e3d8e7eff 100644 --- a/spec/features/merge_requests/diff_notes_resolve_spec.rb +++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb @@ -201,7 +201,7 @@ feature 'Diff notes resolve', feature: true, js: true do expect(first('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}") end - expect(page).not_to have_content('Last updated') + expect(page).to have_content('Last updated') page.within '.line-resolve-all-container' do expect(page).to have_content('0/1 discussion resolved') -- cgit v1.2.1 From 78529bbd19252beba7e747f047894033bdc84100 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Wed, 23 Nov 2016 09:36:45 +0100 Subject: Clean build storage after each RSpec test example This prevents subsequent test examples to be affected by previous tests that were executed. --- spec/support/setup_builds_storage.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/support/setup_builds_storage.rb b/spec/support/setup_builds_storage.rb index a4f21e95338..a0a7120ceb5 100644 --- a/spec/support/setup_builds_storage.rb +++ b/spec/support/setup_builds_storage.rb @@ -1,6 +1,6 @@ RSpec.configure do |config| def builds_path - Rails.root.join('tmp/builds') + Rails.root.join('tmp/tests/builds') end config.before(:each) do @@ -9,7 +9,7 @@ RSpec.configure do |config| Settings.gitlab_ci['builds_path'] = builds_path end - config.after(:suite) do + config.after(:each) do Dir[File.join(builds_path, '*')].each do |path| next if File.basename(path) == '.gitkeep' -- cgit v1.2.1 From d917ac0f864a25265407947f334c4f8ee2f5a768 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Wed, 23 Nov 2016 09:51:50 +0100 Subject: Clear Carrierwave upload after each test example --- spec/support/carrierwave.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/support/carrierwave.rb b/spec/support/carrierwave.rb index aa89afd8fb3..72af2c70324 100644 --- a/spec/support/carrierwave.rb +++ b/spec/support/carrierwave.rb @@ -1,7 +1,7 @@ CarrierWave.root = 'tmp/tests/uploads' RSpec.configure do |config| - config.after(:suite) do + config.after(:each) do FileUtils.rm_rf('tmp/tests/uploads') end end -- cgit v1.2.1 From da553a4f915185f5cc45dfbbe5eba316fe968251 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 23 Nov 2016 08:54:15 +0000 Subject: Fixed timeago when creating new discussion --- app/assets/javascripts/notes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 6cb87f9ba81..4149235ee21 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -332,7 +332,7 @@ gl.diffNotesCompileComponents(); } - gl.utils.localTimeAgo($('.js-timeago', note_html), false); + gl.utils.localTimeAgo($('.js-timeago'), false); return this.updateNotesCount(1); }; -- cgit v1.2.1 From cd78e02096ffe210ede99f90d794ce4ba3f727c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Tue, 22 Nov 2016 19:13:41 +0100 Subject: Ensure we sanitize branch names with path-unfriendly characters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- lib/gitlab/ee_compat_check.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb index f4d1505ea91..c8e36d8ff4a 100644 --- a/lib/gitlab/ee_compat_check.rb +++ b/lib/gitlab/ee_compat_check.rb @@ -149,7 +149,7 @@ module Gitlab end def ce_patch_name - @ce_patch_name ||= "#{ce_branch}.patch" + @ce_patch_name ||= patch_name_from_branch(ce_branch) end def ce_patch_full_path @@ -161,13 +161,17 @@ module Gitlab end def ee_patch_name - @ee_patch_name ||= "#{ee_branch}.patch" + @ee_patch_name ||= patch_name_from_branch(ee_branch) end def ee_patch_full_path @ee_patch_full_path ||= patches_dir.join(ee_patch_name) end + def patch_name_from_branch(branch_name) + branch_name.parameterize << '.patch' + end + def step(desc, cmd = nil) puts "\n=> #{desc}\n" -- cgit v1.2.1 From 5ea2628921325bf60abdf9b192abec1f5bcc129e Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Wed, 23 Nov 2016 18:25:32 +0800 Subject: Fix test description to mention latest pipeline, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7333#note_18819886 --- spec/models/commit_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 21590cd4ff1..a16b2e73dd7 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -216,7 +216,7 @@ eos end end - it 'gives compound status' do + it 'gives compound status from latest pipelines' do expect(commit.status).to eq(Ci::Pipeline.latest.status) end end @@ -243,7 +243,7 @@ eos expect(commit.status('fix')).to eq(pipeline_from_fix.status) end - it 'gives compound status if ref is nil' do + it 'gives compound status from latest pipelines if ref is nil' do expect(commit.status(nil)).to eq(Ci::Pipeline.latest.status) end end -- cgit v1.2.1 From 01f588edbcf1f0f0453cae1677c9372c76e25775 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Wed, 23 Nov 2016 11:52:38 +0100 Subject: Remove entire test files directory for test builds --- spec/support/setup_builds_storage.rb | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/spec/support/setup_builds_storage.rb b/spec/support/setup_builds_storage.rb index a0a7120ceb5..fd729434898 100644 --- a/spec/support/setup_builds_storage.rb +++ b/spec/support/setup_builds_storage.rb @@ -3,17 +3,15 @@ RSpec.configure do |config| Rails.root.join('tmp/tests/builds') end + config.before(:suite) do + Settings.gitlab_ci['builds_path'] = builds_path + end + config.before(:each) do FileUtils.mkdir_p(builds_path) - FileUtils.touch(File.join(builds_path, ".gitkeep")) - Settings.gitlab_ci['builds_path'] = builds_path end config.after(:each) do - Dir[File.join(builds_path, '*')].each do |path| - next if File.basename(path) == '.gitkeep' - - FileUtils.rm_rf(path) - end + FileUtils.rm_rf(builds_path) end end -- cgit v1.2.1 From 4262687a978c9521b8025a08543942286989fb11 Mon Sep 17 00:00:00 2001 From: Ahmad Sherif <me@ahmadsherif.com> Date: Mon, 21 Nov 2016 16:38:54 +0200 Subject: Use the minimum access level of group link and group member when inserting authorized project records --- app/models/group.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/group.rb b/app/models/group.rb index 73b0f1c6572..40ba8b6a34d 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -65,7 +65,7 @@ class Group < Namespace def select_for_project_authorization if current_scope.joins_values.include?(:shared_projects) - select("members.user_id, projects.id AS project_id, project_group_links.group_access") + select("members.user_id, projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level") else super end -- cgit v1.2.1 From 74650b280bbe514115e1a085d00cb37c160f2d8e Mon Sep 17 00:00:00 2001 From: Ahmad Sherif <me@ahmadsherif.com> Date: Mon, 21 Nov 2016 21:26:34 +0200 Subject: Change personal projects access level to master in User#project_authorizations_union So that it matches the same access given in Projects::CreateService#after_create_actions --- app/models/user.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index 29fb849940a..223d84ba916 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -926,7 +926,7 @@ class User < ActiveRecord::Base # Returns a union query of projects that the user is authorized to access def project_authorizations_union relations = [ - personal_projects.select("#{id} AS user_id, projects.id AS project_id, #{Gitlab::Access::OWNER} AS access_level"), + personal_projects.select("#{id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"), groups_projects.select_for_project_authorization, projects.select_for_project_authorization, groups.joins(:shared_projects).select_for_project_authorization -- cgit v1.2.1 From 229b7e9a8e10d51eca057e7948a48935011cc51a Mon Sep 17 00:00:00 2001 From: Ahmad Sherif <me@ahmadsherif.com> Date: Mon, 21 Nov 2016 19:21:14 +0200 Subject: Refresh user's authorized projects when one of his memberships are updated --- app/models/member.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/member.rb b/app/models/member.rb index 7be2665bf48..df93aaee847 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -246,7 +246,7 @@ class Member < ActiveRecord::Base end def post_update_hook - # override in subclass + UserProjectAccessChangedService.new(user.id).execute if access_level_changed? end def post_destroy_hook -- cgit v1.2.1 From 916332815e33c655f727a28457f655f6425306ae Mon Sep 17 00:00:00 2001 From: Ahmad Sherif <me@ahmadsherif.com> Date: Fri, 18 Nov 2016 19:15:47 +0200 Subject: Drop Project#authorized_for_user? in favor of ProjectTeam#member? Closes #23938 --- app/models/issue.rb | 2 +- app/models/project.rb | 8 ------ app/models/project_team.rb | 8 ++++-- spec/helpers/members_helper_spec.rb | 4 +-- spec/models/issue_spec.rb | 2 +- spec/models/project_spec.rb | 51 ------------------------------------- spec/models/project_team_spec.rb | 51 +++++++++++++++++++++++++++++++++++++ 7 files changed, 61 insertions(+), 65 deletions(-) diff --git a/app/models/issue.rb b/app/models/issue.rb index 6e8f5d3c422..dd0cb75f9a8 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -93,7 +93,7 @@ class Issue < ActiveRecord::Base # Check if we are scoped to a specific project's issues if owner_project - if owner_project.authorized_for_user?(user, Gitlab::Access::REPORTER) + if owner_project.team.member?(user, Gitlab::Access::REPORTER) # If the project is authorized for the user, they can see all issues in the project return all else diff --git a/app/models/project.rb b/app/models/project.rb index 76c1fc4945d..4408c29a54d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1293,14 +1293,6 @@ class Project < ActiveRecord::Base end end - # Checks if `user` is authorized for this project, with at least the - # `min_access_level` (if given). - def authorized_for_user?(user, min_access_level = nil) - return false unless user - - user.authorized_project?(self, min_access_level) - end - def append_or_update_attribute(name, value) old_values = public_send(name.to_s) diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 65549d8cd08..5845203055a 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -141,8 +141,12 @@ class ProjectTeam max_member_access(user.id) == Gitlab::Access::MASTER end - def member?(user, min_member_access = Gitlab::Access::GUEST) - max_member_access(user.id) >= min_member_access + # Checks if `user` is authorized for this project, with at least the + # `min_access_level` (if given). + def member?(user, min_access_level = Gitlab::Access::GUEST) + return false unless user + + user.authorized_project?(project, min_access_level) end def human_max_access(user_id) diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb index ffca1c94da1..33934cdf8b1 100644 --- a/spec/helpers/members_helper_spec.rb +++ b/spec/helpers/members_helper_spec.rb @@ -10,7 +10,7 @@ describe MembersHelper do end describe '#remove_member_message' do - let(:requester) { build(:user) } + let(:requester) { create(:user) } let(:project) { create(:empty_project, :public, :access_requestable) } let(:project_member) { build(:project_member, project: project) } let(:project_member_invite) { build(:project_member, project: project).tap { |m| m.generate_invite_token! } } @@ -31,7 +31,7 @@ describe MembersHelper do end describe '#remove_member_title' do - let(:requester) { build(:user) } + let(:requester) { create(:user) } let(:project) { create(:empty_project, :public, :access_requestable) } let(:project_member) { build(:project_member, project: project) } let(:project_member_request) { project.request_access(requester) } diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 300425767ed..89e93dce8c5 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -331,7 +331,7 @@ describe Issue, models: true do end context 'with a user' do - let(:user) { build(:user) } + let(:user) { create(:user) } let(:issue) { build(:issue) } it 'returns true when the issue is readable' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 5ba741f40ec..da38254d1bc 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1507,57 +1507,6 @@ describe Project, models: true do end end - describe 'authorized_for_user' do - let(:group) { create(:group) } - let(:developer) { create(:user) } - let(:master) { create(:user) } - let(:personal_project) { create(:project, namespace: developer.namespace) } - let(:group_project) { create(:project, namespace: group) } - let(:members_project) { create(:project) } - let(:shared_project) { create(:project) } - - before do - group.add_master(master) - group.add_developer(developer) - - members_project.team << [developer, :developer] - members_project.team << [master, :master] - - create(:project_group_link, project: shared_project, group: group, group_access: Gitlab::Access::DEVELOPER) - end - - it 'returns false for no user' do - expect(personal_project.authorized_for_user?(nil)).to be(false) - end - - it 'returns true for personal projects of the user' do - expect(personal_project.authorized_for_user?(developer)).to be(true) - end - - it 'returns true for projects of groups the user is a member of' do - expect(group_project.authorized_for_user?(developer)).to be(true) - end - - it 'returns true for projects for which the user is a member of' do - expect(members_project.authorized_for_user?(developer)).to be(true) - end - - it 'returns true for projects shared on a group the user is a member of' do - expect(shared_project.authorized_for_user?(developer)).to be(true) - end - - it 'checks for the correct minimum level access' do - expect(group_project.authorized_for_user?(developer, Gitlab::Access::MASTER)).to be(false) - expect(group_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(true) - expect(members_project.authorized_for_user?(developer, Gitlab::Access::MASTER)).to be(false) - expect(members_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(true) - expect(shared_project.authorized_for_user?(developer, Gitlab::Access::MASTER)).to be(false) - expect(shared_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(false) - expect(shared_project.authorized_for_user?(developer, Gitlab::Access::DEVELOPER)).to be(true) - expect(shared_project.authorized_for_user?(master, Gitlab::Access::DEVELOPER)).to be(true) - end - end - describe 'change_head' do let(:project) { create(:project) } diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index eb6b009c7cf..573da5e50d4 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -261,6 +261,57 @@ describe ProjectTeam, models: true do end end + describe '#member?' do + let(:group) { create(:group) } + let(:developer) { create(:user) } + let(:master) { create(:user) } + let(:personal_project) { create(:project, namespace: developer.namespace) } + let(:group_project) { create(:project, namespace: group) } + let(:members_project) { create(:project) } + let(:shared_project) { create(:project) } + + before do + group.add_master(master) + group.add_developer(developer) + + members_project.team << [developer, :developer] + members_project.team << [master, :master] + + create(:project_group_link, project: shared_project, group: group) + end + + it 'returns false for no user' do + expect(personal_project.team.member?(nil)).to be(false) + end + + it 'returns true for personal projects of the user' do + expect(personal_project.team.member?(developer)).to be(true) + end + + it 'returns true for projects of groups the user is a member of' do + expect(group_project.team.member?(developer)).to be(true) + end + + it 'returns true for projects for which the user is a member of' do + expect(members_project.team.member?(developer)).to be(true) + end + + it 'returns true for projects shared on a group the user is a member of' do + expect(shared_project.team.member?(developer)).to be(true) + end + + it 'checks for the correct minimum level access' do + expect(group_project.team.member?(developer, Gitlab::Access::MASTER)).to be(false) + expect(group_project.team.member?(master, Gitlab::Access::MASTER)).to be(true) + expect(members_project.team.member?(developer, Gitlab::Access::MASTER)).to be(false) + expect(members_project.team.member?(master, Gitlab::Access::MASTER)).to be(true) + expect(shared_project.team.member?(developer, Gitlab::Access::MASTER)).to be(false) + expect(shared_project.team.member?(master, Gitlab::Access::MASTER)).to be(false) + expect(shared_project.team.member?(developer, Gitlab::Access::DEVELOPER)).to be(true) + expect(shared_project.team.member?(master, Gitlab::Access::DEVELOPER)).to be(true) + end + end + shared_examples_for "#max_member_access_for_users" do |enable_request_store| describe "#max_member_access_for_users" do before do -- cgit v1.2.1 From 0f3c3a1c57beb8269f97e408f19e76ba77aac99c Mon Sep 17 00:00:00 2001 From: Ahmad Sherif <me@ahmadsherif.com> Date: Mon, 21 Nov 2016 15:33:58 +0200 Subject: Update user's authorized projects if project is allowed to share with group --- app/models/group.rb | 4 +++- app/models/namespace.rb | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/models/group.rb b/app/models/group.rb index 40ba8b6a34d..4248e1162d8 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -65,7 +65,9 @@ class Group < Namespace def select_for_project_authorization if current_scope.joins_values.include?(:shared_projects) - select("members.user_id, projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level") + joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id') + .where('project_namespace.share_with_group_lock = ?', false) + .select("members.user_id, projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level") else super end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index b67049f0f55..99da26a89fb 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -27,6 +27,7 @@ class Namespace < ActiveRecord::Base delegate :name, to: :owner, allow_nil: true, prefix: true after_update :move_dir, if: :path_changed? + after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') } # Save the storage paths before the projects are destroyed to use them on after destroy before_destroy(prepend: true) { @old_repository_storage_paths = repository_storage_paths } @@ -175,4 +176,11 @@ class Namespace < ActiveRecord::Base end end end + + def refresh_access_of_projects_invited_groups + Group. + joins(project_group_links: :project). + where(projects: { namespace_id: id }). + find_each(&:refresh_members_authorized_projects) + end end -- cgit v1.2.1 From 747959a832bddcdffba3e7c687b66904e8c6dbf9 Mon Sep 17 00:00:00 2001 From: Ahmad Sherif <me@ahmadsherif.com> Date: Mon, 21 Nov 2016 15:36:40 +0200 Subject: Update ProjectTeam#max_member_access_for_user_ids to use project authorizations --- app/models/project.rb | 1 + app/models/project_team.rb | 49 ++++------------------------ spec/lib/gitlab/import_export/all_models.yml | 2 ++ spec/models/project_team_spec.rb | 12 +++---- 4 files changed, 15 insertions(+), 49 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 4408c29a54d..5d475c25e4a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -126,6 +126,7 @@ class Project < ActiveRecord::Base has_many :hooks, dependent: :destroy, class_name: 'ProjectHook' has_many :protected_branches, dependent: :destroy + has_many :project_authorizations, dependent: :destroy has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source alias_method :members, :project_members has_many :users, through: :project_members diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 5845203055a..1d0e97396f3 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -169,54 +169,21 @@ class ProjectTeam # Lookup only the IDs we need user_ids = user_ids - access.keys + users_access = project.project_authorizations. + where(user: user_ids). + group(:user_id). + maximum(:access_level) - if user_ids.present? - user_ids.each { |id| access[id] = Gitlab::Access::NO_ACCESS } - - member_access = project.members.access_for_user_ids(user_ids) - merge_max!(access, member_access) - - if group - group_access = group.members.access_for_user_ids(user_ids) - merge_max!(access, group_access) - end - - # Each group produces a list of maximum access level per user. We take the - # max of the values produced by each group. - if project_shared_with_group? - project.project_group_links.each do |group_link| - invited_access = max_invited_level_for_users(group_link, user_ids) - merge_max!(access, invited_access) - end - end - end - + access.merge!(users_access) access end def max_member_access(user_id) - max_member_access_for_user_ids([user_id])[user_id] + max_member_access_for_user_ids([user_id])[user_id] || Gitlab::Access::NO_ACCESS end private - # For a given group, return the maximum access level for the user. This is the min of - # the invited access level of the group and the access level of the user within the group. - # For example, if the group has been given DEVELOPER access but the member has MASTER access, - # the user should receive only DEVELOPER access. - def max_invited_level_for_users(group_link, user_ids) - invited_group = group_link.group - capped_access_level = group_link.group_access - access = invited_group.group_members.access_for_user_ids(user_ids) - - # If the user is not in the list, assume he/she does not have access - missing_users = user_ids - access.keys - missing_users.each { |id| access[id] = Gitlab::Access::NO_ACCESS } - - # Cap the maximum access by the invited level access - access.each { |key, value| access[key] = [value, capped_access_level].min } - end - def fetch_members(level = nil) project_members = project.members group_members = group ? group.members : [] @@ -240,10 +207,6 @@ class ProjectTeam project.group end - def merge_max!(first_hash, second_hash) - first_hash.merge!(second_hash) { |_key, old, new| old > new ? old : new } - end - def project_shared_with_group? project.invited_groups.any? && project.allowed_to_share_with_group? end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index fe3c39e38db..7e00e214c6e 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -186,6 +186,8 @@ project: - environments - deployments - project_feature +- authorized_users +- project_authorizations award_emoji: - awardable - user diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 573da5e50d4..700776be931 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -37,7 +37,7 @@ describe ProjectTeam, models: true do context 'group project' do let(:group) { create(:group) } - let(:project) { create(:empty_project, group: group) } + let!(:project) { create(:empty_project, group: group) } before do group.add_master(master) @@ -178,9 +178,9 @@ describe ProjectTeam, models: true do it 'returns Master role' do user = create(:user) group = create(:group) - group.add_master(user) + project = create(:empty_project, namespace: group) - project = build_stubbed(:empty_project, namespace: group) + group.add_master(user) expect(project.team.human_max_access(user.id)).to eq 'Master' end @@ -188,9 +188,9 @@ describe ProjectTeam, models: true do it 'returns Owner role' do user = create(:user) group = create(:group) - group.add_owner(user) + project = create(:empty_project, namespace: group) - project = build_stubbed(:empty_project, namespace: group) + group.add_owner(user) expect(project.team.human_max_access(user.id)).to eq 'Owner' end @@ -244,7 +244,7 @@ describe ProjectTeam, models: true do context 'group project' do let(:group) { create(:group, :access_requestable) } - let(:project) { create(:empty_project, group: group) } + let!(:project) { create(:empty_project, group: group) } before do group.add_master(master) -- cgit v1.2.1 From 2ea5ef0ba4ee00b5551b88a6b9a68e045bf4b3f4 Mon Sep 17 00:00:00 2001 From: Ahmad Sherif <me@ahmadsherif.com> Date: Mon, 21 Nov 2016 16:26:12 +0200 Subject: Update ProjectTeam#fetch_members to use project authorizations --- app/models/project.rb | 1 + app/models/project_team.rb | 60 +++------------------- .../fix-drop-project-authorized-for-user.yml | 4 ++ db/fixtures/development/06_teams.rb | 33 +++++++----- spec/models/project_team_spec.rb | 2 +- 5 files changed, 32 insertions(+), 68 deletions(-) create mode 100644 changelogs/unreleased/fix-drop-project-authorized-for-user.yml diff --git a/app/models/project.rb b/app/models/project.rb index 5d475c25e4a..bd9fcb2f3b7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -127,6 +127,7 @@ class Project < ActiveRecord::Base has_many :protected_branches, dependent: :destroy has_many :project_authorizations, dependent: :destroy + has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User' has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source alias_method :members, :project_members has_many :users, through: :project_members diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 1d0e97396f3..8a53e974b6f 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -80,19 +80,19 @@ class ProjectTeam alias_method :users, :members def guests - @guests ||= fetch_members(:guests) + @guests ||= fetch_members(Gitlab::Access::GUEST) end def reporters - @reporters ||= fetch_members(:reporters) + @reporters ||= fetch_members(Gitlab::Access::REPORTER) end def developers - @developers ||= fetch_members(:developers) + @developers ||= fetch_members(Gitlab::Access::DEVELOPER) end def masters - @masters ||= fetch_members(:masters) + @masters ||= fetch_members(Gitlab::Access::MASTER) end def import(source_project, current_user = nil) @@ -185,59 +185,13 @@ class ProjectTeam private def fetch_members(level = nil) - project_members = project.members - group_members = group ? group.members : [] + members = project.authorized_users + members = members.where(project_authorizations: { access_level: level }) if level - if level - project_members = project_members.public_send(level) - group_members = group_members.public_send(level) if group - end - - user_ids = project_members.pluck(:user_id) - - invited_members = fetch_invited_members(level) - user_ids.push(*invited_members.map(&:user_id)) if invited_members.any? - - user_ids.push(*group_members.pluck(:user_id)) if group - - User.where(id: user_ids) + members end def group project.group end - - def project_shared_with_group? - project.invited_groups.any? && project.allowed_to_share_with_group? - end - - def fetch_invited_members(level = nil) - invited_members = [] - - return invited_members unless project_shared_with_group? - - project.project_group_links.includes(group: [:group_members]).each do |link| - invited_group_members = link.group.members - - if level - numeric_level = GroupMember.access_level_roles[level.to_s.singularize.titleize] - - # If we're asked for a level that's higher than the group's access, - # there's nothing left to do - next if numeric_level > link.group_access - - # Make sure we include everyone _above_ the requested level as well - invited_group_members = - if numeric_level == link.group_access - invited_group_members.where("access_level >= ?", link.group_access) - else - invited_group_members.public_send(level) - end - end - - invited_members << invited_group_members - end - - invited_members.flatten.compact - end end diff --git a/changelogs/unreleased/fix-drop-project-authorized-for-user.yml b/changelogs/unreleased/fix-drop-project-authorized-for-user.yml new file mode 100644 index 00000000000..0d11969575a --- /dev/null +++ b/changelogs/unreleased/fix-drop-project-authorized-for-user.yml @@ -0,0 +1,4 @@ +--- +title: Use authorized projects in ProjectTeam +merge_request: +author: diff --git a/db/fixtures/development/06_teams.rb b/db/fixtures/development/06_teams.rb index 9739a5ac8d5..04c3690e152 100644 --- a/db/fixtures/development/06_teams.rb +++ b/db/fixtures/development/06_teams.rb @@ -1,20 +1,25 @@ -Gitlab::Seeder.quiet do - Group.all.each do |group| - User.all.sample(4).each do |user| - if group.add_user(user, Gitlab::Access.values.sample).persisted? - print '.' - else - print 'F' +require 'sidekiq/testing' +require './db/fixtures/support/serialized_transaction' + +Sidekiq::Testing.inline! do + Gitlab::Seeder.quiet do + Group.all.each do |group| + User.all.sample(4).each do |user| + if group.add_user(user, Gitlab::Access.values.sample).persisted? + print '.' + else + print 'F' + end end end - end - Project.all.each do |project| - User.all.sample(4).each do |user| - if project.team << [user, Gitlab::Access.values.sample] - print '.' - else - print 'F' + Project.all.each do |project| + User.all.sample(4).each do |user| + if project.team << [user, Gitlab::Access.values.sample] + print '.' + else + print 'F' + end end end end diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 700776be931..0475cecaa2d 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -118,7 +118,7 @@ describe ProjectTeam, models: true do context 'group project' do let(:group) { create(:group) } - let(:project) { create(:empty_project, group: group) } + let!(:project) { create(:empty_project, group: group) } it 'returns project members' do group_member = create(:group_member, group: group) -- cgit v1.2.1 From 3789cfe056c1d8a5fb91267cc2b1dd0f9f5902a9 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Tue, 15 Nov 2016 19:48:30 +0200 Subject: Add a starting date to milestones --- app/assets/javascripts/due_date_select.js.es6 | 22 ++++------- app/assets/stylesheets/framework/issue_box.scss | 4 ++ app/controllers/groups/milestones_controller.rb | 2 +- app/controllers/projects/milestones_controller.rb | 2 +- app/helpers/issues_helper.rb | 2 + app/helpers/milestones_helper.rb | 24 ++++++++++++ app/models/concerns/milestoneish.rb | 24 ++++++++++++ app/models/global_milestone.rb | 33 +++++------------ app/models/milestone.rb | 25 ++++--------- app/views/groups/milestones/new.html.haml | 13 +------ app/views/projects/milestones/_form.html.haml | 7 +--- app/views/projects/milestones/show.html.haml | 6 ++- app/views/shared/_milestone_expired.html.haml | 6 ++- app/views/shared/milestones/_form_dates.html.haml | 15 ++++++++ app/views/shared/milestones/_top.html.haml | 4 +- changelogs/unreleased/milestone_start_date.yml | 4 ++ .../20161115173905_add_start_date_to_milestones.rb | 12 ++++++ db/schema.rb | 1 + doc/api/milestones.md | 3 ++ lib/api/entities.rb | 1 + lib/api/milestones.rb | 3 +- spec/features/milestone_spec.rb | 5 +++ spec/helpers/milestones_helper_spec.rb | 19 ++++++++++ .../gitlab/import_export/safe_model_attributes.yml | 1 + spec/models/concerns/milestoneish_spec.rb | 20 ++++++++++ spec/models/milestone_spec.rb | 43 +++++++++++++--------- spec/requests/api/milestones_spec.rb | 5 ++- 27 files changed, 205 insertions(+), 101 deletions(-) create mode 100644 app/views/shared/milestones/_form_dates.html.haml create mode 100644 changelogs/unreleased/milestone_start_date.yml create mode 100644 db/migrate/20161115173905_add_start_date_to_milestones.rb diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6 index fd7f961aab9..e84f5ac9183 100644 --- a/app/assets/javascripts/due_date_select.js.es6 +++ b/app/assets/javascripts/due_date_select.js.es6 @@ -145,25 +145,19 @@ class DueDateSelectors { constructor() { - this.initMilestoneDueDate(); + this.initMilestoneDatePicker(); this.initIssuableSelect(); } - initMilestoneDueDate() { - const $datePicker = $('.datepicker'); + initMilestoneDatePicker() { + $('.datepicker').datepicker({ + dateFormat: 'yy-mm-dd' + }); - if ($datePicker.length) { - const $dueDate = $('#milestone_due_date'); - $datePicker.datepicker({ - dateFormat: 'yy-mm-dd', - onSelect: (dateText, inst) => { - $dueDate.val(dateText); - } - }).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val())); - } - $('.js-clear-due-date').on('click', (e) => { + $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => { e.preventDefault(); - $.datepicker._clearDate($datePicker); + const datepicker = $(e.target).siblings('.datepicker'); + $.datepicker._clearDate(datepicker); }); } diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index ba3930e03bd..ff6f316d576 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -39,4 +39,8 @@ &.status-box-expired { background: #cea61b; } + + &.status-box-upcoming { + background: #8f8f8f; + } } diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 506484932cc..24ec4eec3f2 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -67,7 +67,7 @@ class Groups::MilestonesController < Groups::ApplicationController end def milestone_params - params.require(:milestone).permit(:title, :description, :due_date, :state_event) + params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event) end def milestone_path(title) diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index ff63f22cb5b..be52b0fa7cf 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -112,6 +112,6 @@ class Projects::MilestonesController < Projects::ApplicationController end def milestone_params - params.require(:milestone).permit(:title, :description, :due_date, :state_event) + params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event) end end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 1644c346dd8..a8a49e43b05 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -64,6 +64,8 @@ module IssuesHelper 'status-box-merged' elsif item.closed? 'status-box-closed' + elsif item.respond_to?(:upcoming?) && item.upcoming? + 'status-box-upcoming' else 'status-box-open' end diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 83a2a4ad3ec..729928ce1dd 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -86,6 +86,30 @@ module MilestonesHelper days = milestone.remaining_days content = content_tag(:strong, days) content << " #{'day'.pluralize(days)} remaining" + elsif milestone.upcoming? + content_tag(:strong, 'Upcoming') + elsif milestone.start_date && milestone.start_date.past? + days = milestone.elapsed_days + content = content_tag(:strong, days) + content << " #{'day'.pluralize(days)} elapsed" + end + end + + def milestone_date_range(milestone) + if milestone.start_date && milestone.due_date + "#{milestone.start_date.to_s(:medium)} - #{milestone.due_date.to_s(:medium)}" + elsif milestone.due_date + if milestone.due_date.past? + "expired on #{milestone.due_date.to_s(:medium)}" + else + "expires on #{milestone.due_date.to_s(:medium)}" + end + elsif milestone.start_date + if milestone.start_date.past? + "started on #{milestone.start_date.to_s(:medium)}" + else + "starts on #{milestone.start_date.to_s(:medium)}" + end end end end diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index 7bcc78247ba..e65fc9eaa09 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -23,7 +23,31 @@ module Milestoneish (due_date - Date.today).to_i end + def elapsed_days + return 0 if !start_date || start_date.future? + + (Date.today - start_date).to_i + end + def issues_visible_to_user(user = nil) issues.visible_to_user(user) end + + def upcoming? + start_date && start_date.future? + end + + def expires_at + if due_date + if due_date.past? + "expired on #{due_date.to_s(:medium)}" + else + "expires on #{due_date.to_s(:medium)}" + end + end + end + + def expired? + due_date && due_date.past? + end end diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index cde4a568577..b01607dcda9 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -28,26 +28,16 @@ class GlobalMilestone @title.to_slug.normalize.to_s end - def expired? - if due_date - due_date.past? - else - false - end - end - def projects @projects ||= Project.for_milestones(milestones.select(:id)) end def state - state = milestones.map { |milestone| milestone.state } - - if state.count('closed') == state.size - 'closed' - else - 'active' + milestones.each do |milestone| + return 'active' if milestone.state != 'closed' end + + 'closed' end def active? @@ -81,18 +71,15 @@ class GlobalMilestone @due_date = if @milestones.all? { |x| x.due_date == @milestones.first.due_date } @milestones.first.due_date - else - nil end end - def expires_at - if due_date - if due_date.past? - "expired on #{due_date.to_s(:medium)}" - else - "expires on #{due_date.to_s(:medium)}" + def start_date + return @start_date if defined?(@start_date) + + @start_date = + if @milestones.all? { |x| x.start_date == @milestones.first.start_date } + @milestones.first.start_date end - end end end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 23aecbfa3a6..c774e69080c 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -29,6 +29,7 @@ class Milestone < ActiveRecord::Base validates :title, presence: true, uniqueness: { scope: :project_id } validates :project, presence: true + validate :start_date_should_be_less_than_due_date, if: Proc.new { |m| m.start_date.present? && m.due_date.present? } strip_attributes :title @@ -131,24 +132,6 @@ class Milestone < ActiveRecord::Base self.title end - def expired? - if due_date - due_date.past? - else - false - end - end - - def expires_at - if due_date - if due_date.past? - "expired on #{due_date.to_s(:medium)}" - else - "expires on #{due_date.to_s(:medium)}" - end - end - end - def can_be_closed? active? && issues.opened.count.zero? end @@ -212,4 +195,10 @@ class Milestone < ActiveRecord::Base def sanitize_title(value) CGI.unescape_html(Sanitize.clean(value.to_s)) end + + def start_date_should_be_less_than_due_date + if due_date <= start_date + errors.add(:start_date, "Can't be greater than due date") + end + end end diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml index 0dfaf743992..63cadfca530 100644 --- a/app/views/groups/milestones/new.html.haml +++ b/app/views/groups/milestones/new.html.haml @@ -36,19 +36,8 @@ = f.collection_select :project_ids, @group.projects.non_archived, :id, :name, { selected: @group.projects.non_archived.pluck(:id) }, required: true, multiple: true, class: 'select2' - .col-md-6 - .form-group - = f.label :due_date, "Due Date", class: "control-label" - .col-sm-10 - = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date" + = render "shared/milestones/form_dates", f: f .form-actions = f.submit 'Create Milestone', class: "btn-create btn" = link_to "Cancel", group_milestones_path(@group), class: "btn btn-cancel" - - -:javascript - $(".datepicker").datepicker({ - dateFormat: "yy-mm-dd", - onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) } - }).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val())); diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index cbf1ba04170..513710e8e66 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -14,12 +14,7 @@ = render 'projects/notes/hints' .clearfix .error-alert - .col-md-6 - .form-group - = f.label :due_date, "Due Date", class: "control-label" - .col-sm-10 - = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date" - %a.inline.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date + = render "shared/milestones/form_dates", f: f .form-actions - if @milestone.new_record? diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index e01aca3dda6..c3a6096aa54 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -10,15 +10,17 @@ Closed - elsif @milestone.expired? Past due + - elsif @milestone.upcoming? + Upcoming - else Open .header-text-content %span.identifier Milestone ##{@milestone.iid} - - if @milestone.expires_at + - if @milestone.due_date || @milestone.start_date %span.creator · - = @milestone.expires_at + = milestone_date_range(@milestone) .milestone-buttons - if can?(current_user, :admin_milestone, @project) - if @milestone.active? diff --git a/app/views/shared/_milestone_expired.html.haml b/app/views/shared/_milestone_expired.html.haml index b8eef15fbec..5e9007aaaac 100644 --- a/app/views/shared/_milestone_expired.html.haml +++ b/app/views/shared/_milestone_expired.html.haml @@ -1,5 +1,7 @@ - if milestone.expired? and not milestone.closed? %span.cred (Expired) -- if milestone.expires_at +- if milestone.upcoming? + %span.clgray (Upcoming) +- if milestone.due_date || milestone.start_date %span - = milestone.expires_at + = milestone_date_range(milestone) diff --git a/app/views/shared/milestones/_form_dates.html.haml b/app/views/shared/milestones/_form_dates.html.haml new file mode 100644 index 00000000000..748b10a1298 --- /dev/null +++ b/app/views/shared/milestones/_form_dates.html.haml @@ -0,0 +1,15 @@ +.col-md-6 + .form-group + = f.label :start_date, "Start Date", class: "control-label" + .col-sm-10 + = f.text_field :start_date, class: "datepicker form-control", placeholder: "Select start date" + %a.inline.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date +.col-md-6 + .form-group + = f.label :due_date, "Due Date", class: "control-label" + .col-sm-10 + = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date" + %a.inline.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date + +:javascript + new gl.DueDateSelectors(); diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml index 548215243db..497446c1ef3 100644 --- a/app/views/shared/milestones/_top.html.haml +++ b/app/views/shared/milestones/_top.html.haml @@ -12,10 +12,10 @@ Open %span.identifier Milestone #{milestone.title} - - if milestone.expires_at + - if milestone.due_date || milestone.start_date %span.creator · - = milestone.expires_at + = milestone_date_range(milestone) - if group .pull-right - if can?(current_user, :admin_milestones, group) diff --git a/changelogs/unreleased/milestone_start_date.yml b/changelogs/unreleased/milestone_start_date.yml new file mode 100644 index 00000000000..39ac1344329 --- /dev/null +++ b/changelogs/unreleased/milestone_start_date.yml @@ -0,0 +1,4 @@ +--- +title: Add a starting date to milestones +merge_request: +author: diff --git a/db/migrate/20161115173905_add_start_date_to_milestones.rb b/db/migrate/20161115173905_add_start_date_to_milestones.rb new file mode 100644 index 00000000000..413733b8db7 --- /dev/null +++ b/db/migrate/20161115173905_add_start_date_to_milestones.rb @@ -0,0 +1,12 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddStartDateToMilestones < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :milestones, :start_date, :date + end +end diff --git a/db/schema.rb b/db/schema.rb index 6b28e80f01d..b3c49b52597 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -720,6 +720,7 @@ ActiveRecord::Schema.define(version: 20161118183841) do t.integer "iid" t.text "title_html" t.text "description_html" + t.date "start_date" end add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} diff --git a/doc/api/milestones.md b/doc/api/milestones.md index 28e4b3105a6..12497acff98 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -35,6 +35,7 @@ Example Response: "title": "10.0", "description": "Version", "due_date": "2013-11-29", + "start_date": "2013-11-10", "state": "active", "updated_at": "2013-10-02T09:24:18Z", "created_at": "2013-10-02T09:24:18Z" @@ -70,6 +71,7 @@ Parameters: - `title` (required) - The title of an milestone - `description` (optional) - The description of the milestone - `due_date` (optional) - The due date of the milestone +- `start_date` (optional) - The start date of the milestone ## Edit milestone @@ -86,6 +88,7 @@ Parameters: - `title` (optional) - The title of a milestone - `description` (optional) - The description of a milestone - `due_date` (optional) - The due date of the milestone +- `start_date` (optional) - The start date of the milestone - `state_event` (optional) - The state event of the milestone (close|activate) ## Get all issues assigned to a single milestone diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 33cb6fd3704..7a724487e02 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -210,6 +210,7 @@ module API class Milestone < ProjectEntity expose :due_date + expose :start_date end class Issue < ProjectEntity diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index 29bf73934d2..50d6109be3d 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -14,7 +14,8 @@ module API params :optional_params do optional :description, type: String, desc: 'The description of the milestone' - optional :due_date, type: String, desc: 'The due date of the milestone' + optional :due_date, type: String, desc: 'The due date of the milestone. The ISO 8601 date format (%Y-%m-%d)' + optional :start_date, type: String, desc: 'The start date of the milestone. The ISO 8601 date format (%Y-%m-%d)' end end diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb index b8c838bf7ab..a2e40546588 100644 --- a/spec/features/milestone_spec.rb +++ b/spec/features/milestone_spec.rb @@ -14,12 +14,17 @@ feature 'Milestone', feature: true do feature 'Create a milestone' do scenario 'shows an informative message for a new milestone' do visit new_namespace_project_milestone_path(project.namespace, project) + page.within '.milestone-form' do fill_in "milestone_title", with: '8.7' + fill_in "milestone_start_date", with: '2016-11-16' + fill_in "milestone_due_date", with: '2016-12-16' end + find('input[name="commit"]').click expect(find('.alert-success')).to have_content('Assign some issues to this milestone.') + expect(page).to have_content('Nov 16, 2016 - Dec 16, 2016') end end diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb index 28c2268f8d0..ea744dbb629 100644 --- a/spec/helpers/milestones_helper_spec.rb +++ b/spec/helpers/milestones_helper_spec.rb @@ -1,6 +1,25 @@ require 'spec_helper' describe MilestonesHelper do + describe "#milestone_date_range" do + def result_for(*args) + milestone_date_range(build(:milestone, *args)) + end + + let(:yesterday) { Date.yesterday } + let(:tomorrow) { yesterday + 2 } + let(:format) { '%b %-d, %Y' } + let(:yesterday_formatted) { yesterday.strftime(format) } + let(:tomorrow_formatted) { tomorrow.strftime(format) } + + it { expect(result_for(due_date: nil, start_date: nil)).to be_nil } + it { expect(result_for(due_date: tomorrow)).to eq("expires on #{tomorrow_formatted}") } + it { expect(result_for(due_date: yesterday)).to eq("expired on #{yesterday_formatted}") } + it { expect(result_for(start_date: tomorrow)).to eq("starts on #{tomorrow_formatted}") } + it { expect(result_for(start_date: yesterday)).to eq("started on #{yesterday_formatted}") } + it { expect(result_for(start_date: yesterday, due_date: tomorrow)).to eq("#{yesterday_formatted} - #{tomorrow_formatted}") } + end + describe '#milestone_counts' do let(:project) { FactoryGirl.create(:project) } let(:counts) { helper.milestone_counts(project.milestones) } diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index d6807941b31..78d6b2c5032 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -78,6 +78,7 @@ Milestone: - project_id - description - due_date +- start_date - created_at - updated_at - state diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb index b7e973798a3..0e097559b59 100644 --- a/spec/models/concerns/milestoneish_spec.rb +++ b/spec/models/concerns/milestoneish_spec.rb @@ -115,4 +115,24 @@ describe Milestone, 'Milestoneish' do expect(milestone.percent_complete(admin)).to eq 60 end end + + describe '#elapsed_days' do + it 'shows 0 if no start_date set' do + milestone = build(:milestone) + + expect(milestone.elapsed_days).to eq(0) + end + + it 'shows 0 if start_date is a future' do + milestone = build(:milestone, start_date: Time.now + 2.days) + + expect(milestone.elapsed_days).to eq(0) + end + + it 'shows correct amount of days' do + milestone = build(:milestone, start_date: Time.now - 2.days) + + expect(milestone.elapsed_days).to eq(2) + end + end end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 33fe22dd98c..a4bfe851dfb 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -1,11 +1,6 @@ require 'spec_helper' describe Milestone, models: true do - describe "Associations" do - it { is_expected.to belong_to(:project) } - it { is_expected.to have_many(:issues) } - end - describe "Validation" do before do allow(subject).to receive(:set_iid).and_return(false) @@ -13,6 +8,20 @@ describe Milestone, models: true do it { is_expected.to validate_presence_of(:title) } it { is_expected.to validate_presence_of(:project) } + + describe 'start_date' do + it 'adds an error when start_date is greated then due_date' do + milestone = build(:milestone, start_date: Date.tomorrow, due_date: Date.yesterday) + + expect(milestone).not_to be_valid + expect(milestone.errors[:start_date]).to include("Can't be greater than due date") + end + end + end + + describe "Associations" do + it { is_expected.to belong_to(:project) } + it { is_expected.to have_many(:issues) } end let(:milestone) { create(:milestone) } @@ -58,18 +67,6 @@ describe Milestone, models: true do end end - describe "#expires_at" do - it "is nil when due_date is unset" do - milestone.update_attributes(due_date: nil) - expect(milestone.expires_at).to be_nil - end - - it "is not nil when due_date is set" do - milestone.update_attributes(due_date: Date.tomorrow) - expect(milestone.expires_at).to be_present - end - end - describe '#expired?' do context "expired" do before do @@ -88,6 +85,18 @@ describe Milestone, models: true do end end + describe '#upcoming?' do + it 'returns true' do + milestone = build(:milestone, start_date: Time.now + 1.month) + expect(milestone.upcoming?).to be_truthy + end + + it 'returns false' do + milestone = build(:milestone, start_date: Date.today.prev_year) + expect(milestone.upcoming?).to be_falsey + end + end + describe '#percent_complete' do before do allow(milestone).to receive_messages( diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index 5d7b39e71b8..b0946a838a1 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -92,13 +92,14 @@ describe API::API, api: true do expect(json_response['description']).to be_nil end - it 'creates a new project milestone with description and due date' do + it 'creates a new project milestone with description and dates' do post api("/projects/#{project.id}/milestones", user), - title: 'new milestone', description: 'release', due_date: '2013-03-02' + title: 'new milestone', description: 'release', due_date: '2013-03-02', start_date: '2013-02-02' expect(response).to have_http_status(201) expect(json_response['description']).to eq('release') expect(json_response['due_date']).to eq('2013-03-02') + expect(json_response['start_date']).to eq('2013-02-02') end it 'returns a 400 error if title is missing' do -- cgit v1.2.1 From bd3a544ab0417b758cf5e8e5bcd4ae9c0fed1bae Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Wed, 23 Nov 2016 19:43:56 +0800 Subject: Wrap against 80 chars and rename failed_or_canceled to better reflect the status. Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7508#note_18858398 --- app/models/ci/pipeline.rb | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index c0f2c8ba787..4294a10e9e3 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -169,17 +169,19 @@ module Ci end def cancel_running - Gitlab::OptimisticLocking.retry_lock(statuses.cancelable) do |cancelable| - cancelable.each(&:cancel) - end + Gitlab::OptimisticLocking.retry_lock( + statuses.cancelable) do |cancelable| + cancelable.each(&:cancel) + end end def retry_failed(user) - Gitlab::OptimisticLocking.retry_lock(builds.latest.failed_or_canceled) do |failed| - failed.select(&:retryable?).each do |build| - Ci::Build.retry(build, user) + Gitlab::OptimisticLocking.retry_lock( + builds.latest.failed_or_canceled) do |failed_or_canceled| + failed_or_canceled.select(&:retryable?).each do |build| + Ci::Build.retry(build, user) + end end - end end def mark_as_processable_after_stage(stage_idx) -- cgit v1.2.1 From eff1b05ab1d50895be668be12de8239def648d97 Mon Sep 17 00:00:00 2001 From: Robert Schilling <rschilling@student.tugraz.at> Date: Tue, 22 Nov 2016 11:23:41 +0100 Subject: API: Add endpoint to delete a group share --- changelogs/unreleased/api-delete-group-share.yml | 4 ++++ doc/api/projects.md | 19 +++++++++++++++ lib/api/projects.rb | 13 ++++++++++ spec/requests/api/projects_spec.rb | 30 ++++++++++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 changelogs/unreleased/api-delete-group-share.yml diff --git a/changelogs/unreleased/api-delete-group-share.yml b/changelogs/unreleased/api-delete-group-share.yml new file mode 100644 index 00000000000..26cfb35bba3 --- /dev/null +++ b/changelogs/unreleased/api-delete-group-share.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Add ability to unshare a project from a group' +merge_request: 7662 +author: Robert Schilling diff --git a/doc/api/projects.md b/doc/api/projects.md index 467a880ac13..de5d3b07c21 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1074,6 +1074,25 @@ Parameters: | `group_access` | integer | yes | The permissions level to grant the group | | `expires_at` | string | no | Share expiration date in ISO 8601 format: 2016-09-26 | +### Delete a shared project link within a group + +Unshare the project from the group. Returns `204` and no content on success. + +``` +DELETE /projects/:id/share/:group_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME | +| `group_id` | integer | yes | The ID of the group | + +```bash +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/share/17 +``` + ## Hooks Also called Project Hooks and Webhooks. diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 6b856128c2e..ddfde178d30 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -438,6 +438,19 @@ module API end end + params do + requires :group_id, type: Integer, desc: 'The ID of the group' + end + delete ":id/share/:group_id" do + authorize! :admin_project, user_project + + link = user_project.project_group_links.find_by(group_id: params[:group_id]) + not_found!('Group Link') unless link + + link.destroy + no_content! + end + # Upload a file # # Parameters: diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index f020d471422..e53ee2a4e76 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -908,6 +908,36 @@ describe API::API, api: true do end end + describe 'DELETE /projects/:id/share/:group_id' do + it 'returns 204 when deleting a group share' do + group = create(:group, :public) + create(:project_group_link, group: group, project: project) + + delete api("/projects/#{project.id}/share/#{group.id}", user) + + expect(response).to have_http_status(204) + expect(project.project_group_links).to be_empty + end + + it 'returns a 400 when group id is not an integer' do + delete api("/projects/#{project.id}/share/foo", user) + + expect(response).to have_http_status(400) + end + + it 'returns a 404 error when group link does not exist' do + delete api("/projects/#{project.id}/share/1234", user) + + expect(response).to have_http_status(404) + end + + it 'returns a 404 error when project does not exist' do + delete api("/projects/123/share/1234", user) + + expect(response).to have_http_status(404) + end + end + describe 'GET /projects/search/:query' do let!(:query) { 'query'} let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) } -- cgit v1.2.1 From f2a0c6f6bf768db8837283ad65fe6790b9105d26 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Wed, 23 Nov 2016 20:00:58 +0800 Subject: Correctly determine mergeability of MR with no discussions --- app/models/merge_request.rb | 6 ++- ...24863-mrs-without-discussions-are-mergeable.yml | 4 ++ spec/models/merge_request_spec.rb | 54 ++++++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/24863-mrs-without-discussions-are-mergeable.yml diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 6c3c093d084..fdf54cc8a7e 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -494,10 +494,14 @@ class MergeRequest < ActiveRecord::Base discussions_resolvable? && diff_discussions.none?(&:to_be_resolved?) end + def discussions_to_be_resolved? + discussions_resolvable? && !discussions_resolved? + end + def mergeable_discussions_state? return true unless project.only_allow_merge_if_all_discussions_are_resolved? - discussions_resolved? + !discussions_to_be_resolved? end def hook_attrs diff --git a/changelogs/unreleased/24863-mrs-without-discussions-are-mergeable.yml b/changelogs/unreleased/24863-mrs-without-discussions-are-mergeable.yml new file mode 100644 index 00000000000..9bdb9411135 --- /dev/null +++ b/changelogs/unreleased/24863-mrs-without-discussions-are-mergeable.yml @@ -0,0 +1,4 @@ +--- +title: Correctly determine mergeability of MR with no discussions +merge_request: +author: diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 0b4277b1edd..58ccd056328 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -937,6 +937,16 @@ describe MergeRequest, models: true do expect(merge_request.mergeable_discussions_state?).to be_falsey end end + + context 'with no discussions' do + before do + merge_request.notes.destroy_all + end + + it 'returns true' do + expect(merge_request.mergeable_discussions_state?).to be_truthy + end + end end context 'when project.only_allow_merge_if_all_discussions_are_resolved == false' do @@ -1198,6 +1208,50 @@ describe MergeRequest, models: true do end end end + + describe "#discussions_to_be_resolved?" do + context "when discussions are not resolvable" do + before do + allow(subject).to receive(:discussions_resolvable?).and_return(false) + end + + it "returns false" do + expect(subject.discussions_to_be_resolved?).to be false + end + end + + context "when discussions are resolvable" do + before do + allow(subject).to receive(:discussions_resolvable?).and_return(true) + + allow(first_discussion).to receive(:resolvable?).and_return(true) + allow(second_discussion).to receive(:resolvable?).and_return(false) + allow(third_discussion).to receive(:resolvable?).and_return(true) + end + + context "when all resolvable discussions are resolved" do + before do + allow(first_discussion).to receive(:resolved?).and_return(true) + allow(third_discussion).to receive(:resolved?).and_return(true) + end + + it "returns false" do + expect(subject.discussions_to_be_resolved?).to be false + end + end + + context "when some resolvable discussions are not resolved" do + before do + allow(first_discussion).to receive(:resolved?).and_return(true) + allow(third_discussion).to receive(:resolved?).and_return(false) + end + + it "returns true" do + expect(subject.discussions_to_be_resolved?).to be true + end + end + end + end end describe '#conflicts_can_be_resolved_in_ui?' do -- cgit v1.2.1 From 6683fdcfb0ae4ceb368b6f5f63dde0a10a4a3e1b Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Mon, 14 Nov 2016 16:55:31 +0200 Subject: Add nested groups support to the routing Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- app/controllers/application_controller.rb | 8 + app/models/project.rb | 1 + app/models/user.rb | 6 +- app/validators/namespace_validator.rb | 22 +- app/validators/project_path_validator.rb | 36 + app/views/projects/wikis/_nav.html.haml | 2 +- .../unreleased/dz-allow-nested-group-routing.yml | 4 + config/routes.rb | 20 +- config/routes/git_http.rb | 68 +- config/routes/group.rb | 20 +- config/routes/project.rb | 58 +- config/routes/repository.rb | 122 +-- config/routes/user.rb | 3 + config/routes/wiki.rb | 27 +- features/steps/project/labels.rb | 2 - features/steps/shared/diff_note.rb | 5 + features/steps/shared/note.rb | 4 + lib/constraints/constrainer_helper.rb | 15 - lib/constraints/group_url_constrainer.rb | 20 +- lib/constraints/project_url_constrainer.rb | 13 + lib/constraints/user_url_constrainer.rb | 12 +- lib/gitlab/regex.rb | 14 +- spec/controllers/application_controller_spec.rb | 23 +- spec/lib/constraints/constrainer_helper_spec.rb | 20 - spec/lib/constraints/group_url_constrainer_spec.rb | 20 +- .../constraints/project_url_constrainer_spec.rb | 32 + spec/lib/constraints/user_url_constrainer_spec.rb | 21 +- spec/models/user_spec.rb | 11 + spec/routing/project_routing_spec.rb | 1034 ++++++++++---------- spec/routing/routing_spec.rb | 16 +- 30 files changed, 904 insertions(+), 755 deletions(-) create mode 100644 app/validators/project_path_validator.rb create mode 100644 changelogs/unreleased/dz-allow-nested-group-routing.yml delete mode 100644 lib/constraints/constrainer_helper.rb create mode 100644 lib/constraints/project_url_constrainer.rb delete mode 100644 spec/lib/constraints/constrainer_helper_spec.rb create mode 100644 spec/lib/constraints/project_url_constrainer_spec.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 517ad4f03f3..6f3ae9bf8eb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -49,6 +49,14 @@ class ApplicationController < ActionController::Base render_404 end + def route_not_found + if current_user + not_found + else + redirect_to new_user_session_path + end + end + protected # This filter handles both private tokens and personal access tokens diff --git a/app/models/project.rb b/app/models/project.rb index f8a54324341..4976d3ab680 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -174,6 +174,7 @@ class Project < ActiveRecord::Base message: Gitlab::Regex.project_name_regex_message } validates :path, presence: true, + project_path: true, length: { within: 0..255 }, format: { with: Gitlab::Regex.project_path_regex, message: Gitlab::Regex.project_path_regex_message } diff --git a/app/models/user.rb b/app/models/user.rb index 29fb849940a..e3d2c57e47f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -291,8 +291,12 @@ class User < ActiveRecord::Base end end + def find_by_username(username) + iwhere(username: username).take + end + def find_by_username!(username) - find_by!('lower(username) = ?', username.downcase) + iwhere(username: username).take! end def find_by_personal_access_token(token_string) diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb index 2821ecf0a88..eb3ed31b65b 100644 --- a/app/validators/namespace_validator.rb +++ b/app/validators/namespace_validator.rb @@ -35,8 +35,22 @@ class NamespaceValidator < ActiveModel::EachValidator users ].freeze + def self.valid?(value) + !reserved?(value) && follow_format?(value) + end + + def self.reserved?(value) + RESERVED.include?(value) + end + + def self.follow_format?(value) + value =~ Gitlab::Regex.namespace_regex + end + + delegate :reserved?, :follow_format?, to: :class + def validate_each(record, attribute, value) - unless value =~ Gitlab::Regex.namespace_regex + unless follow_format?(value) record.errors.add(attribute, Gitlab::Regex.namespace_regex_message) end @@ -44,10 +58,4 @@ class NamespaceValidator < ActiveModel::EachValidator record.errors.add(attribute, "#{value} is a reserved name") end end - - private - - def reserved?(value) - RESERVED.include?(value) - end end diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb new file mode 100644 index 00000000000..927c67b65b0 --- /dev/null +++ b/app/validators/project_path_validator.rb @@ -0,0 +1,36 @@ +# ProjectPathValidator +# +# Custom validator for GitLab project path values. +# +# Values are checked for formatting and exclusion from a list of reserved path +# names. +class ProjectPathValidator < ActiveModel::EachValidator + # All project routes with wildcard argument must be listed here. + # Otherwise it can lead to routing issues when route considered as project name. + # + # Example: + # /group/project/tree/deploy_keys + # + # without tree as reserved name routing can match 'group/project' as group name, + # 'tree' as project name and 'deploy_keys' as route. + # + RESERVED = (NamespaceValidator::RESERVED + + %w[tree commits wikis new edit create update logs_tree + preview blob blame raw files create_dir find_file]).freeze + + def self.valid?(value) + !reserved?(value) + end + + def self.reserved?(value) + RESERVED.include?(value) + end + + delegate :reserved?, to: :class + + def validate_each(record, attribute, value) + if reserved?(value) + record.errors.add(attribute, "#{value} is a reserved name") + end + end +end diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml index 09c4411d67e..afdef70e1cf 100644 --- a/app/views/projects/wikis/_nav.html.haml +++ b/app/views/projects/wikis/_nav.html.haml @@ -7,7 +7,7 @@ = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) = nav_link(path: 'wikis#pages') do - = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project) + = link_to 'Pages', namespace_project_wikis_pages_path(@project.namespace, @project) = nav_link(path: 'wikis#git_access') do = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do diff --git a/changelogs/unreleased/dz-allow-nested-group-routing.yml b/changelogs/unreleased/dz-allow-nested-group-routing.yml new file mode 100644 index 00000000000..9d8e6e17914 --- /dev/null +++ b/changelogs/unreleased/dz-allow-nested-group-routing.yml @@ -0,0 +1,4 @@ +--- +title: Add nested groups support to the routing +merge_request: 7459 +author: diff --git a/config/routes.rb b/config/routes.rb index 7bf6c03e69b..03b47261e7e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,7 @@ require 'sidekiq/web' require 'sidekiq/cron/web' require 'api/api' +require 'constraints/group_url_constrainer' Rails.application.routes.draw do concern :access_requestable do @@ -78,10 +79,21 @@ Rails.application.routes.draw do draw :user draw :project - # Get all keys of user - get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: /.*/ } - root to: "root#index" - get '*unmatched_route', to: 'application#not_found' + # Since group show page is wildcard routing + # we want all other routing to be checked before matching this one + constraints(GroupUrlConstrainer.new) do + scope(path: '*id', + as: :group, + constraints: { id: Gitlab::Regex.namespace_route_regex, format: /(html|json|atom)/ }, + controller: :groups) do + get '/', action: :show + patch '/', action: :update + put '/', action: :update + delete '/', action: :destroy + end + end + + get '*unmatched_route', to: 'application#route_not_found' end diff --git a/config/routes/git_http.rb b/config/routes/git_http.rb index 03adc4815f3..42d874eeebc 100644 --- a/config/routes/git_http.rb +++ b/config/routes/git_http.rb @@ -1,37 +1,47 @@ -scope constraints: { id: /.+\.git/, format: nil } do - # Git HTTP clients ('git clone' etc.) - get '/info/refs', to: 'git_http#info_refs' - post '/git-upload-pack', to: 'git_http#git_upload_pack' - post '/git-receive-pack', to: 'git_http#git_receive_pack' +scope(path: '*namespace_id/:project_id', constraints: { format: nil }) do + scope(constraints: { project_id: Gitlab::Regex.project_git_route_regex }, module: :projects) do + # Git HTTP clients ('git clone' etc.) + scope(controller: :git_http) do + get '/info/refs', action: :info_refs + post '/git-upload-pack', action: :git_upload_pack + post '/git-receive-pack', action: :git_receive_pack + end - # Git LFS API (metadata) - post '/info/lfs/objects/batch', to: 'lfs_api#batch' - post '/info/lfs/objects', to: 'lfs_api#deprecated' - get '/info/lfs/objects/*oid', to: 'lfs_api#deprecated' + # Git LFS API (metadata) + scope(path: 'info/lfs/objects', controller: :lfs_api) do + post :batch + post '/', action: :deprecated + get '/*oid', action: :deprecated + end - # GitLab LFS object storage - scope constraints: { oid: /[a-f0-9]{64}/ } do - get '/gitlab-lfs/objects/*oid', to: 'lfs_storage#download' + # GitLab LFS object storage + scope(path: 'gitlab-lfs/objects/*oid', controller: :lfs_storage, constraints: { oid: /[a-f0-9]{64}/ }) do + get '/', action: :download - scope constraints: { size: /[0-9]+/ } do - put '/gitlab-lfs/objects/*oid/*size/authorize', to: 'lfs_storage#upload_authorize' - put '/gitlab-lfs/objects/*oid/*size', to: 'lfs_storage#upload_finalize' + scope constraints: { size: /[0-9]+/ } do + put '/*size/authorize', action: :upload_authorize + put '/*size', action: :upload_finalize + end end end -end -# Allow /info/refs, /info/refs?service=git-upload-pack, and -# /info/refs?service=git-receive-pack, but nothing else. -# -git_http_handshake = lambda do |request| - request.query_string.blank? || - request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/) -end + # Redirect /group/project/info/refs to /group/project.git/info/refs + scope(constraints: { project_id: Gitlab::Regex.project_route_regex }) do + # Allow /info/refs, /info/refs?service=git-upload-pack, and + # /info/refs?service=git-receive-pack, but nothing else. + # + git_http_handshake = lambda do |request| + ProjectUrlConstrainer.new.matches?(request) && + (request.query_string.blank? || + request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/)) + end -ref_redirect = redirect do |params, request| - path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs" - path << "?#{request.query_string}" unless request.query_string.blank? - path -end + ref_redirect = redirect do |params, request| + path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs" + path << "?#{request.query_string}" unless request.query_string.blank? + path + end -get '/info/refs', constraints: git_http_handshake, to: ref_redirect + get '/info/refs', constraints: git_http_handshake, to: ref_redirect + end +end diff --git a/config/routes/group.rb b/config/routes/group.rb index a3a001178b4..9fe72990994 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -1,20 +1,6 @@ -require 'constraints/group_url_constrainer' - -constraints(GroupUrlConstrainer.new) do - scope(path: ':id', - as: :group, - constraints: { id: Gitlab::Regex.namespace_route_regex }, - controller: :groups) do - get '/', action: :show - patch '/', action: :update - put '/', action: :update - delete '/', action: :destroy - end -end - resources :groups, only: [:index, :new, :create] -scope(path: 'groups/:id', +scope(path: 'groups/*id', controller: :groups, constraints: { id: Gitlab::Regex.namespace_route_regex }) do get :edit, as: :edit_group @@ -24,7 +10,7 @@ scope(path: 'groups/:id', get :activity, as: :activity_group end -scope(path: 'groups/:group_id', +scope(path: 'groups/*group_id', module: :groups, as: :group, constraints: { group_id: Gitlab::Regex.namespace_route_regex }) do @@ -42,4 +28,4 @@ scope(path: 'groups/:group_id', end # Must be last route in this file -get 'groups/:id' => 'groups#show', as: :group_canonical, constraints: { id: Gitlab::Regex.namespace_route_regex } +get 'groups/*id' => 'groups#show', as: :group_canonical, constraints: { id: Gitlab::Regex.namespace_route_regex } diff --git a/config/routes/project.rb b/config/routes/project.rb index d6eae1c9fce..1336484a399 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -1,28 +1,15 @@ -resources :projects, constraints: { id: /[^\/]+/ }, only: [:index, :new, :create] - -resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do - resources(:projects, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, except: - [:new, :create, :index], path: "/") do - member do - put :transfer - delete :remove_fork - post :archive - post :unarchive - post :housekeeping - post :toggle_star - post :preview_markdown - post :export - post :remove_export - post :generate_new_export - get :download_export - get :autocomplete_sources - get :activity - get :refs - put :new_issue_address - end +require 'constraints/project_url_constrainer' + +resources :projects, only: [:index, :new, :create] + +draw :git_http - scope module: :projects do - draw :git_http +constraints(ProjectUrlConstrainer.new) do + scope(path: '*namespace_id', as: :namespace) do + scope(path: ':project_id', + constraints: { project_id: Gitlab::Regex.project_route_regex }, + module: :projects, + as: :project) do # # Templates @@ -311,5 +298,28 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: draw :wiki draw :repository end + + resources(:projects, + path: '/', + constraints: { id: Gitlab::Regex.project_route_regex }, + only: [:edit, :show, :update, :destroy]) do + member do + put :transfer + delete :remove_fork + post :archive + post :unarchive + post :housekeeping + post :toggle_star + post :preview_markdown + post :export + post :remove_export + post :generate_new_export + get :download_export + get :autocomplete_sources + get :activity + get :refs + put :new_issue_address + end + end end end diff --git a/config/routes/repository.rb b/config/routes/repository.rb index 76dcf113aea..f8966c5ae75 100644 --- a/config/routes/repository.rb +++ b/config/routes/repository.rb @@ -29,82 +29,60 @@ get '/edit/*id', to: 'blob#edit', constraints: { id: /.+/ }, as: 'edit_blob' put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob' post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob' -scope do - get( - '/blob/*id/diff', - to: 'blob#diff', - constraints: { id: /.+/, format: false }, - as: :blob_diff - ) - get( - '/blob/*id', - to: 'blob#show', - constraints: { id: /.+/, format: false }, - as: :blob - ) - delete( - '/blob/*id', - to: 'blob#destroy', - constraints: { id: /.+/, format: false } - ) - put( - '/blob/*id', - to: 'blob#update', - constraints: { id: /.+/, format: false } - ) - post( - '/blob/*id', - to: 'blob#create', - constraints: { id: /.+/, format: false } - ) +scope('/blob/*id', as: :blob, controller: :blob, constraints: { id: /.+/, format: false }) do + get :diff + get '/', action: :show + delete '/', action: :destroy + post '/', action: :create + put '/', action: :update +end - get( - '/raw/*id', - to: 'raw#show', - constraints: { id: /.+/, format: /(html|js)/ }, - as: :raw - ) +get( + '/raw/*id', + to: 'raw#show', + constraints: { id: /.+/, format: /(html|js)/ }, + as: :raw +) - get( - '/tree/*id', - to: 'tree#show', - constraints: { id: /.+/, format: /(html|js)/ }, - as: :tree - ) +get( + '/tree/*id', + to: 'tree#show', + constraints: { id: /.+/, format: /(html|js)/ }, + as: :tree +) - get( - '/find_file/*id', - to: 'find_file#show', - constraints: { id: /.+/, format: /html/ }, - as: :find_file - ) +get( + '/find_file/*id', + to: 'find_file#show', + constraints: { id: /.+/, format: /html/ }, + as: :find_file +) - get( - '/files/*id', - to: 'find_file#list', - constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ }, - as: :files - ) +get( + '/files/*id', + to: 'find_file#list', + constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ }, + as: :files +) - post( - '/create_dir/*id', - to: 'tree#create_dir', - constraints: { id: /.+/ }, - as: 'create_dir' - ) +post( + '/create_dir/*id', + to: 'tree#create_dir', + constraints: { id: /.+/ }, + as: 'create_dir' +) - get( - '/blame/*id', - to: 'blame#show', - constraints: { id: /.+/, format: /(html|js)/ }, - as: :blame - ) +get( + '/blame/*id', + to: 'blame#show', + constraints: { id: /.+/, format: /(html|js)/ }, + as: :blame +) - # File/dir history - get( - '/commits/*id', - to: 'commits#show', - constraints: { id: /.+/, format: false }, - as: :commits - ) -end +# File/dir history +get( + '/commits/*id', + to: 'commits#show', + constraints: { id: /.+/, format: false }, + as: :commits +) diff --git a/config/routes/user.rb b/config/routes/user.rb index dc1068af6f6..b064a15e802 100644 --- a/config/routes/user.rb +++ b/config/routes/user.rb @@ -12,6 +12,9 @@ devise_scope :user do end constraints(UserUrlConstrainer.new) do + # Get all keys of user + get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: Gitlab::Regex.namespace_route_regex } + scope(path: ':username', as: :user, constraints: { username: Gitlab::Regex.namespace_route_regex }, diff --git a/config/routes/wiki.rb b/config/routes/wiki.rb index ecd4d395d66..dad746d59a1 100644 --- a/config/routes/wiki.rb +++ b/config/routes/wiki.rb @@ -1,16 +1,19 @@ WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID -scope do - # Order matters to give priority to these matches - get '/wikis/git_access', to: 'wikis#git_access' - get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages' - post '/wikis', to: 'wikis#create' +scope(controller: :wikis) do + scope(path: 'wikis', as: :wikis) do + get :git_access + get :pages + get '/', to: redirect('/%{namespace_id}/%{project_id}/wikis/home') + post '/', to: 'wikis#create' + end - get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID - get '/wikis/*id/edit', to: 'wikis#edit', as: 'wiki_edit', constraints: WIKI_SLUG_ID - - get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID - delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID - put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID - post '/wikis/*id/preview_markdown', to: 'wikis#preview_markdown', constraints: WIKI_SLUG_ID, as: 'wiki_preview_markdown' + scope(path: 'wikis/*id', as: :wiki, constraints: WIKI_SLUG_ID, format: false) do + get :edit + get :history + post :preview_markdown + get '/', action: :show + put '/', action: :update + delete '/', action: :destroy + end end diff --git a/features/steps/project/labels.rb b/features/steps/project/labels.rb index 118ffef4774..dbeb07c78db 100644 --- a/features/steps/project/labels.rb +++ b/features/steps/project/labels.rb @@ -2,9 +2,7 @@ class Spinach::Features::Labels < Spinach::FeatureSteps include SharedAuthentication include SharedIssuable include SharedProject - include SharedNote include SharedPaths - include SharedMarkdown step 'And I visit project "Shop" labels page' do visit namespace_project_labels_path(project.namespace, project) diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index 35b71599708..11fa85ed2fe 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -1,6 +1,11 @@ module SharedDiffNote include Spinach::DSL include RepoHelpers + include WaitForAjax + + after do + wait_for_ajax if javascript_test? + end step 'I cancel the diff comment' do page.within(diff_file_selector) do diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index 9dc1fc41b3b..1870f6bc0c3 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -2,6 +2,10 @@ module SharedNote include Spinach::DSL include WaitForAjax + after do + wait_for_ajax if javascript_test? + end + step 'I delete a comment' do page.within('.main-notes-list') do find('.note').hover diff --git a/lib/constraints/constrainer_helper.rb b/lib/constraints/constrainer_helper.rb deleted file mode 100644 index ab07a6793d9..00000000000 --- a/lib/constraints/constrainer_helper.rb +++ /dev/null @@ -1,15 +0,0 @@ -module ConstrainerHelper - def extract_resource_path(path) - id = path.dup - id.sub!(/\A#{relative_url_root}/, '') if relative_url_root - id.sub(/\A\/+/, '').sub(/\/+\z/, '').sub(/.atom\z/, '') - end - - private - - def relative_url_root - if defined?(Gitlab::Application.config.relative_url_root) - Gitlab::Application.config.relative_url_root - end - end -end diff --git a/lib/constraints/group_url_constrainer.rb b/lib/constraints/group_url_constrainer.rb index 2af6e1a11c8..5711d96a586 100644 --- a/lib/constraints/group_url_constrainer.rb +++ b/lib/constraints/group_url_constrainer.rb @@ -1,15 +1,17 @@ -require_relative 'constrainer_helper' - class GroupUrlConstrainer - include ConstrainerHelper - def matches?(request) - id = extract_resource_path(request.path) + id = request.params[:id] + + return false unless valid?(id) + + Group.find_by(path: id).present? + end + + private - if id =~ Gitlab::Regex.namespace_regex - Group.find_by(path: id).present? - else - false + def valid?(id) + id.split('/').all? do |namespace| + NamespaceValidator.valid?(namespace) end end end diff --git a/lib/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb new file mode 100644 index 00000000000..730b05bed97 --- /dev/null +++ b/lib/constraints/project_url_constrainer.rb @@ -0,0 +1,13 @@ +class ProjectUrlConstrainer + def matches?(request) + namespace_path = request.params[:namespace_id] + project_path = request.params[:project_id] || request.params[:id] + full_path = namespace_path + '/' + project_path + + unless ProjectPathValidator.valid?(project_path) + return false + end + + Project.find_with_namespace(full_path).present? + end +end diff --git a/lib/constraints/user_url_constrainer.rb b/lib/constraints/user_url_constrainer.rb index 4d722ad5af2..9ab5bcb12ff 100644 --- a/lib/constraints/user_url_constrainer.rb +++ b/lib/constraints/user_url_constrainer.rb @@ -1,15 +1,5 @@ -require_relative 'constrainer_helper' - class UserUrlConstrainer - include ConstrainerHelper - def matches?(request) - id = extract_resource_path(request.path) - - if id =~ Gitlab::Regex.namespace_regex - User.find_by('lower(username) = ?', id.downcase).present? - else - false - end + User.find_by_username(request.params[:username]).present? end end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index c12358ceef4..a06cf6a989c 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -8,8 +8,10 @@ module Gitlab # allow non-regex validatiions, etc), `NAMESPACE_REGEX_STR_SIMPLE` serves as a Javascript-compatible version of # `NAMESPACE_REGEX_STR`, with the negative lookbehind assertion removed. This means that the client-side validation # will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation. - NAMESPACE_REGEX_STR_SIMPLE = '[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze + PATH_REGEX_STR = '[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*'.freeze + NAMESPACE_REGEX_STR_SIMPLE = PATH_REGEX_STR + '[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze NAMESPACE_REGEX_STR = '(?:' + NAMESPACE_REGEX_STR_SIMPLE + ')(?<!\.git|\.atom)'.freeze + PROJECT_REGEX_STR = PATH_REGEX_STR + '(?<!\.git|\.atom)'.freeze def namespace_regex @namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze @@ -42,7 +44,15 @@ module Gitlab end def project_path_regex - @project_path_regex ||= /\A[a-zA-Z0-9_.][a-zA-Z0-9_\-\.]*(?<!\.git|\.atom)\z/.freeze + @project_path_regex ||= /\A#{PROJECT_REGEX_STR}\z/.freeze + end + + def project_route_regex + @project_route_regex ||= /#{PROJECT_REGEX_STR}/.freeze + end + + def project_git_route_regex + @project_route_git_regex ||= /#{PATH_REGEX_STR}\.git/.freeze end def project_path_regex_message diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 98e912f000c..81cbccd5436 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -1,8 +1,9 @@ require 'spec_helper' describe ApplicationController do + let(:user) { create(:user) } + describe '#check_password_expiration' do - let(:user) { create(:user) } let(:controller) { ApplicationController.new } it 'redirects if the user is over their password expiry' do @@ -39,8 +40,6 @@ describe ApplicationController do end end - let(:user) { create(:user) } - context "when the 'private_token' param is populated with the private token" do it "logs the user in" do get :index, private_token: user.private_token @@ -73,7 +72,6 @@ describe ApplicationController do end end - let(:user) { create(:user) } let(:personal_access_token) { create(:personal_access_token, user: user) } context "when the 'personal_access_token' param is populated with the personal access token" do @@ -100,4 +98,21 @@ describe ApplicationController do end end end + + describe '#route_not_found' do + let(:controller) { ApplicationController.new } + + it 'renders 404 if authenticated' do + allow(controller).to receive(:current_user).and_return(user) + expect(controller).to receive(:not_found) + controller.send(:route_not_found) + end + + it 'does redirect to login page if not authenticated' do + allow(controller).to receive(:current_user).and_return(nil) + expect(controller).to receive(:redirect_to) + expect(controller).to receive(:new_user_session_path) + controller.send(:route_not_found) + end + end end diff --git a/spec/lib/constraints/constrainer_helper_spec.rb b/spec/lib/constraints/constrainer_helper_spec.rb deleted file mode 100644 index 27c8d72aefc..00000000000 --- a/spec/lib/constraints/constrainer_helper_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'spec_helper' - -describe ConstrainerHelper, lib: true do - include ConstrainerHelper - - describe '#extract_resource_path' do - it { expect(extract_resource_path('/gitlab/')).to eq('gitlab') } - it { expect(extract_resource_path('///gitlab//')).to eq('gitlab') } - it { expect(extract_resource_path('/gitlab.atom')).to eq('gitlab') } - - context 'relative url' do - before do - allow(Gitlab::Application.config).to receive(:relative_url_root) { '/gitlab' } - end - - it { expect(extract_resource_path('/gitlab/foo')).to eq('foo') } - it { expect(extract_resource_path('/foo/bar')).to eq('foo/bar') } - end - end -end diff --git a/spec/lib/constraints/group_url_constrainer_spec.rb b/spec/lib/constraints/group_url_constrainer_spec.rb index 42299b17c2b..892554f2870 100644 --- a/spec/lib/constraints/group_url_constrainer_spec.rb +++ b/spec/lib/constraints/group_url_constrainer_spec.rb @@ -4,16 +4,20 @@ describe GroupUrlConstrainer, lib: true do let!(:group) { create(:group, path: 'gitlab') } describe '#matches?' do - context 'root group' do - it { expect(subject.matches?(request '/gitlab')).to be_truthy } - it { expect(subject.matches?(request '/gitlab.atom')).to be_truthy } - it { expect(subject.matches?(request '/gitlab/edit')).to be_falsey } - it { expect(subject.matches?(request '/gitlab-ce')).to be_falsey } - it { expect(subject.matches?(request '/.gitlab')).to be_falsey } + context 'valid request' do + let(:request) { build_request(group.path) } + + it { expect(subject.matches?(request)).to be_truthy } + end + + context 'invalid request' do + let(:request) { build_request('foo') } + + it { expect(subject.matches?(request)).to be_falsey } end end - def request(path) - double(:request, path: path) + def build_request(path) + double(:request, params: { id: path }) end end diff --git a/spec/lib/constraints/project_url_constrainer_spec.rb b/spec/lib/constraints/project_url_constrainer_spec.rb new file mode 100644 index 00000000000..94266f6653b --- /dev/null +++ b/spec/lib/constraints/project_url_constrainer_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe ProjectUrlConstrainer, lib: true do + let!(:project) { create(:project) } + let!(:namespace) { project.namespace } + + describe '#matches?' do + context 'valid request' do + let(:request) { build_request(namespace.path, project.path) } + + it { expect(subject.matches?(request)).to be_truthy } + end + + context 'invalid request' do + context "non-existing project" do + let(:request) { build_request('foo', 'bar') } + + it { expect(subject.matches?(request)).to be_falsey } + end + + context "project id ending with .git" do + let(:request) { build_request(namespace.path, project.path + '.git') } + + it { expect(subject.matches?(request)).to be_falsey } + end + end + end + + def build_request(namespace, project) + double(:request, params: { namespace_id: namespace, id: project }) + end +end diff --git a/spec/lib/constraints/user_url_constrainer_spec.rb b/spec/lib/constraints/user_url_constrainer_spec.rb index b3f8530c609..207b6fe6c9e 100644 --- a/spec/lib/constraints/user_url_constrainer_spec.rb +++ b/spec/lib/constraints/user_url_constrainer_spec.rb @@ -1,16 +1,23 @@ require 'spec_helper' describe UserUrlConstrainer, lib: true do - let!(:username) { create(:user, username: 'dz') } + let!(:user) { create(:user, username: 'dz') } describe '#matches?' do - it { expect(subject.matches?(request '/dz')).to be_truthy } - it { expect(subject.matches?(request '/dz.atom')).to be_truthy } - it { expect(subject.matches?(request '/dz/projects')).to be_falsey } - it { expect(subject.matches?(request '/gitlab')).to be_falsey } + context 'valid request' do + let(:request) { build_request(user.username) } + + it { expect(subject.matches?(request)).to be_truthy } + end + + context 'invalid request' do + let(:request) { build_request('foo') } + + it { expect(subject.matches?(request)).to be_falsey } + end end - def request(path) - double(:request, path: path) + def build_request(username) + double(:request, params: { username: username }) end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index e84042f8063..91826e5884d 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -752,6 +752,17 @@ describe User, models: true do end end + describe '.find_by_username' do + it 'returns nil if not found' do + expect(described_class.find_by_username('JohnDoe')).to be_nil + end + + it 'is case-insensitive' do + user = create(:user, username: 'JohnDoe') + expect(described_class.find_by_username('JOHNDOE')).to eq user + end + end + describe '.find_by_username!' do it 'raises RecordNotFound' do expect { described_class.find_by_username!('JohnDoe') }. diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 2322430d212..b6e7da841b1 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -1,511 +1,531 @@ require 'spec_helper' -# Shared examples for a resource inside a Project -# -# By default it tests all the default REST actions: index, create, new, edit, -# show, update, and destroy. You can remove actions by customizing the -# `actions` variable. -# -# It also expects a `controller` variable to be available which defines both -# the path to the resource as well as the controller name. -# -# Examples -# -# # Default behavior -# it_behaves_like 'RESTful project resources' do -# let(:controller) { 'issues' } -# end -# -# # Customizing actions -# it_behaves_like 'RESTful project resources' do -# let(:actions) { [:index] } -# let(:controller) { 'issues' } -# end -shared_examples 'RESTful project resources' do - let(:actions) { [:index, :create, :new, :edit, :show, :update, :destroy] } - - it 'to #index' do - expect(get("/gitlab/gitlabhq/#{controller}")).to route_to("projects/#{controller}#index", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:index) - end - - it 'to #create' do - expect(post("/gitlab/gitlabhq/#{controller}")).to route_to("projects/#{controller}#create", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:create) - end - - it 'to #new' do - expect(get("/gitlab/gitlabhq/#{controller}/new")).to route_to("projects/#{controller}#new", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:new) - end - - it 'to #edit' do - expect(get("/gitlab/gitlabhq/#{controller}/1/edit")).to route_to("projects/#{controller}#edit", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:edit) - end - - it 'to #show' do - expect(get("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#show", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:show) - end - - it 'to #update' do - expect(put("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#update", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:update) - end - - it 'to #destroy' do - expect(delete("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#destroy", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:destroy) - end -end - -# projects POST /projects(.:format) projects#create -# new_project GET /projects/new(.:format) projects#new -# files_project GET /:id/files(.:format) projects#files -# edit_project GET /:id/edit(.:format) projects#edit -# project GET /:id(.:format) projects#show -# PUT /:id(.:format) projects#update -# DELETE /:id(.:format) projects#destroy -# preview_markdown_project POST /:id/preview_markdown(.:format) projects#preview_markdown -describe ProjectsController, 'routing' do - it 'to #create' do - expect(post('/projects')).to route_to('projects#create') - end - - it 'to #new' do - expect(get('/projects/new')).to route_to('projects#new') - end - - it 'to #edit' do - expect(get('/gitlab/gitlabhq/edit')).to route_to('projects#edit', namespace_id: 'gitlab', id: 'gitlabhq') - end - - it 'to #autocomplete_sources' do - expect(get('/gitlab/gitlabhq/autocomplete_sources')).to route_to('projects#autocomplete_sources', namespace_id: 'gitlab', id: 'gitlabhq') - end - - it 'to #show' do - expect(get('/gitlab/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq') - expect(get('/gitlab/gitlabhq.keys')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq.keys') - end - - it 'to #update' do - expect(put('/gitlab/gitlabhq')).to route_to('projects#update', namespace_id: 'gitlab', id: 'gitlabhq') - end - - it 'to #destroy' do - expect(delete('/gitlab/gitlabhq')).to route_to('projects#destroy', namespace_id: 'gitlab', id: 'gitlabhq') - end - - it 'to #preview_markdown' do - expect(post('/gitlab/gitlabhq/preview_markdown')).to( - route_to('projects#preview_markdown', namespace_id: 'gitlab', id: 'gitlabhq') - ) - end -end - -# pages_project_wikis GET /:project_id/wikis/pages(.:format) projects/wikis#pages -# history_project_wiki GET /:project_id/wikis/:id/history(.:format) projects/wikis#history -# project_wikis POST /:project_id/wikis(.:format) projects/wikis#create -# edit_project_wiki GET /:project_id/wikis/:id/edit(.:format) projects/wikis#edit -# project_wiki GET /:project_id/wikis/:id(.:format) projects/wikis#show -# DELETE /:project_id/wikis/:id(.:format) projects/wikis#destroy -describe Projects::WikisController, 'routing' do - it 'to #pages' do - expect(get('/gitlab/gitlabhq/wikis/pages')).to route_to('projects/wikis#pages', namespace_id: 'gitlab', project_id: 'gitlabhq') - end - - it 'to #history' do - expect(get('/gitlab/gitlabhq/wikis/1/history')).to route_to('projects/wikis#history', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') - end - - it_behaves_like 'RESTful project resources' do - let(:actions) { [:create, :edit, :show, :destroy] } - let(:controller) { 'wikis' } - end -end - -# branches_project_repository GET /:project_id/repository/branches(.:format) projects/repositories#branches -# tags_project_repository GET /:project_id/repository/tags(.:format) projects/repositories#tags -# archive_project_repository GET /:project_id/repository/archive(.:format) projects/repositories#archive -# edit_project_repository GET /:project_id/repository/edit(.:format) projects/repositories#edit -describe Projects::RepositoriesController, 'routing' do - it 'to #archive' do - expect(get('/gitlab/gitlabhq/repository/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq') - end - - it 'to #archive format:zip' do - expect(get('/gitlab/gitlabhq/repository/archive.zip')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'zip') - end - - it 'to #archive format:tar.bz2' do - expect(get('/gitlab/gitlabhq/repository/archive.tar.bz2')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'tar.bz2') - end -end - -describe Projects::BranchesController, 'routing' do - it 'to #branches' do - expect(get('/gitlab/gitlabhq/branches')).to route_to('projects/branches#index', namespace_id: 'gitlab', project_id: 'gitlabhq') - expect(delete('/gitlab/gitlabhq/branches/feature%2345')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45') - expect(delete('/gitlab/gitlabhq/branches/feature%2B45')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45') - expect(delete('/gitlab/gitlabhq/branches/feature@45')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45') - expect(delete('/gitlab/gitlabhq/branches/feature%2345/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45/foo/bar/baz') - expect(delete('/gitlab/gitlabhq/branches/feature%2B45/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45/foo/bar/baz') - expect(delete('/gitlab/gitlabhq/branches/feature@45/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45/foo/bar/baz') - end -end - -describe Projects::TagsController, 'routing' do - it 'to #tags' do - expect(get('/gitlab/gitlabhq/tags')).to route_to('projects/tags#index', namespace_id: 'gitlab', project_id: 'gitlabhq') - expect(delete('/gitlab/gitlabhq/tags/feature%2345')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45') - expect(delete('/gitlab/gitlabhq/tags/feature%2B45')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45') - expect(delete('/gitlab/gitlabhq/tags/feature@45')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45') - expect(delete('/gitlab/gitlabhq/tags/feature%2345/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45/foo/bar/baz') - expect(delete('/gitlab/gitlabhq/tags/feature%2B45/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45/foo/bar/baz') - expect(delete('/gitlab/gitlabhq/tags/feature@45/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45/foo/bar/baz') - end -end - -# project_deploy_keys GET /:project_id/deploy_keys(.:format) deploy_keys#index -# POST /:project_id/deploy_keys(.:format) deploy_keys#create -# new_project_deploy_key GET /:project_id/deploy_keys/new(.:format) deploy_keys#new -# project_deploy_key GET /:project_id/deploy_keys/:id(.:format) deploy_keys#show -# DELETE /:project_id/deploy_keys/:id(.:format) deploy_keys#destroy -describe Projects::DeployKeysController, 'routing' do - it_behaves_like 'RESTful project resources' do - let(:actions) { [:index, :new, :create] } - let(:controller) { 'deploy_keys' } - end -end - -# project_protected_branches GET /:project_id/protected_branches(.:format) protected_branches#index -# POST /:project_id/protected_branches(.:format) protected_branches#create -# project_protected_branch DELETE /:project_id/protected_branches/:id(.:format) protected_branches#destroy -describe Projects::ProtectedBranchesController, 'routing' do - it_behaves_like 'RESTful project resources' do - let(:actions) { [:index, :create, :destroy] } - let(:controller) { 'protected_branches' } - end -end - -# switch_project_refs GET /:project_id/refs/switch(.:format) refs#switch -# logs_tree_project_ref GET /:project_id/refs/:id/logs_tree(.:format) refs#logs_tree -# logs_file_project_ref GET /:project_id/refs/:id/logs_tree/:path(.:format) refs#logs_tree -describe Projects::RefsController, 'routing' do - it 'to #switch' do - expect(get('/gitlab/gitlabhq/refs/switch')).to route_to('projects/refs#switch', namespace_id: 'gitlab', project_id: 'gitlabhq') - end - - it 'to #logs_tree' do - expect(get('/gitlab/gitlabhq/refs/stable/logs_tree')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'stable') - expect(get('/gitlab/gitlabhq/refs/feature%2345/logs_tree')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45') - expect(get('/gitlab/gitlabhq/refs/feature%2B45/logs_tree')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45') - expect(get('/gitlab/gitlabhq/refs/feature@45/logs_tree')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45') - expect(get('/gitlab/gitlabhq/refs/stable/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'stable', path: 'foo/bar/baz') - expect(get('/gitlab/gitlabhq/refs/feature%2345/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45', path: 'foo/bar/baz') - expect(get('/gitlab/gitlabhq/refs/feature%2B45/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45', path: 'foo/bar/baz') - expect(get('/gitlab/gitlabhq/refs/feature@45/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45', path: 'foo/bar/baz') - expect(get('/gitlab/gitlabhq/refs/stable/logs_tree/files.scss')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'stable', path: 'files.scss') - end -end - -# diffs_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/diffs(.:format) projects/merge_requests#diffs -# commits_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/commits(.:format) projects/merge_requests#commits -# merge_namespace_project_merge_request POST /:namespace_id/:project_id/merge_requests/:id/merge(.:format) projects/merge_requests#merge -# merge_check_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/merge_check(.:format) projects/merge_requests#merge_check -# ci_status_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/ci_status(.:format) projects/merge_requests#ci_status -# toggle_subscription_namespace_project_merge_request POST /:namespace_id/:project_id/merge_requests/:id/toggle_subscription(.:format) projects/merge_requests#toggle_subscription -# branch_from_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/branch_from(.:format) projects/merge_requests#branch_from -# branch_to_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/branch_to(.:format) projects/merge_requests#branch_to -# update_branches_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/update_branches(.:format) projects/merge_requests#update_branches -# namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests(.:format) projects/merge_requests#index -# POST /:namespace_id/:project_id/merge_requests(.:format) projects/merge_requests#create -# new_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/new(.:format) projects/merge_requests#new -# edit_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/edit(.:format) projects/merge_requests#edit -# namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#show -# PATCH /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#update -# PUT /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#update -describe Projects::MergeRequestsController, 'routing' do - it 'to #diffs' do - expect(get('/gitlab/gitlabhq/merge_requests/1/diffs')).to route_to('projects/merge_requests#diffs', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') - end - - it 'to #commits' do - expect(get('/gitlab/gitlabhq/merge_requests/1/commits')).to route_to('projects/merge_requests#commits', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') - end - - it 'to #merge' do - expect(post('/gitlab/gitlabhq/merge_requests/1/merge')).to route_to( - 'projects/merge_requests#merge', - namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1' - ) - end - - it 'to #merge_check' do - expect(get('/gitlab/gitlabhq/merge_requests/1/merge_check')).to route_to('projects/merge_requests#merge_check', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') - end - - it 'to #branch_from' do - expect(get('/gitlab/gitlabhq/merge_requests/branch_from')).to route_to('projects/merge_requests#branch_from', namespace_id: 'gitlab', project_id: 'gitlabhq') - end - - it 'to #branch_to' do - expect(get('/gitlab/gitlabhq/merge_requests/branch_to')).to route_to('projects/merge_requests#branch_to', namespace_id: 'gitlab', project_id: 'gitlabhq') - end - - it 'to #show' do - expect(get('/gitlab/gitlabhq/merge_requests/1.diff')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'diff') - expect(get('/gitlab/gitlabhq/merge_requests/1.patch')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'patch') - end - - it_behaves_like 'RESTful project resources' do - let(:controller) { 'merge_requests' } - let(:actions) { [:index, :create, :new, :edit, :show, :update] } - end -end - -# raw_project_snippet GET /:project_id/snippets/:id/raw(.:format) snippets#raw -# project_snippets GET /:project_id/snippets(.:format) snippets#index -# POST /:project_id/snippets(.:format) snippets#create -# new_project_snippet GET /:project_id/snippets/new(.:format) snippets#new -# edit_project_snippet GET /:project_id/snippets/:id/edit(.:format) snippets#edit -# project_snippet GET /:project_id/snippets/:id(.:format) snippets#show -# PUT /:project_id/snippets/:id(.:format) snippets#update -# DELETE /:project_id/snippets/:id(.:format) snippets#destroy -describe SnippetsController, 'routing' do - it 'to #raw' do - expect(get('/gitlab/gitlabhq/snippets/1/raw')).to route_to('projects/snippets#raw', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') - end - - it 'to #index' do - expect(get('/gitlab/gitlabhq/snippets')).to route_to('projects/snippets#index', namespace_id: 'gitlab', project_id: 'gitlabhq') - end - - it 'to #create' do - expect(post('/gitlab/gitlabhq/snippets')).to route_to('projects/snippets#create', namespace_id: 'gitlab', project_id: 'gitlabhq') - end - - it 'to #new' do - expect(get('/gitlab/gitlabhq/snippets/new')).to route_to('projects/snippets#new', namespace_id: 'gitlab', project_id: 'gitlabhq') - end - - it 'to #edit' do - expect(get('/gitlab/gitlabhq/snippets/1/edit')).to route_to('projects/snippets#edit', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') - end - - it 'to #show' do - expect(get('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') - end - - it 'to #update' do - expect(put('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#update', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') - end - - it 'to #destroy' do - expect(delete('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') - end -end - -# test_project_hook GET /:project_id/hooks/:id/test(.:format) hooks#test -# project_hooks GET /:project_id/hooks(.:format) hooks#index -# POST /:project_id/hooks(.:format) hooks#create -# project_hook DELETE /:project_id/hooks/:id(.:format) hooks#destroy -describe Projects::HooksController, 'routing' do - it 'to #test' do - expect(get('/gitlab/gitlabhq/hooks/1/test')).to route_to('projects/hooks#test', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') - end - - it_behaves_like 'RESTful project resources' do - let(:actions) { [:index, :create, :destroy] } - let(:controller) { 'hooks' } - end -end - -# project_commit GET /:project_id/commit/:id(.:format) commit#show {id: /\h{7,40}/, project_id: /[^\/]+/} -describe Projects::CommitController, 'routing' do - it 'to #show' do - expect(get('/gitlab/gitlabhq/commit/4246fbd')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd') - expect(get('/gitlab/gitlabhq/commit/4246fbd.diff')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd', format: 'diff') - expect(get('/gitlab/gitlabhq/commit/4246fbd.patch')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd', format: 'patch') - expect(get('/gitlab/gitlabhq/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5') - end -end - -# patch_project_commit GET /:project_id/commits/:id/patch(.:format) commits#patch -# project_commits GET /:project_id/commits(.:format) commits#index -# POST /:project_id/commits(.:format) commits#create -# project_commit GET /:project_id/commits/:id(.:format) commits#show -describe Projects::CommitsController, 'routing' do - it_behaves_like 'RESTful project resources' do - let(:actions) { [:show] } - let(:controller) { 'commits' } - end - - it 'to #show' do - expect(get('/gitlab/gitlabhq/commits/master.atom')).to route_to('projects/commits#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master.atom') - end -end - -# project_project_members GET /:project_id/project_members(.:format) project_members#index -# POST /:project_id/project_members(.:format) project_members#create -# PUT /:project_id/project_members/:id(.:format) project_members#update -# DELETE /:project_id/project_members/:id(.:format) project_members#destroy -describe Projects::ProjectMembersController, 'routing' do - it_behaves_like 'RESTful project resources' do - let(:actions) { [:index, :create, :update, :destroy] } - let(:controller) { 'project_members' } - end -end - -# project_milestones GET /:project_id/milestones(.:format) milestones#index -# POST /:project_id/milestones(.:format) milestones#create -# new_project_milestone GET /:project_id/milestones/new(.:format) milestones#new -# edit_project_milestone GET /:project_id/milestones/:id/edit(.:format) milestones#edit -# project_milestone GET /:project_id/milestones/:id(.:format) milestones#show -# PUT /:project_id/milestones/:id(.:format) milestones#update -# DELETE /:project_id/milestones/:id(.:format) milestones#destroy -describe Projects::MilestonesController, 'routing' do - it_behaves_like 'RESTful project resources' do - let(:controller) { 'milestones' } - let(:actions) { [:index, :create, :new, :edit, :show, :update] } - end -end - -# project_labels GET /:project_id/labels(.:format) labels#index -describe Projects::LabelsController, 'routing' do - it 'to #index' do - expect(get('/gitlab/gitlabhq/labels')).to route_to('projects/labels#index', namespace_id: 'gitlab', project_id: 'gitlabhq') - end -end - -# sort_project_issues POST /:project_id/issues/sort(.:format) issues#sort -# bulk_update_project_issues POST /:project_id/issues/bulk_update(.:format) issues#bulk_update -# search_project_issues GET /:project_id/issues/search(.:format) issues#search -# project_issues GET /:project_id/issues(.:format) issues#index -# POST /:project_id/issues(.:format) issues#create -# new_project_issue GET /:project_id/issues/new(.:format) issues#new -# edit_project_issue GET /:project_id/issues/:id/edit(.:format) issues#edit -# project_issue GET /:project_id/issues/:id(.:format) issues#show -# PUT /:project_id/issues/:id(.:format) issues#update -# DELETE /:project_id/issues/:id(.:format) issues#destroy -describe Projects::IssuesController, 'routing' do - it 'to #bulk_update' do - expect(post('/gitlab/gitlabhq/issues/bulk_update')).to route_to('projects/issues#bulk_update', namespace_id: 'gitlab', project_id: 'gitlabhq') - end - - it_behaves_like 'RESTful project resources' do - let(:controller) { 'issues' } - let(:actions) { [:index, :create, :new, :edit, :show, :update] } - end -end - -# project_notes GET /:project_id/notes(.:format) notes#index -# POST /:project_id/notes(.:format) notes#create -# project_note DELETE /:project_id/notes/:id(.:format) notes#destroy -describe Projects::NotesController, 'routing' do - it_behaves_like 'RESTful project resources' do - let(:actions) { [:index, :create, :destroy] } - let(:controller) { 'notes' } - end -end - -# project_blame GET /:project_id/blame/:id(.:format) blame#show {id: /.+/, project_id: /[^\/]+/} -describe Projects::BlameController, 'routing' do - it 'to #show' do - expect(get('/gitlab/gitlabhq/blame/master/app/models/project.rb')).to route_to('projects/blame#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb') - expect(get('/gitlab/gitlabhq/blame/master/files.scss')).to route_to('projects/blame#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss') - end -end - -# project_blob GET /:project_id/blob/:id(.:format) blob#show {id: /.+/, project_id: /[^\/]+/} -describe Projects::BlobController, 'routing' do - it 'to #show' do - expect(get('/gitlab/gitlabhq/blob/master/app/models/project.rb')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb') - expect(get('/gitlab/gitlabhq/blob/master/app/models/compare.rb')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/compare.rb') - expect(get('/gitlab/gitlabhq/blob/master/app/models/diff.js')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/diff.js') - expect(get('/gitlab/gitlabhq/blob/master/files.scss')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss') - end -end - -# project_tree GET /:project_id/tree/:id(.:format) tree#show {id: /.+/, project_id: /[^\/]+/} -describe Projects::TreeController, 'routing' do - it 'to #show' do - expect(get('/gitlab/gitlabhq/tree/master/app/models/project.rb')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb') - expect(get('/gitlab/gitlabhq/tree/master/files.scss')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss') - end -end - -# project_find_file GET /:namespace_id/:project_id/find_file/*id(.:format) projects/find_file#show {:id=>/.+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?<!\.atom)/, :format=>/html/} -# project_files GET /:namespace_id/:project_id/files/*id(.:format) projects/find_file#list {:id=>/(?:[^.]|\.(?!json$))+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?<!\.atom)/, :format=>/json/} -describe Projects::FindFileController, 'routing' do - it 'to #show' do - expect(get('/gitlab/gitlabhq/find_file/master')).to route_to('projects/find_file#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master') - end - - it 'to #list' do - expect(get('/gitlab/gitlabhq/files/master.json')).to route_to('projects/find_file#list', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json') - end -end - -describe Projects::BlobController, 'routing' do - it 'to #edit' do - expect(get('/gitlab/gitlabhq/edit/master/app/models/project.rb')).to( - route_to('projects/blob#edit', - namespace_id: 'gitlab', project_id: 'gitlabhq', - id: 'master/app/models/project.rb')) - end - - it 'to #preview' do - expect(post('/gitlab/gitlabhq/preview/master/app/models/project.rb')).to( - route_to('projects/blob#preview', - namespace_id: 'gitlab', project_id: 'gitlabhq', - id: 'master/app/models/project.rb')) - end -end - -# project_compare_index GET /:project_id/compare(.:format) compare#index {id: /[^\/]+/, project_id: /[^\/]+/} -# POST /:project_id/compare(.:format) compare#create {id: /[^\/]+/, project_id: /[^\/]+/} -# project_compare /:project_id/compare/:from...:to(.:format) compare#show {from: /.+/, to: /.+/, id: /[^\/]+/, project_id: /[^\/]+/} -describe Projects::CompareController, 'routing' do - it 'to #index' do - expect(get('/gitlab/gitlabhq/compare')).to route_to('projects/compare#index', namespace_id: 'gitlab', project_id: 'gitlabhq') - end - - it 'to #compare' do - expect(post('/gitlab/gitlabhq/compare')).to route_to('projects/compare#create', namespace_id: 'gitlab', project_id: 'gitlabhq') - end - - it 'to #show' do - expect(get('/gitlab/gitlabhq/compare/master...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'master', to: 'stable') - expect(get('/gitlab/gitlabhq/compare/issue/1234...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'issue/1234', to: 'stable') - end -end - -describe Projects::NetworkController, 'routing' do - it 'to #show' do - expect(get('/gitlab/gitlabhq/network/master')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master') - expect(get('/gitlab/gitlabhq/network/ends-with.json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'ends-with.json') - expect(get('/gitlab/gitlabhq/network/master?format=json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json') - end -end - -describe Projects::GraphsController, 'routing' do - it 'to #show' do - expect(get('/gitlab/gitlabhq/graphs/master')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master') - expect(get('/gitlab/gitlabhq/graphs/ends-with.json')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'ends-with.json') - expect(get('/gitlab/gitlabhq/graphs/master?format=json')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json') - end -end - -describe Projects::ForksController, 'routing' do - it 'to #new' do - expect(get('/gitlab/gitlabhq/forks/new')).to route_to('projects/forks#new', namespace_id: 'gitlab', project_id: 'gitlabhq') - end - - it 'to #create' do - expect(post('/gitlab/gitlabhq/forks')).to route_to('projects/forks#create', namespace_id: 'gitlab', project_id: 'gitlabhq') - end -end - -# project_avatar DELETE /project/avatar(.:format) projects/avatars#destroy -describe Projects::AvatarsController, 'routing' do - it 'to #destroy' do - expect(delete('/gitlab/gitlabhq/avatar')).to route_to( - 'projects/avatars#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq') +describe 'project routing' do + before do + allow(Project).to receive(:find_with_namespace).and_return(false) + allow(Project).to receive(:find_with_namespace).with('gitlab/gitlabhq').and_return(true) + end + + # Shared examples for a resource inside a Project + # + # By default it tests all the default REST actions: index, create, new, edit, + # show, update, and destroy. You can remove actions by customizing the + # `actions` variable. + # + # It also expects a `controller` variable to be available which defines both + # the path to the resource as well as the controller name. + # + # Examples + # + # # Default behavior + # it_behaves_like 'RESTful project resources' do + # let(:controller) { 'issues' } + # end + # + # # Customizing actions + # it_behaves_like 'RESTful project resources' do + # let(:actions) { [:index] } + # let(:controller) { 'issues' } + # end + shared_examples 'RESTful project resources' do + let(:actions) { [:index, :create, :new, :edit, :show, :update, :destroy] } + + it 'to #index' do + expect(get("/gitlab/gitlabhq/#{controller}")).to route_to("projects/#{controller}#index", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:index) + end + + it 'to #create' do + expect(post("/gitlab/gitlabhq/#{controller}")).to route_to("projects/#{controller}#create", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:create) + end + + it 'to #new' do + expect(get("/gitlab/gitlabhq/#{controller}/new")).to route_to("projects/#{controller}#new", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:new) + end + + it 'to #edit' do + expect(get("/gitlab/gitlabhq/#{controller}/1/edit")).to route_to("projects/#{controller}#edit", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:edit) + end + + it 'to #show' do + expect(get("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#show", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:show) + end + + it 'to #update' do + expect(put("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#update", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:update) + end + + it 'to #destroy' do + expect(delete("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#destroy", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:destroy) + end + end + + # projects POST /projects(.:format) projects#create + # new_project GET /projects/new(.:format) projects#new + # files_project GET /:id/files(.:format) projects#files + # edit_project GET /:id/edit(.:format) projects#edit + # project GET /:id(.:format) projects#show + # PUT /:id(.:format) projects#update + # DELETE /:id(.:format) projects#destroy + # preview_markdown_project POST /:id/preview_markdown(.:format) projects#preview_markdown + describe ProjectsController, 'routing' do + it 'to #create' do + expect(post('/projects')).to route_to('projects#create') + end + + it 'to #new' do + expect(get('/projects/new')).to route_to('projects#new') + end + + it 'to #edit' do + expect(get('/gitlab/gitlabhq/edit')).to route_to('projects#edit', namespace_id: 'gitlab', id: 'gitlabhq') + end + + it 'to #autocomplete_sources' do + expect(get('/gitlab/gitlabhq/autocomplete_sources')).to route_to('projects#autocomplete_sources', namespace_id: 'gitlab', id: 'gitlabhq') + end + + describe 'to #show' do + context 'regular name' do + it { expect(get('/gitlab/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq') } + end + + context 'name with dot' do + before { allow(Project).to receive(:find_with_namespace).with('gitlab/gitlabhq.keys').and_return(true) } + + it { expect(get('/gitlab/gitlabhq.keys')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq.keys') } + end + + context 'with nested group' do + before { allow(Project).to receive(:find_with_namespace).with('gitlab/subgroup/gitlabhq').and_return(true) } + + it { expect(get('/gitlab/subgroup/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab/subgroup', id: 'gitlabhq') } + end + end + + it 'to #update' do + expect(put('/gitlab/gitlabhq')).to route_to('projects#update', namespace_id: 'gitlab', id: 'gitlabhq') + end + + it 'to #destroy' do + expect(delete('/gitlab/gitlabhq')).to route_to('projects#destroy', namespace_id: 'gitlab', id: 'gitlabhq') + end + + it 'to #preview_markdown' do + expect(post('/gitlab/gitlabhq/preview_markdown')).to( + route_to('projects#preview_markdown', namespace_id: 'gitlab', id: 'gitlabhq') + ) + end + end + + # pages_project_wikis GET /:project_id/wikis/pages(.:format) projects/wikis#pages + # history_project_wiki GET /:project_id/wikis/:id/history(.:format) projects/wikis#history + # project_wikis POST /:project_id/wikis(.:format) projects/wikis#create + # edit_project_wiki GET /:project_id/wikis/:id/edit(.:format) projects/wikis#edit + # project_wiki GET /:project_id/wikis/:id(.:format) projects/wikis#show + # DELETE /:project_id/wikis/:id(.:format) projects/wikis#destroy + describe Projects::WikisController, 'routing' do + it 'to #pages' do + expect(get('/gitlab/gitlabhq/wikis/pages')).to route_to('projects/wikis#pages', namespace_id: 'gitlab', project_id: 'gitlabhq') + end + + it 'to #history' do + expect(get('/gitlab/gitlabhq/wikis/1/history')).to route_to('projects/wikis#history', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') + end + + it_behaves_like 'RESTful project resources' do + let(:actions) { [:create, :edit, :show, :destroy] } + let(:controller) { 'wikis' } + end + end + + # branches_project_repository GET /:project_id/repository/branches(.:format) projects/repositories#branches + # tags_project_repository GET /:project_id/repository/tags(.:format) projects/repositories#tags + # archive_project_repository GET /:project_id/repository/archive(.:format) projects/repositories#archive + # edit_project_repository GET /:project_id/repository/edit(.:format) projects/repositories#edit + describe Projects::RepositoriesController, 'routing' do + it 'to #archive' do + expect(get('/gitlab/gitlabhq/repository/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq') + end + + it 'to #archive format:zip' do + expect(get('/gitlab/gitlabhq/repository/archive.zip')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'zip') + end + + it 'to #archive format:tar.bz2' do + expect(get('/gitlab/gitlabhq/repository/archive.tar.bz2')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'tar.bz2') + end + end + + describe Projects::BranchesController, 'routing' do + it 'to #branches' do + expect(get('/gitlab/gitlabhq/branches')).to route_to('projects/branches#index', namespace_id: 'gitlab', project_id: 'gitlabhq') + expect(delete('/gitlab/gitlabhq/branches/feature%2345')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45') + expect(delete('/gitlab/gitlabhq/branches/feature%2B45')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45') + expect(delete('/gitlab/gitlabhq/branches/feature@45')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45') + expect(delete('/gitlab/gitlabhq/branches/feature%2345/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45/foo/bar/baz') + expect(delete('/gitlab/gitlabhq/branches/feature%2B45/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45/foo/bar/baz') + expect(delete('/gitlab/gitlabhq/branches/feature@45/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45/foo/bar/baz') + end + end + + describe Projects::TagsController, 'routing' do + it 'to #tags' do + expect(get('/gitlab/gitlabhq/tags')).to route_to('projects/tags#index', namespace_id: 'gitlab', project_id: 'gitlabhq') + expect(delete('/gitlab/gitlabhq/tags/feature%2345')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45') + expect(delete('/gitlab/gitlabhq/tags/feature%2B45')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45') + expect(delete('/gitlab/gitlabhq/tags/feature@45')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45') + expect(delete('/gitlab/gitlabhq/tags/feature%2345/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45/foo/bar/baz') + expect(delete('/gitlab/gitlabhq/tags/feature%2B45/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45/foo/bar/baz') + expect(delete('/gitlab/gitlabhq/tags/feature@45/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45/foo/bar/baz') + end + end + + # project_deploy_keys GET /:project_id/deploy_keys(.:format) deploy_keys#index + # POST /:project_id/deploy_keys(.:format) deploy_keys#create + # new_project_deploy_key GET /:project_id/deploy_keys/new(.:format) deploy_keys#new + # project_deploy_key GET /:project_id/deploy_keys/:id(.:format) deploy_keys#show + # DELETE /:project_id/deploy_keys/:id(.:format) deploy_keys#destroy + describe Projects::DeployKeysController, 'routing' do + it_behaves_like 'RESTful project resources' do + let(:actions) { [:index, :new, :create] } + let(:controller) { 'deploy_keys' } + end + end + + # project_protected_branches GET /:project_id/protected_branches(.:format) protected_branches#index + # POST /:project_id/protected_branches(.:format) protected_branches#create + # project_protected_branch DELETE /:project_id/protected_branches/:id(.:format) protected_branches#destroy + describe Projects::ProtectedBranchesController, 'routing' do + it_behaves_like 'RESTful project resources' do + let(:actions) { [:index, :create, :destroy] } + let(:controller) { 'protected_branches' } + end + end + + # switch_project_refs GET /:project_id/refs/switch(.:format) refs#switch + # logs_tree_project_ref GET /:project_id/refs/:id/logs_tree(.:format) refs#logs_tree + # logs_file_project_ref GET /:project_id/refs/:id/logs_tree/:path(.:format) refs#logs_tree + describe Projects::RefsController, 'routing' do + it 'to #switch' do + expect(get('/gitlab/gitlabhq/refs/switch')).to route_to('projects/refs#switch', namespace_id: 'gitlab', project_id: 'gitlabhq') + end + + it 'to #logs_tree' do + expect(get('/gitlab/gitlabhq/refs/stable/logs_tree')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'stable') + expect(get('/gitlab/gitlabhq/refs/feature%2345/logs_tree')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45') + expect(get('/gitlab/gitlabhq/refs/feature%2B45/logs_tree')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45') + expect(get('/gitlab/gitlabhq/refs/feature@45/logs_tree')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45') + expect(get('/gitlab/gitlabhq/refs/stable/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'stable', path: 'foo/bar/baz') + expect(get('/gitlab/gitlabhq/refs/feature%2345/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45', path: 'foo/bar/baz') + expect(get('/gitlab/gitlabhq/refs/feature%2B45/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45', path: 'foo/bar/baz') + expect(get('/gitlab/gitlabhq/refs/feature@45/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45', path: 'foo/bar/baz') + expect(get('/gitlab/gitlabhq/refs/stable/logs_tree/files.scss')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'stable', path: 'files.scss') + end + end + + # diffs_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/diffs(.:format) projects/merge_requests#diffs + # commits_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/commits(.:format) projects/merge_requests#commits + # merge_namespace_project_merge_request POST /:namespace_id/:project_id/merge_requests/:id/merge(.:format) projects/merge_requests#merge + # merge_check_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/merge_check(.:format) projects/merge_requests#merge_check + # ci_status_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/ci_status(.:format) projects/merge_requests#ci_status + # toggle_subscription_namespace_project_merge_request POST /:namespace_id/:project_id/merge_requests/:id/toggle_subscription(.:format) projects/merge_requests#toggle_subscription + # branch_from_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/branch_from(.:format) projects/merge_requests#branch_from + # branch_to_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/branch_to(.:format) projects/merge_requests#branch_to + # update_branches_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/update_branches(.:format) projects/merge_requests#update_branches + # namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests(.:format) projects/merge_requests#index + # POST /:namespace_id/:project_id/merge_requests(.:format) projects/merge_requests#create + # new_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/new(.:format) projects/merge_requests#new + # edit_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/edit(.:format) projects/merge_requests#edit + # namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#show + # PATCH /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#update + # PUT /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#update + describe Projects::MergeRequestsController, 'routing' do + it 'to #diffs' do + expect(get('/gitlab/gitlabhq/merge_requests/1/diffs')).to route_to('projects/merge_requests#diffs', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') + end + + it 'to #commits' do + expect(get('/gitlab/gitlabhq/merge_requests/1/commits')).to route_to('projects/merge_requests#commits', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') + end + + it 'to #merge' do + expect(post('/gitlab/gitlabhq/merge_requests/1/merge')).to route_to( + 'projects/merge_requests#merge', + namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1' + ) + end + + it 'to #merge_check' do + expect(get('/gitlab/gitlabhq/merge_requests/1/merge_check')).to route_to('projects/merge_requests#merge_check', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') + end + + it 'to #branch_from' do + expect(get('/gitlab/gitlabhq/merge_requests/branch_from')).to route_to('projects/merge_requests#branch_from', namespace_id: 'gitlab', project_id: 'gitlabhq') + end + + it 'to #branch_to' do + expect(get('/gitlab/gitlabhq/merge_requests/branch_to')).to route_to('projects/merge_requests#branch_to', namespace_id: 'gitlab', project_id: 'gitlabhq') + end + + it 'to #show' do + expect(get('/gitlab/gitlabhq/merge_requests/1.diff')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'diff') + expect(get('/gitlab/gitlabhq/merge_requests/1.patch')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'patch') + end + + it_behaves_like 'RESTful project resources' do + let(:controller) { 'merge_requests' } + let(:actions) { [:index, :create, :new, :edit, :show, :update] } + end + end + + # raw_project_snippet GET /:project_id/snippets/:id/raw(.:format) snippets#raw + # project_snippets GET /:project_id/snippets(.:format) snippets#index + # POST /:project_id/snippets(.:format) snippets#create + # new_project_snippet GET /:project_id/snippets/new(.:format) snippets#new + # edit_project_snippet GET /:project_id/snippets/:id/edit(.:format) snippets#edit + # project_snippet GET /:project_id/snippets/:id(.:format) snippets#show + # PUT /:project_id/snippets/:id(.:format) snippets#update + # DELETE /:project_id/snippets/:id(.:format) snippets#destroy + describe SnippetsController, 'routing' do + it 'to #raw' do + expect(get('/gitlab/gitlabhq/snippets/1/raw')).to route_to('projects/snippets#raw', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') + end + + it 'to #index' do + expect(get('/gitlab/gitlabhq/snippets')).to route_to('projects/snippets#index', namespace_id: 'gitlab', project_id: 'gitlabhq') + end + + it 'to #create' do + expect(post('/gitlab/gitlabhq/snippets')).to route_to('projects/snippets#create', namespace_id: 'gitlab', project_id: 'gitlabhq') + end + + it 'to #new' do + expect(get('/gitlab/gitlabhq/snippets/new')).to route_to('projects/snippets#new', namespace_id: 'gitlab', project_id: 'gitlabhq') + end + + it 'to #edit' do + expect(get('/gitlab/gitlabhq/snippets/1/edit')).to route_to('projects/snippets#edit', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') + end + + it 'to #show' do + expect(get('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') + end + + it 'to #update' do + expect(put('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#update', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') + end + + it 'to #destroy' do + expect(delete('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') + end + end + + # test_project_hook GET /:project_id/hooks/:id/test(.:format) hooks#test + # project_hooks GET /:project_id/hooks(.:format) hooks#index + # POST /:project_id/hooks(.:format) hooks#create + # project_hook DELETE /:project_id/hooks/:id(.:format) hooks#destroy + describe Projects::HooksController, 'routing' do + it 'to #test' do + expect(get('/gitlab/gitlabhq/hooks/1/test')).to route_to('projects/hooks#test', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') + end + + it_behaves_like 'RESTful project resources' do + let(:actions) { [:index, :create, :destroy] } + let(:controller) { 'hooks' } + end + end + + # project_commit GET /:project_id/commit/:id(.:format) commit#show {id: /\h{7,40}/, project_id: /[^\/]+/} + describe Projects::CommitController, 'routing' do + it 'to #show' do + expect(get('/gitlab/gitlabhq/commit/4246fbd')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd') + expect(get('/gitlab/gitlabhq/commit/4246fbd.diff')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd', format: 'diff') + expect(get('/gitlab/gitlabhq/commit/4246fbd.patch')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd', format: 'patch') + expect(get('/gitlab/gitlabhq/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5') + end + end + + # patch_project_commit GET /:project_id/commits/:id/patch(.:format) commits#patch + # project_commits GET /:project_id/commits(.:format) commits#index + # POST /:project_id/commits(.:format) commits#create + # project_commit GET /:project_id/commits/:id(.:format) commits#show + describe Projects::CommitsController, 'routing' do + it_behaves_like 'RESTful project resources' do + let(:actions) { [:show] } + let(:controller) { 'commits' } + end + + it 'to #show' do + expect(get('/gitlab/gitlabhq/commits/master.atom')).to route_to('projects/commits#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master.atom') + end + end + + # project_project_members GET /:project_id/project_members(.:format) project_members#index + # POST /:project_id/project_members(.:format) project_members#create + # PUT /:project_id/project_members/:id(.:format) project_members#update + # DELETE /:project_id/project_members/:id(.:format) project_members#destroy + describe Projects::ProjectMembersController, 'routing' do + it_behaves_like 'RESTful project resources' do + let(:actions) { [:index, :create, :update, :destroy] } + let(:controller) { 'project_members' } + end + end + + # project_milestones GET /:project_id/milestones(.:format) milestones#index + # POST /:project_id/milestones(.:format) milestones#create + # new_project_milestone GET /:project_id/milestones/new(.:format) milestones#new + # edit_project_milestone GET /:project_id/milestones/:id/edit(.:format) milestones#edit + # project_milestone GET /:project_id/milestones/:id(.:format) milestones#show + # PUT /:project_id/milestones/:id(.:format) milestones#update + # DELETE /:project_id/milestones/:id(.:format) milestones#destroy + describe Projects::MilestonesController, 'routing' do + it_behaves_like 'RESTful project resources' do + let(:controller) { 'milestones' } + let(:actions) { [:index, :create, :new, :edit, :show, :update] } + end + end + + # project_labels GET /:project_id/labels(.:format) labels#index + describe Projects::LabelsController, 'routing' do + it 'to #index' do + expect(get('/gitlab/gitlabhq/labels')).to route_to('projects/labels#index', namespace_id: 'gitlab', project_id: 'gitlabhq') + end + end + + # sort_project_issues POST /:project_id/issues/sort(.:format) issues#sort + # bulk_update_project_issues POST /:project_id/issues/bulk_update(.:format) issues#bulk_update + # search_project_issues GET /:project_id/issues/search(.:format) issues#search + # project_issues GET /:project_id/issues(.:format) issues#index + # POST /:project_id/issues(.:format) issues#create + # new_project_issue GET /:project_id/issues/new(.:format) issues#new + # edit_project_issue GET /:project_id/issues/:id/edit(.:format) issues#edit + # project_issue GET /:project_id/issues/:id(.:format) issues#show + # PUT /:project_id/issues/:id(.:format) issues#update + # DELETE /:project_id/issues/:id(.:format) issues#destroy + describe Projects::IssuesController, 'routing' do + it 'to #bulk_update' do + expect(post('/gitlab/gitlabhq/issues/bulk_update')).to route_to('projects/issues#bulk_update', namespace_id: 'gitlab', project_id: 'gitlabhq') + end + + it_behaves_like 'RESTful project resources' do + let(:controller) { 'issues' } + let(:actions) { [:index, :create, :new, :edit, :show, :update] } + end + end + + # project_notes GET /:project_id/notes(.:format) notes#index + # POST /:project_id/notes(.:format) notes#create + # project_note DELETE /:project_id/notes/:id(.:format) notes#destroy + describe Projects::NotesController, 'routing' do + it_behaves_like 'RESTful project resources' do + let(:actions) { [:index, :create, :destroy] } + let(:controller) { 'notes' } + end + end + + # project_blame GET /:project_id/blame/:id(.:format) blame#show {id: /.+/, project_id: /[^\/]+/} + describe Projects::BlameController, 'routing' do + it 'to #show' do + expect(get('/gitlab/gitlabhq/blame/master/app/models/project.rb')).to route_to('projects/blame#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb') + expect(get('/gitlab/gitlabhq/blame/master/files.scss')).to route_to('projects/blame#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss') + end + end + + # project_blob GET /:project_id/blob/:id(.:format) blob#show {id: /.+/, project_id: /[^\/]+/} + describe Projects::BlobController, 'routing' do + it 'to #show' do + expect(get('/gitlab/gitlabhq/blob/master/app/models/project.rb')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb') + expect(get('/gitlab/gitlabhq/blob/master/app/models/compare.rb')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/compare.rb') + expect(get('/gitlab/gitlabhq/blob/master/app/models/diff.js')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/diff.js') + expect(get('/gitlab/gitlabhq/blob/master/files.scss')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss') + end + end + + # project_tree GET /:project_id/tree/:id(.:format) tree#show {id: /.+/, project_id: /[^\/]+/} + describe Projects::TreeController, 'routing' do + it 'to #show' do + expect(get('/gitlab/gitlabhq/tree/master/app/models/project.rb')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb') + expect(get('/gitlab/gitlabhq/tree/master/files.scss')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss') + end + end + + # project_find_file GET /:namespace_id/:project_id/find_file/*id(.:format) projects/find_file#show {:id=>/.+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?<!\.atom)/, :format=>/html/} + # project_files GET /:namespace_id/:project_id/files/*id(.:format) projects/find_file#list {:id=>/(?:[^.]|\.(?!json$))+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?<!\.atom)/, :format=>/json/} + describe Projects::FindFileController, 'routing' do + it 'to #show' do + expect(get('/gitlab/gitlabhq/find_file/master')).to route_to('projects/find_file#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master') + end + + it 'to #list' do + expect(get('/gitlab/gitlabhq/files/master.json')).to route_to('projects/find_file#list', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json') + end + end + + describe Projects::BlobController, 'routing' do + it 'to #edit' do + expect(get('/gitlab/gitlabhq/edit/master/app/models/project.rb')).to( + route_to('projects/blob#edit', + namespace_id: 'gitlab', project_id: 'gitlabhq', + id: 'master/app/models/project.rb')) + end + + it 'to #preview' do + expect(post('/gitlab/gitlabhq/preview/master/app/models/project.rb')).to( + route_to('projects/blob#preview', + namespace_id: 'gitlab', project_id: 'gitlabhq', + id: 'master/app/models/project.rb')) + end + end + + # project_compare_index GET /:project_id/compare(.:format) compare#index {id: /[^\/]+/, project_id: /[^\/]+/} + # POST /:project_id/compare(.:format) compare#create {id: /[^\/]+/, project_id: /[^\/]+/} + # project_compare /:project_id/compare/:from...:to(.:format) compare#show {from: /.+/, to: /.+/, id: /[^\/]+/, project_id: /[^\/]+/} + describe Projects::CompareController, 'routing' do + it 'to #index' do + expect(get('/gitlab/gitlabhq/compare')).to route_to('projects/compare#index', namespace_id: 'gitlab', project_id: 'gitlabhq') + end + + it 'to #compare' do + expect(post('/gitlab/gitlabhq/compare')).to route_to('projects/compare#create', namespace_id: 'gitlab', project_id: 'gitlabhq') + end + + it 'to #show' do + expect(get('/gitlab/gitlabhq/compare/master...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'master', to: 'stable') + expect(get('/gitlab/gitlabhq/compare/issue/1234...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'issue/1234', to: 'stable') + end + end + + describe Projects::NetworkController, 'routing' do + it 'to #show' do + expect(get('/gitlab/gitlabhq/network/master')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master') + expect(get('/gitlab/gitlabhq/network/ends-with.json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'ends-with.json') + expect(get('/gitlab/gitlabhq/network/master?format=json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json') + end + end + + describe Projects::GraphsController, 'routing' do + it 'to #show' do + expect(get('/gitlab/gitlabhq/graphs/master')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master') + expect(get('/gitlab/gitlabhq/graphs/ends-with.json')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'ends-with.json') + expect(get('/gitlab/gitlabhq/graphs/master?format=json')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json') + end + end + + describe Projects::ForksController, 'routing' do + it 'to #new' do + expect(get('/gitlab/gitlabhq/forks/new')).to route_to('projects/forks#new', namespace_id: 'gitlab', project_id: 'gitlabhq') + end + + it 'to #create' do + expect(post('/gitlab/gitlabhq/forks')).to route_to('projects/forks#create', namespace_id: 'gitlab', project_id: 'gitlabhq') + end + end + + # project_avatar DELETE /project/avatar(.:format) projects/avatars#destroy + describe Projects::AvatarsController, 'routing' do + it 'to #destroy' do + expect(delete('/gitlab/gitlabhq/avatar')).to route_to( + 'projects/avatars#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq') + end end end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index f15c45cbaac..9f6defe1450 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -9,7 +9,7 @@ require 'spec_helper' # user_calendar_activities GET /u/:username/calendar_activities(.:format) describe UsersController, "routing" do it "to #show" do - allow(User).to receive(:find_by).and_return(true) + allow_any_instance_of(UserUrlConstrainer).to receive(:matches?).and_return(true) expect(get("/User")).to route_to('users#show', username: 'User') end @@ -195,6 +195,8 @@ describe Profiles::KeysController, "routing" do # get all the ssh-keys of a user it "to #get_keys" do + allow_any_instance_of(UserUrlConstrainer).to receive(:matches?).and_return(true) + expect(get("/foo.keys")).to route_to('profiles/keys#get_keys', username: 'foo') end end @@ -263,13 +265,17 @@ end describe "Groups", "routing" do let(:name) { 'complex.group-namegit' } + before { allow_any_instance_of(GroupUrlConstrainer).to receive(:matches?).and_return(true) } + it "to #show" do expect(get("/groups/#{name}")).to route_to('groups#show', id: name) end - it "also display group#show on the short path" do - allow(Group).to receive(:find_by).and_return(true) + it "also supports nested groups" do + expect(get("/#{name}/#{name}")).to route_to('groups#show', id: "#{name}/#{name}") + end + it "also display group#show on the short path" do expect(get("/#{name}")).to route_to('groups#show', id: name) end @@ -284,6 +290,10 @@ describe "Groups", "routing" do it "to #members" do expect(get("/groups/#{name}/group_members")).to route_to('groups/group_members#index', group_id: name) end + + it "also display group#show with slash in the path" do + expect(get('/group/subgroup')).to route_to('groups#show', id: 'group/subgroup') + end end describe HealthCheckController, 'routing' do -- cgit v1.2.1 From d2985eb57201a98b3bb9c1abebdabd2061eabd10 Mon Sep 17 00:00:00 2001 From: Robert Schilling <rschilling@student.tugraz.at> Date: Wed, 23 Nov 2016 13:34:08 +0100 Subject: Grapify the sidekiq metrics API --- lib/api/sidekiq_metrics.rb | 36 ++++-------------------------------- 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/lib/api/sidekiq_metrics.rb b/lib/api/sidekiq_metrics.rb index d3d6827dc54..11f2b40269a 100644 --- a/lib/api/sidekiq_metrics.rb +++ b/lib/api/sidekiq_metrics.rb @@ -39,50 +39,22 @@ module API end end - # Get Sidekiq Queue metrics - # - # Parameters: - # None - # - # Example: - # GET /sidekiq/queue_metrics - # + desc 'Get the Sidekiq queue metrics' get 'sidekiq/queue_metrics' do { queues: queue_metrics } end - # Get Sidekiq Process metrics - # - # Parameters: - # None - # - # Example: - # GET /sidekiq/process_metrics - # + desc 'Get the Sidekiq process metrics' get 'sidekiq/process_metrics' do { processes: process_metrics } end - # Get Sidekiq Job statistics - # - # Parameters: - # None - # - # Example: - # GET /sidekiq/job_stats - # + desc 'Get the Sidekiq job statistics' get 'sidekiq/job_stats' do { jobs: job_stats } end - # Get Sidekiq Compound metrics. Includes all previous metrics - # - # Parameters: - # None - # - # Example: - # GET /sidekiq/compound_metrics - # + desc 'Get the Sidekiq Compound metrics. Includes queue, process, and job statistics' get 'sidekiq/compound_metrics' do { queues: queue_metrics, processes: process_metrics, jobs: job_stats } end -- cgit v1.2.1 From 71ad3d294ec6ddfb36346e19e6b50ec5eb8d4ef2 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Wed, 23 Nov 2016 13:27:45 +0100 Subject: Clear test build storage directory before each example --- spec/support/setup_builds_storage.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/support/setup_builds_storage.rb b/spec/support/setup_builds_storage.rb index fd729434898..2e7c88bfc09 100644 --- a/spec/support/setup_builds_storage.rb +++ b/spec/support/setup_builds_storage.rb @@ -7,11 +7,12 @@ RSpec.configure do |config| Settings.gitlab_ci['builds_path'] = builds_path end - config.before(:each) do + config.before(:all) do FileUtils.mkdir_p(builds_path) end - config.after(:each) do + config.before(:each) do FileUtils.rm_rf(builds_path) + FileUtils.mkdir_p(builds_path) end end -- cgit v1.2.1 From 5371da341e9d7768ebab8e159b3e2cc8fad1d827 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Wed, 23 Nov 2016 14:14:04 +0100 Subject: Remove event caching code Flushing the events cache worked by updating a recent number of rows in the "events" table. This has the result that on PostgreSQL a lot of dead tuples are produced on a regular basis. This in turn means that PostgreSQL will spend considerable amounts of time vacuuming this table. This in turn can lead to an increase of database load. For GitLab.com we measured the impact of not using events caching and found no measurable increase in response timings. Meanwhile not flushing the events cache lead to the "events" table having no more dead tuples as now rows are only inserted into this table. As a result of this we are hereby removing events caching as it does not appear to help and only increases database load. For more information see the following comment: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6578#note_18864037 --- app/controllers/profiles/avatars_controller.rb | 1 - app/controllers/projects/avatars_controller.rb | 1 - app/models/event.rb | 6 ------ app/models/issue.rb | 12 ------------ app/models/merge_request.rb | 12 ------------ app/models/note.rb | 13 ------------- app/models/project.rb | 17 ----------------- app/models/user.rb | 14 -------------- app/services/issuable_base_service.rb | 2 -- app/services/notes/delete_service.rb | 1 - app/services/notes/update_service.rb | 1 - app/services/projects/transfer_service.rb | 3 --- app/uploaders/avatar_uploader.rb | 6 ------ app/views/events/_event.html.haml | 19 +++++++++---------- changelogs/unreleased/events-cache-invalidation.yml | 4 ++++ 15 files changed, 13 insertions(+), 99 deletions(-) create mode 100644 changelogs/unreleased/events-cache-invalidation.yml diff --git a/app/controllers/profiles/avatars_controller.rb b/app/controllers/profiles/avatars_controller.rb index f193adb46b4..daa51ae41df 100644 --- a/app/controllers/profiles/avatars_controller.rb +++ b/app/controllers/profiles/avatars_controller.rb @@ -4,7 +4,6 @@ class Profiles::AvatarsController < Profiles::ApplicationController @user.remove_avatar! @user.save - @user.reset_events_cache redirect_to profile_path end diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb index ada7db3c552..53788687076 100644 --- a/app/controllers/projects/avatars_controller.rb +++ b/app/controllers/projects/avatars_controller.rb @@ -20,7 +20,6 @@ class Projects::AvatarsController < Projects::ApplicationController @project.remove_avatar! @project.save - @project.reset_events_cache redirect_to edit_project_path(@project) end diff --git a/app/models/event.rb b/app/models/event.rb index 21eaca917b8..216dba46e74 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -43,12 +43,6 @@ class Event < ActiveRecord::Base scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) } class << self - def reset_event_cache_for(target) - Event.where(target_id: target.id, target_type: target.class.to_s). - order('id DESC').limit(100). - update_all(updated_at: Time.now) - end - # Update Gitlab::ContributionsCalendar#activity_dates if this changes def contributions where("action = ? OR (target_type in (?) AND action in (?))", diff --git a/app/models/issue.rb b/app/models/issue.rb index 6e8f5d3c422..544f830cc69 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -182,18 +182,6 @@ class Issue < ActiveRecord::Base branches_with_iid - branches_with_merge_request end - # Reset issue events cache - # - # Since we do cache @event we need to reset cache in special cases: - # * when an issue is updated - # Events cache stored like events/23-20130109142513. - # The cache key includes updated_at timestamp. - # Thus it will automatically generate a new fragment - # when the event is updated because the key changes. - def reset_events_cache - Event.reset_event_cache_for(self) - end - # To allow polymorphism with MergeRequest. def source_project project diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 6c3c093d084..bf9edb0b823 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -601,18 +601,6 @@ class MergeRequest < ActiveRecord::Base self.target_project.repository.branch_names.include?(self.target_branch) end - # Reset merge request events cache - # - # Since we do cache @event we need to reset cache in special cases: - # * when a merge request is updated - # Events cache stored like events/23-20130109142513. - # The cache key includes updated_at timestamp. - # Thus it will automatically generate a new fragment - # when the event is updated because the key changes. - def reset_events_cache - Event.reset_event_cache_for(self) - end - def merge_commit_message message = "Merge branch '#{source_branch}' into '#{target_branch}'\n\n" message << "#{title}\n\n" diff --git a/app/models/note.rb b/app/models/note.rb index 9ff5e308ed2..9881506edc8 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -198,19 +198,6 @@ class Note < ActiveRecord::Base super(noteable_type.to_s.classify.constantize.base_class.to_s) end - # Reset notes events cache - # - # Since we do cache @event we need to reset cache in special cases: - # * when a note is updated - # * when a note is removed - # Events cache stored like events/23-20130109142513. - # The cache key includes updated_at timestamp. - # Thus it will automatically generate a new fragment - # when the event is updated because the key changes. - def reset_events_cache - Event.reset_event_cache_for(self) - end - def editable? !system? end diff --git a/app/models/project.rb b/app/models/project.rb index 76c1fc4945d..21d9ed0f51f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -973,7 +973,6 @@ class Project < ActiveRecord::Base begin gitlab_shell.mv_repository(repository_storage_path, "#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki") send_move_instructions(old_path_with_namespace) - reset_events_cache @old_path_with_namespace = old_path_with_namespace @@ -1040,22 +1039,6 @@ class Project < ActiveRecord::Base attrs end - # Reset events cache related to this project - # - # Since we do cache @event we need to reset cache in special cases: - # * when project was moved - # * when project was renamed - # * when the project avatar changes - # Events cache stored like events/23-20130109142513. - # The cache key includes updated_at timestamp. - # Thus it will automatically generate a new fragment - # when the event is updated because the key changes. - def reset_events_cache - Event.where(project_id: self.id). - order('id DESC').limit(100). - update_all(updated_at: Time.now) - end - def project_member(user) project_members.find_by(user_id: user) end diff --git a/app/models/user.rb b/app/models/user.rb index 29fb849940a..8a67aa94e79 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -704,20 +704,6 @@ class User < ActiveRecord::Base project.project_member(self) end - # Reset project events cache related to this user - # - # Since we do cache @event we need to reset cache in special cases: - # * when the user changes their avatar - # Events cache stored like events/23-20130109142513. - # The cache key includes updated_at timestamp. - # Thus it will automatically generate a new fragment - # when the event is updated because the key changes. - def reset_events_cache - Event.where(author_id: id). - order('id DESC').limit(1000). - update_all(updated_at: Time.now) - end - def full_website_url return "http://#{website_url}" if website_url !~ /\Ahttps?:\/\// diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 575795788de..d698b295e6d 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -184,8 +184,6 @@ class IssuableBaseService < BaseService params[:label_ids] = process_label_ids(params, existing_label_ids: issuable.label_ids) if params.present? && update_issuable(issuable, params) - issuable.reset_events_cache - # We do not touch as it will affect a update on updated_at field ActiveRecord::Base.no_touching do handle_common_system_notes(issuable, old_labels: old_labels) diff --git a/app/services/notes/delete_service.rb b/app/services/notes/delete_service.rb index 7f1b30ec84e..a673e8e9dde 100644 --- a/app/services/notes/delete_service.rb +++ b/app/services/notes/delete_service.rb @@ -2,7 +2,6 @@ module Notes class DeleteService < BaseService def execute(note) note.destroy - note.reset_events_cache end end end diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb index 1361b1e0300..75a4b3ed826 100644 --- a/app/services/notes/update_service.rb +++ b/app/services/notes/update_service.rb @@ -5,7 +5,6 @@ module Notes note.update_attributes(params.merge(updated_by: current_user)) note.create_new_cross_references!(current_user) - note.reset_events_cache if note.previous_changes.include?('note') TodoService.new.update_note(note, current_user) diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 28470f59807..34ec575e808 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -61,9 +61,6 @@ module Projects # Move missing group labels to project Labels::TransferService.new(current_user, old_group, project).execute - # clear project cached events - project.reset_events_cache - # Move uploads Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path) diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb index 71ff14a3f20..38683fdf6d7 100644 --- a/app/uploaders/avatar_uploader.rb +++ b/app/uploaders/avatar_uploader.rb @@ -3,16 +3,10 @@ class AvatarUploader < CarrierWave::Uploader::Base storage :file - after :store, :reset_events_cache - def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end - def reset_events_cache(file) - model.reset_events_cache if model.is_a?(User) - end - def exists? model.avatar.file && model.avatar.file.exists? end diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 5c318cd3b8b..a0bd14df209 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -3,14 +3,13 @@ .event-item-timestamp #{time_ago_with_tooltip(event.created_at)} - = cache [event, current_application_settings, "v2.2"] do - = author_avatar(event, size: 40) + = author_avatar(event, size: 40) - - if event.created_project? - = render "events/event/created_project", event: event - - elsif event.push? - = render "events/event/push", event: event - - elsif event.commented? - = render "events/event/note", event: event - - else - = render "events/event/common", event: event + - if event.created_project? + = render "events/event/created_project", event: event + - elsif event.push? + = render "events/event/push", event: event + - elsif event.commented? + = render "events/event/note", event: event + - else + = render "events/event/common", event: event diff --git a/changelogs/unreleased/events-cache-invalidation.yml b/changelogs/unreleased/events-cache-invalidation.yml new file mode 100644 index 00000000000..2b30f4dcbce --- /dev/null +++ b/changelogs/unreleased/events-cache-invalidation.yml @@ -0,0 +1,4 @@ +--- +title: Remove caching of events data +merge_request: 6578 +author: -- cgit v1.2.1 From 934eaee94e0a16b4f76a19521a13ce86fdea8d9c Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 23 Nov 2016 14:00:06 +0000 Subject: Fixed dragging issues on issue boards Closes #24654 --- app/assets/javascripts/boards/components/board_card.js.es6 | 3 +++ app/assets/javascripts/boards/components/board_list.js.es6 | 6 ++---- app/views/projects/boards/components/_board_list.html.haml | 2 +- app/views/projects/boards/components/_card.html.haml | 1 + changelogs/unreleased/issue-boards-dragging-fix.yml | 4 ++++ 5 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 changelogs/unreleased/issue-boards-dragging-fix.yml diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6 index b1afbe7d97e..2299dafd217 100644 --- a/app/assets/javascripts/boards/components/board_card.js.es6 +++ b/app/assets/javascripts/boards/components/board_card.js.es6 @@ -54,6 +54,9 @@ mouseDown () { this.showDetail = true; }, + mouseMove() { + this.showDetail = false; + }, showIssue (e) { const targetTagName = e.target.tagName.toLowerCase(); diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index 379f4f0d72b..8e91cbfac75 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -94,12 +94,10 @@ gl.issueBoards.onStart(); }, onAdd: (e) => { - // Add the element back to original list to allow Vue to handle DOM updates - e.from.appendChild(e.item); + gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue); this.$nextTick(() => { - // Update the issues once we know the element has been moved - gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue); + e.item.remove(); }); }, }); diff --git a/app/views/projects/boards/components/_board_list.html.haml b/app/views/projects/boards/components/_board_list.html.haml index d86e0ed8540..34fdb1f6a74 100644 --- a/app/views/projects/boards/components/_board_list.html.haml +++ b/app/views/projects/boards/components/_board_list.html.haml @@ -35,7 +35,7 @@ ":issue" => "issue", ":issue-link-base" => "issueLinkBase", ":disabled" => "disabled", - "key" => "id" } + ":key" => "issue.id" } %li.board-list-count.text-center{ "v-if" => "showCount" } = icon("spinner spin", "v-show" => "list.loadingMore" ) %span{ "v-if" => "list.issues.length === list.issuesSize" } diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml index 72b31b8cdae..34effac17b2 100644 --- a/app/views/projects/boards/components/_card.html.haml +++ b/app/views/projects/boards/components/_card.html.haml @@ -1,6 +1,7 @@ %li.card{ ":class" => '{ "user-can-drag": !disabled && issue.id, "is-disabled": disabled || !issue.id, "is-active": issueDetailVisible }', ":index" => "index", "@mousedown" => "mouseDown", + "@mousemove" => "mouseMove", "@mouseup" => "showIssue($event)" } %h4.card-title = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential") diff --git a/changelogs/unreleased/issue-boards-dragging-fix.yml b/changelogs/unreleased/issue-boards-dragging-fix.yml new file mode 100644 index 00000000000..565e09b930b --- /dev/null +++ b/changelogs/unreleased/issue-boards-dragging-fix.yml @@ -0,0 +1,4 @@ +--- +title: Fixed issue boards dragging card removing random issues +merge_request: +author: -- cgit v1.2.1 From 395a772786f91e40f935008a7cdb06ec0fe1f280 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Wed, 23 Nov 2016 15:18:05 +0100 Subject: Add support of Chrome/Chromium in requirements.md [ci skip] --- doc/install/requirements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 42b515761e0..e942346e2d7 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -143,6 +143,6 @@ On a very active server (10,000 active users) the Sidekiq process can use 1GB+ o ## Supported web browsers -We support the current and the previous major release of Firefox, Safari and Microsoft browsers (Microsoft Edge and Internet Explorer 11). +We support the current and the previous major release of Firefox, Chrome/Chromium, Safari and Microsoft browsers (Microsoft Edge and Internet Explorer 11). Each time a new browser version is released, we begin supporting that version and stop supporting the third most recent version. -- cgit v1.2.1 From 5636825fd0bd8b11ab9d85350246cdcc0fb28afe Mon Sep 17 00:00:00 2001 From: Semyon Pupkov <mail@semyonpupkov.com> Date: Tue, 22 Nov 2016 14:32:48 +0500 Subject: Move abuse report spinach test to rspec part of https://gitlab.com/gitlab-org/gitlab-ce/issues/23036 --- .../move-abuse-report-spinach-test-to-rspec.yml | 4 +++ features/abuse_report.feature | 17 ------------ features/steps/abuse_reports.rb | 32 ---------------------- spec/features/abuse_report_spec.rb | 24 ++++++++++++++++ 4 files changed, 28 insertions(+), 49 deletions(-) create mode 100644 changelogs/unreleased/move-abuse-report-spinach-test-to-rspec.yml delete mode 100644 features/abuse_report.feature delete mode 100644 features/steps/abuse_reports.rb create mode 100644 spec/features/abuse_report_spec.rb diff --git a/changelogs/unreleased/move-abuse-report-spinach-test-to-rspec.yml b/changelogs/unreleased/move-abuse-report-spinach-test-to-rspec.yml new file mode 100644 index 00000000000..9de7477c200 --- /dev/null +++ b/changelogs/unreleased/move-abuse-report-spinach-test-to-rspec.yml @@ -0,0 +1,4 @@ +--- +title: Move abuse report spinach test to rspec +merge_request: 7659 +author: Semyon Pupkov diff --git a/features/abuse_report.feature b/features/abuse_report.feature deleted file mode 100644 index 212972a762a..00000000000 --- a/features/abuse_report.feature +++ /dev/null @@ -1,17 +0,0 @@ -Feature: Abuse reports - Background: - Given I sign in as a user - And user "Mike" exists - - Scenario: Report abuse - Given I visit "Mike" user page - 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 see a red "Report abuse" button diff --git a/features/steps/abuse_reports.rb b/features/steps/abuse_reports.rb deleted file mode 100644 index 499accb0b08..00000000000 --- a/features/steps/abuse_reports.rb +++ /dev/null @@ -1,32 +0,0 @@ -class Spinach::Features::AbuseReports < Spinach::FeatureSteps - include SharedAuthentication - - step 'I visit "Mike" user page' do - visit user_path(user_mike) - end - - step 'I click "Report abuse" button' do - click_link 'Report abuse' - end - - step 'I fill and submit abuse form' do - fill_in 'abuse_report_message', with: 'This user send spam' - click_button 'Send report' - end - - step 'I should see success message' do - page.should have_content 'Thank you for your report' - end - - step 'user "Mike" exists' do - user_mike - end - - step 'I should see a red "Report abuse" button' do - expect(page).to have_button("Already reported for abuse") - end - - def user_mike - @user_mike ||= create(:user, name: 'Mike') - end -end diff --git a/spec/features/abuse_report_spec.rb b/spec/features/abuse_report_spec.rb new file mode 100644 index 00000000000..1e11fb756b2 --- /dev/null +++ b/spec/features/abuse_report_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +feature 'Abuse reports', feature: true do + let(:another_user) { create(:user) } + + before do + login_as :user + end + + scenario 'Report abuse' do + visit user_path(another_user) + + click_link 'Report abuse' + + fill_in 'abuse_report_message', with: 'This user send spam' + click_button 'Send report' + + expect(page).to have_content 'Thank you for your report' + + visit user_path(another_user) + + expect(page).to have_button("Already reported for abuse") + end +end -- cgit v1.2.1 From 56d2906293a9edb022449a214258d05e15471f83 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 23 Nov 2016 14:48:01 +0000 Subject: Adds polyfill for CustomEvent --- .../javascripts/lib/utils/custom_event_polyfill.js.es6 | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 app/assets/javascripts/lib/utils/custom_event_polyfill.js.es6 diff --git a/app/assets/javascripts/lib/utils/custom_event_polyfill.js.es6 b/app/assets/javascripts/lib/utils/custom_event_polyfill.js.es6 new file mode 100644 index 00000000000..5ae978010c9 --- /dev/null +++ b/app/assets/javascripts/lib/utils/custom_event_polyfill.js.es6 @@ -0,0 +1,12 @@ +/** + * CustomEvent support for IE + */ +if (typeof window.CustomEvent !== 'function') { + window.CustomEvent = function CustomEvent(e, params) { + const options = params || { bubbles: false, cancelable: false, detail: undefined }; + const evt = document.createEvent('CustomEvent'); + evt.initCustomEvent(e, options.bubbles, options.cancelable, options.detail); + return evt; + }; + window.CustomEvent.prototype = window.Event.prototype; +} -- cgit v1.2.1 From 6892293e730b98d324b5d92663349e9ee42a70a9 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" <lbennett@gitlab.com> Date: Wed, 23 Nov 2016 14:56:28 +0000 Subject: Use default `closest` if available! --- app/assets/javascripts/extensions/element.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/extensions/element.js.es6 b/app/assets/javascripts/extensions/element.js.es6 index afb2f0d6956..6d9b0c4bc3e 100644 --- a/app/assets/javascripts/extensions/element.js.es6 +++ b/app/assets/javascripts/extensions/element.js.es6 @@ -3,7 +3,7 @@ Element.prototype.matches = Element.prototype.matches || Element.prototype.msMatchesSelector; -Element.prototype.closest = function closest(selector, selectedElement = this) { +Element.prototype.closest = Element.prototype.closest || function closest(selector, selectedElement = this) { if (!selectedElement) return; return selectedElement.matches(selector) ? selectedElement : Element.prototype.closest(selector, selectedElement.parentElement); }; -- cgit v1.2.1 From c18f96cfe950b11e2784479cbc7e518667273143 Mon Sep 17 00:00:00 2001 From: Semyon Pupkov <mail@semyonpupkov.com> Date: Wed, 23 Nov 2016 20:13:24 +0500 Subject: Move admin spam spinach test to Rspec https://gitlab.com/gitlab-org/gitlab-ce/issues/23036 --- .../move-admin-spam-spinach-test-to-rspec.yml | 4 ++++ features/admin/spam_logs.feature | 8 ------- features/steps/admin/spam_logs.rb | 28 ---------------------- spec/features/admin/admin_browse_spam_logs_spec.rb | 22 +++++++++++++++++ 4 files changed, 26 insertions(+), 36 deletions(-) create mode 100644 changelogs/unreleased/move-admin-spam-spinach-test-to-rspec.yml delete mode 100644 features/admin/spam_logs.feature delete mode 100644 features/steps/admin/spam_logs.rb create mode 100644 spec/features/admin/admin_browse_spam_logs_spec.rb diff --git a/changelogs/unreleased/move-admin-spam-spinach-test-to-rspec.yml b/changelogs/unreleased/move-admin-spam-spinach-test-to-rspec.yml new file mode 100644 index 00000000000..a7ec2c20554 --- /dev/null +++ b/changelogs/unreleased/move-admin-spam-spinach-test-to-rspec.yml @@ -0,0 +1,4 @@ +--- +title: Move admin spam spinach test to Rspec +merge_request: 7708 +author: Semyon Pupkov diff --git a/features/admin/spam_logs.feature b/features/admin/spam_logs.feature deleted file mode 100644 index 92a5389e3a4..00000000000 --- a/features/admin/spam_logs.feature +++ /dev/null @@ -1,8 +0,0 @@ -Feature: Admin spam logs - Background: - Given I sign in as an admin - And spam logs exist - - Scenario: Browse spam logs - When I visit spam logs page - Then I should see list of spam logs diff --git a/features/steps/admin/spam_logs.rb b/features/steps/admin/spam_logs.rb deleted file mode 100644 index ad825fd414c..00000000000 --- a/features/steps/admin/spam_logs.rb +++ /dev/null @@ -1,28 +0,0 @@ -class Spinach::Features::AdminSpamLogs < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedAdmin - - step 'I should see list of spam logs' do - expect(page).to have_content('Spam Logs') - expect(page).to have_content spam_log.source_ip - expect(page).to have_content spam_log.noteable_type - expect(page).to have_content 'N' - expect(page).to have_content spam_log.title - expect(page).to have_content truncate(spam_log.description) - expect(page).to have_link('Remove user') - expect(page).to have_link('Block user') - end - - step 'spam logs exist' do - create(:spam_log) - end - - def spam_log - @spam_log ||= SpamLog.first - end - - def truncate(description) - "#{spam_log.description[0...97]}..." - end -end diff --git a/spec/features/admin/admin_browse_spam_logs_spec.rb b/spec/features/admin/admin_browse_spam_logs_spec.rb new file mode 100644 index 00000000000..562ace92598 --- /dev/null +++ b/spec/features/admin/admin_browse_spam_logs_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe 'Admin browse spam logs' do + let!(:spam_log) { create(:spam_log) } + + before do + login_as :admin + end + + scenario 'Browse spam logs' do + visit admin_spam_logs_path + + expect(page).to have_content('Spam Logs') + expect(page).to have_content(spam_log.source_ip) + expect(page).to have_content(spam_log.noteable_type) + expect(page).to have_content('N') + expect(page).to have_content(spam_log.title) + expect(page).to have_content("#{spam_log.description[0...97]}...") + expect(page).to have_link('Remove user') + expect(page).to have_link('Block user') + end +end -- cgit v1.2.1 From 8badb65d0190d30f4e0d4419f642b32aa4aa4343 Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Wed, 23 Nov 2016 21:39:38 +0600 Subject: fixes non-retina shadow and browser zoom issue --- app/assets/stylesheets/pages/notes.scss | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 54c7caed8ed..e66c1f8d072 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -75,6 +75,8 @@ ul.notes { display: none; padding: 10px 0 0; cursor: pointer; + position: relative; + z-index: 2; &:hover { color: $gl-link-color; @@ -118,11 +120,11 @@ ul.notes { &::after { content: ''; width: 100%; - height: 20px; + height: 67px; position: absolute; left: 0; - bottom: 50px; - background: linear-gradient(rgba($gray-light, .3) 0, $white-light); + bottom: 0; + background: linear-gradient(rgba($gray-light, 0.1) -100px, $white-light 100%); } &.hide-shade { -- cgit v1.2.1 From 42fe0cac09fc335a8704b23bfbd219645914ee41 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Mon, 21 Nov 2016 09:09:19 +0100 Subject: Upgrade grape-entity to 0.6.0 --- Gemfile | 2 +- Gemfile.lock | 5 +++-- changelogs/unreleased/zj-upgrade-grape.yml | 2 +- spec/serializers/environment_serializer_spec.rb | 4 ---- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index 14ea7ec7ea0..cd463fbd82f 100644 --- a/Gemfile +++ b/Gemfile @@ -68,7 +68,7 @@ gem 'github-linguist', '~> 4.7.0', require: 'linguist' # API gem 'grape', '~> 0.15.0' -gem 'grape-entity', '~> 0.5.2' +gem 'grape-entity', '~> 0.6.0' gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' # Pagination diff --git a/Gemfile.lock b/Gemfile.lock index 331ffd89148..cf2ebaa00d6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -316,7 +316,8 @@ GEM rack-accept rack-mount virtus (>= 1.0.0) - grape-entity (0.5.2) + grape-entity (0.6.0) + activesupport multi_json (>= 1.3.2) haml (4.0.7) tilt @@ -868,7 +869,7 @@ DEPENDENCIES gollum-rugged_adapter (~> 0.4.2) gon (~> 6.1.0) grape (~> 0.15.0) - grape-entity (~> 0.5.2) + grape-entity (~> 0.6.0) haml_lint (~> 0.18.2) hamlit (~> 2.6.1) health_check (~> 2.2.0) diff --git a/changelogs/unreleased/zj-upgrade-grape.yml b/changelogs/unreleased/zj-upgrade-grape.yml index 75a72283d1e..1df42d98733 100644 --- a/changelogs/unreleased/zj-upgrade-grape.yml +++ b/changelogs/unreleased/zj-upgrade-grape.yml @@ -1,4 +1,4 @@ --- -title: Update grape entity to 0.5.2 +title: Update grape entity to 0.6.0 merge_request: 7491 author: diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb index 8f95c9250b0..b7ed4eb0239 100644 --- a/spec/serializers/environment_serializer_spec.rb +++ b/spec/serializers/environment_serializer_spec.rb @@ -27,10 +27,6 @@ describe EnvironmentSerializer do let(:deployable) { create(:ci_build) } let(:resource) { deployment.environment } - it 'it generates payload for single object' do - expect(json).to be_an_instance_of Hash - end - it 'contains important elements of environment' do expect(json) .to include(:name, :external_url, :environment_path, :last_deployment) -- cgit v1.2.1 From 4b3c1e56ae7468d0234240dd211d54a7abd39f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Mon, 21 Nov 2016 16:31:51 +0100 Subject: Move LfsHelper to a new LfsRequest concern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also create a new WorkhorseRequest concern Signed-off-by: Rémy Coutable <remy@rymai.me> --- app/controllers/concerns/lfs_request.rb | 109 +++++++++++++++++++++ app/controllers/concerns/workhorse_request.rb | 13 +++ .../projects/git_http_client_controller.rb | 16 +-- app/controllers/projects/git_http_controller.rb | 12 ++- app/controllers/projects/lfs_api_controller.rb | 21 ++-- app/controllers/projects/lfs_storage_controller.rb | 7 +- app/helpers/lfs_helper.rb | 85 ---------------- 7 files changed, 150 insertions(+), 113 deletions(-) create mode 100644 app/controllers/concerns/lfs_request.rb create mode 100644 app/controllers/concerns/workhorse_request.rb delete mode 100644 app/helpers/lfs_helper.rb diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb new file mode 100644 index 00000000000..ed22b1e5470 --- /dev/null +++ b/app/controllers/concerns/lfs_request.rb @@ -0,0 +1,109 @@ +# This concern assumes: +# - a `#project` accessor +# - a `#user` accessor +# - a `#authentication_result` accessor +# - a `#can?(object, action, subject)` method +# - a `#ci?` method +# - a `#download_request?` method +# - a `#upload_request?` method +# - a `#has_authentication_ability?(ability)` method +module LfsRequest + extend ActiveSupport::Concern + + included do + before_action :require_lfs_enabled! + before_action :lfs_check_access! + end + + private + + def require_lfs_enabled! + return if Gitlab.config.lfs.enabled + + render( + json: { + message: 'Git LFS is not enabled on this GitLab server, contact your admin.', + documentation_url: help_url, + }, + status: 501 + ) + end + + def lfs_check_access! + return if download_request? && lfs_download_access? + return if upload_request? && lfs_upload_access? + + if project.public? || can?(user, :read_project, project) + lfs_forbidden! + else + render_lfs_not_found + end + end + + def lfs_forbidden! + render_lfs_forbidden + end + + def render_lfs_forbidden + render( + json: { + message: 'Access forbidden. Check your access level.', + documentation_url: help_url, + }, + content_type: "application/vnd.git-lfs+json", + status: 403 + ) + end + + def render_lfs_not_found + render( + json: { + message: 'Not found.', + documentation_url: help_url, + }, + content_type: "application/vnd.git-lfs+json", + status: 404 + ) + end + + def lfs_download_access? + return false unless project.lfs_enabled? + + ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code? + end + + def lfs_upload_access? + return false unless project.lfs_enabled? + + has_authentication_ability?(:push_code) && can?(user, :push_code, project) + end + + def lfs_deploy_token? + authentication_result.lfs_deploy_token?(project) + end + + def user_can_download_code? + has_authentication_ability?(:download_code) && can?(user, :download_code, project) + end + + def build_can_download_code? + has_authentication_ability?(:build_download_code) && can?(user, :build_download_code, project) + end + + def storage_project + @storage_project ||= begin + result = project + + loop do + break unless result.forked? + result = result.forked_from_project + end + + result + end + end + + def objects + @objects ||= (params[:objects] || []).to_a + end +end diff --git a/app/controllers/concerns/workhorse_request.rb b/app/controllers/concerns/workhorse_request.rb new file mode 100644 index 00000000000..43c0f1b173c --- /dev/null +++ b/app/controllers/concerns/workhorse_request.rb @@ -0,0 +1,13 @@ +module WorkhorseRequest + extend ActiveSupport::Concern + + included do + before_action :verify_workhorse_api! + end + + private + + def verify_workhorse_api! + Gitlab::Workhorse.verify_api_request!(request.headers) + end +end diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index 3f41916e6d3..8714349e27f 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -18,6 +18,14 @@ class Projects::GitHttpClientController < Projects::ApplicationController private + def download_request? + raise NotImplementedError + end + + def upload_request? + raise NotImplementedError + end + def authenticate_user @authentication_result = Gitlab::Auth::Result.new @@ -130,10 +138,6 @@ class Projects::GitHttpClientController < Projects::ApplicationController authentication_result.ci?(project) end - def lfs_deploy_token? - authentication_result.lfs_deploy_token?(project) - end - def authentication_has_download_access? has_authentication_ability?(:download_code) || has_authentication_ability?(:build_download_code) end @@ -149,8 +153,4 @@ class Projects::GitHttpClientController < Projects::ApplicationController def authentication_project authentication_result.project end - - def verify_workhorse_api! - Gitlab::Workhorse.verify_api_request!(request.headers) - end end diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index 13caeb42d40..9184dcccac5 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -1,7 +1,5 @@ -# This file should be identical in GitLab Community Edition and Enterprise Edition - class Projects::GitHttpController < Projects::GitHttpClientController - before_action :verify_workhorse_api! + include WorkhorseRequest # GET /foo/bar.git/info/refs?service=git-upload-pack (git pull) # GET /foo/bar.git/info/refs?service=git-receive-pack (git push) @@ -67,14 +65,18 @@ class Projects::GitHttpController < Projects::GitHttpClientController end def render_denied - if user && user.can?(:read_project, project) - render plain: 'Access denied', status: :forbidden + if user && can?(user, :read_project, project) + render plain: access_denied_message, status: :forbidden else # Do not leak information about project existence render_not_found end end + def access_denied_message + 'Access denied' + end + def upload_pack_allowed? return false unless Gitlab.config.gitlab_shell.upload_pack diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb index 2d493276941..440259b643c 100644 --- a/app/controllers/projects/lfs_api_controller.rb +++ b/app/controllers/projects/lfs_api_controller.rb @@ -1,8 +1,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController - include LfsHelper + include LfsRequest - before_action :require_lfs_enabled! - before_action :lfs_check_access!, except: [:deprecated] + skip_before_action :lfs_check_access!, only: [:deprecated] def batch unless objects.present? @@ -31,6 +30,14 @@ class Projects::LfsApiController < Projects::GitHttpClientController private + def download_request? + params[:operation] == 'download' + end + + def upload_request? + params[:operation] == 'upload' + end + def existing_oids @existing_oids ||= begin storage_project.lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid) @@ -79,12 +86,4 @@ class Projects::LfsApiController < Projects::GitHttpClientController } } end - - def download_request? - params[:operation] == 'download' - end - - def upload_request? - params[:operation] == 'upload' - end end diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb index 9005b104e90..32759672b6c 100644 --- a/app/controllers/projects/lfs_storage_controller.rb +++ b/app/controllers/projects/lfs_storage_controller.rb @@ -1,9 +1,8 @@ class Projects::LfsStorageController < Projects::GitHttpClientController - include LfsHelper + include LfsRequest + include WorkhorseRequest - before_action :require_lfs_enabled! - before_action :lfs_check_access! - before_action :verify_workhorse_api!, only: [:upload_authorize] + skip_before_action :verify_workhorse_api!, only: [:download, :upload_finalize] def download lfs_object = LfsObject.find_by_oid(oid) diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb deleted file mode 100644 index 2425c3a8bc8..00000000000 --- a/app/helpers/lfs_helper.rb +++ /dev/null @@ -1,85 +0,0 @@ -module LfsHelper - include Gitlab::Routing.url_helpers - - def require_lfs_enabled! - return if Gitlab.config.lfs.enabled - - render( - json: { - message: 'Git LFS is not enabled on this GitLab server, contact your admin.', - documentation_url: help_url, - }, - status: 501 - ) - end - - def lfs_check_access! - return if download_request? && lfs_download_access? - return if upload_request? && lfs_upload_access? - - if project.public? || (user && user.can?(:read_project, project)) - render_lfs_forbidden - else - render_lfs_not_found - end - end - - def lfs_download_access? - return false unless project.lfs_enabled? - - ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code? - end - - def objects - @objects ||= (params[:objects] || []).to_a - end - - def user_can_download_code? - has_authentication_ability?(:download_code) && can?(user, :download_code, project) - end - - def build_can_download_code? - has_authentication_ability?(:build_download_code) && can?(user, :build_download_code, project) - end - - def lfs_upload_access? - return false unless project.lfs_enabled? - - has_authentication_ability?(:push_code) && can?(user, :push_code, project) - end - - def render_lfs_forbidden - render( - json: { - message: 'Access forbidden. Check your access level.', - documentation_url: help_url, - }, - content_type: "application/vnd.git-lfs+json", - status: 403 - ) - end - - def render_lfs_not_found - render( - json: { - message: 'Not found.', - documentation_url: help_url, - }, - content_type: "application/vnd.git-lfs+json", - status: 404 - ) - end - - def storage_project - @storage_project ||= begin - result = project - - loop do - break unless result.forked? - result = result.forked_from_project - end - - result - end - end -end -- cgit v1.2.1 From 73f6218ec4f3cf9e403880990565673b730666d9 Mon Sep 17 00:00:00 2001 From: Semyon Pupkov <mail@semyonpupkov.com> Date: Wed, 23 Nov 2016 10:57:12 +0500 Subject: Move admin abuse report spinach test to rspec https://gitlab.com/gitlab-org/gitlab-ce/issues/23036 --- .../move-admin-abuse-report-spinach-test-to-rspec.yml | 4 ++++ features/admin/abuse_report.feature | 8 -------- features/steps/admin/abuse_reports.rb | 15 --------------- spec/features/admin/admin_abuse_reports_spec.rb | 16 ++++++++++------ 4 files changed, 14 insertions(+), 29 deletions(-) create mode 100644 changelogs/unreleased/move-admin-abuse-report-spinach-test-to-rspec.yml delete mode 100644 features/admin/abuse_report.feature delete mode 100644 features/steps/admin/abuse_reports.rb diff --git a/changelogs/unreleased/move-admin-abuse-report-spinach-test-to-rspec.yml b/changelogs/unreleased/move-admin-abuse-report-spinach-test-to-rspec.yml new file mode 100644 index 00000000000..fb70fa2955a --- /dev/null +++ b/changelogs/unreleased/move-admin-abuse-report-spinach-test-to-rspec.yml @@ -0,0 +1,4 @@ +--- +title: Move admin abuse report spinach test to rspec +merge_request: 7691 +author: Semyon Pupkov diff --git a/features/admin/abuse_report.feature b/features/admin/abuse_report.feature deleted file mode 100644 index 7d4ec2556e5..00000000000 --- a/features/admin/abuse_report.feature +++ /dev/null @@ -1,8 +0,0 @@ -Feature: Admin Abuse reports - Background: - Given I sign in as an admin - And abuse reports exist - - Scenario: Browse abuse reports - When I visit abuse reports page - Then I should see list of abuse reports diff --git a/features/steps/admin/abuse_reports.rb b/features/steps/admin/abuse_reports.rb deleted file mode 100644 index 0149416c919..00000000000 --- a/features/steps/admin/abuse_reports.rb +++ /dev/null @@ -1,15 +0,0 @@ -class Spinach::Features::AdminAbuseReports < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedAdmin - - step 'I should see list of abuse reports' do - page.should have_content("Abuse Reports") - page.should have_content AbuseReport.first.message - page.should have_link("Remove user") - end - - step 'abuse reports exist' do - create(:abuse_report) - end -end diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb index c1731e6414a..7fcfe5a54c7 100644 --- a/spec/features/admin/admin_abuse_reports_spec.rb +++ b/spec/features/admin/admin_abuse_reports_spec.rb @@ -4,17 +4,21 @@ describe "Admin::AbuseReports", feature: true, js: true do let(:user) { create(:user) } context 'as an admin' do + before do + login_as :admin + end + describe 'if a user has been reported for abuse' do - before do - create(:abuse_report, user: user) - login_as :admin - end + let!(:abuse_report) { create(:abuse_report, user: user) } describe 'in the abuse report view' do - it "presents a link to the user's profile" do + it 'presents information about abuse report' do visit admin_abuse_reports_path - expect(page).to have_link user.name, href: user_path(user) + expect(page).to have_content('Abuse Reports') + expect(page).to have_content(abuse_report.message) + expect(page).to have_link(user.name, href: user_path(user)) + expect(page).to have_link('Remove user') end end -- cgit v1.2.1 From 0a1f519c2af4bd5f67e1a74701d98583047da117 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran <alfredo@gitlab.com> Date: Tue, 22 Nov 2016 20:04:57 -0500 Subject: Take only objects for the events list --- .../javascripts/cycle_analytics/cycle_analytics_store.js.es6 | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 index 9b905874167..be732971c7f 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 @@ -62,9 +62,11 @@ this.state.events = this.decorateEvents(events); }, decorateEvents(events) { - const newEvents = events; + const newEvents = []; + + events.forEach((item) => { + if (!item) return; - newEvents.forEach((item) => { item.totalTime = item.total_time; item.author.webUrl = item.author.web_url; item.author.avatarUrl = item.author.avatar_url; @@ -79,6 +81,8 @@ delete item.created_at; delete item.short_sha; delete item.commit_url; + + newEvents.push(item); }); return newEvents; -- cgit v1.2.1 From 9ad2dba250e3facc18ff2f21217ebe006451dd8c Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Thu, 24 Nov 2016 02:33:55 +0800 Subject: Use Commit#author so we share logic and cache Closes #24900 --- lib/gitlab/identifier.rb | 6 ++---- spec/lib/gitlab/identifier_spec.rb | 5 ++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/gitlab/identifier.rb b/lib/gitlab/identifier.rb index c5acf18beb5..94678b6ec40 100644 --- a/lib/gitlab/identifier.rb +++ b/lib/gitlab/identifier.rb @@ -21,10 +21,8 @@ module Gitlab return if !commit || !commit.author_email - email = commit.author_email - - identify_with_cache(:email, email) do - User.find_by_any_email(email) + identify_with_cache(:email, commit.author_email) do + commit.author end end diff --git a/spec/lib/gitlab/identifier_spec.rb b/spec/lib/gitlab/identifier_spec.rb index f42c4453dd1..bb758a8a202 100644 --- a/spec/lib/gitlab/identifier_spec.rb +++ b/spec/lib/gitlab/identifier_spec.rb @@ -40,7 +40,7 @@ describe Gitlab::Identifier do describe '#identify_using_commit' do it "returns the User for an existing commit author's Email address" do - commit = double(:commit, author_email: user.email) + commit = double(:commit, author: user, author_email: user.email) expect(project).to receive(:commit).with('123').and_return(commit) @@ -62,10 +62,9 @@ describe Gitlab::Identifier do end it 'caches the found users per Email' do - commit = double(:commit, author_email: user.email) + commit = double(:commit, author: user, author_email: user.email) expect(project).to receive(:commit).with('123').twice.and_return(commit) - expect(User).to receive(:find_by_any_email).once.and_call_original 2.times do expect(identifier.identify_using_commit(project, '123')).to eq(user) -- cgit v1.2.1 From 0dc108507479b7dbd1d31ceab1989e13f766d613 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Wed, 23 Nov 2016 19:53:12 +0100 Subject: Remove tests which do not add value --- spec/serializers/analytics_build_serializer_spec.rb | 4 ---- spec/serializers/analytics_issue_serializer_spec.rb | 4 ---- spec/serializers/analytics_merge_request_serializer_spec.rb | 4 ---- 3 files changed, 12 deletions(-) diff --git a/spec/serializers/analytics_build_serializer_spec.rb b/spec/serializers/analytics_build_serializer_spec.rb index a0a9d9a5f12..f0551c78671 100644 --- a/spec/serializers/analytics_build_serializer_spec.rb +++ b/spec/serializers/analytics_build_serializer_spec.rb @@ -10,10 +10,6 @@ describe AnalyticsBuildSerializer do let(:resource) { create(:ci_build) } context 'when there is a single object provided' do - it 'it generates payload for single object' do - expect(json).to be_an_instance_of Hash - end - it 'contains important elements of analyticsBuild' do expect(json) .to include(:name, :branch, :short_sha, :date, :total_time, :url, :author) diff --git a/spec/serializers/analytics_issue_serializer_spec.rb b/spec/serializers/analytics_issue_serializer_spec.rb index 2842e1ba52f..6afbb2df35c 100644 --- a/spec/serializers/analytics_issue_serializer_spec.rb +++ b/spec/serializers/analytics_issue_serializer_spec.rb @@ -22,10 +22,6 @@ describe AnalyticsIssueSerializer do end context 'when there is a single object provided' do - it 'it generates payload for single object' do - expect(json).to be_an_instance_of Hash - end - it 'contains important elements of the issue' do expect(json).to include(:title, :iid, :created_at, :total_time, :url, :author) end diff --git a/spec/serializers/analytics_merge_request_serializer_spec.rb b/spec/serializers/analytics_merge_request_serializer_spec.rb index 564207984df..cdfae27193f 100644 --- a/spec/serializers/analytics_merge_request_serializer_spec.rb +++ b/spec/serializers/analytics_merge_request_serializer_spec.rb @@ -23,10 +23,6 @@ describe AnalyticsMergeRequestSerializer do end context 'when there is a single object provided' do - it 'it generates payload for single object' do - expect(json).to be_an_instance_of Hash - end - it 'contains important elements of the merge request' do expect(json).to include(:title, :iid, :created_at, :total_time, :url, :author, :state) end -- cgit v1.2.1 From b7e29af0b51bf39ae3c761b349494719cd860ebe Mon Sep 17 00:00:00 2001 From: Patricio Cano <suprnova32@gmail.com> Date: Wed, 23 Nov 2016 13:04:20 -0600 Subject: Fix `LFS enabled` select box. It was not displaying the proper value and had an out of place look to it. --- app/views/projects/edit.html.haml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 0aa8801c2d8..3a5af2723c6 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -92,14 +92,15 @@ = project_feature_access_select(:wiki_access_level) - if Gitlab.config.lfs.enabled && current_user.admin? - .checkbox - = f.label :lfs_enabled do - = f.check_box :lfs_enabled - %strong LFS - %br - %span.descr + .row + .col-md-9 + = f.label :lfs_enabled, 'LFS', class: 'label-light' + %span.help-block Git Large File Storage = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + .col-md-3 + = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control', data: { field: 'lfs_enabled' } + - if Gitlab.config.registry.enabled .form-group.js-container-registry{ style: ("display: none;" if @project.project_feature.send(:repository_access_level) == 0) } -- cgit v1.2.1 From a7486bcb77fff619e4e6b0ec05423b08db3d0fe9 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 23 Nov 2016 14:34:49 +0000 Subject: Fixed commit time not rendering after initial page load Closes #24862 --- app/assets/javascripts/commits.js | 6 ++++-- changelogs/unreleased/fixed-commit-timeago.yml | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/fixed-commit-timeago.yml diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index 951fb338f9d..3627aaf5080 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-undef, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-undef, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, padded-blocks, max-len, prefer-arrow-callback */ (function() { this.CommitsList = (function() { function CommitsList() {} @@ -13,7 +13,9 @@ return false; } }); - Pager.init(limit, false); + Pager.init(limit, false, false, function() { + gl.utils.localTimeAgo($('.js-timeago')); + }); this.content = $("#commits-list"); this.searchField = $("#commits-search"); return this.initSearch(); diff --git a/changelogs/unreleased/fixed-commit-timeago.yml b/changelogs/unreleased/fixed-commit-timeago.yml new file mode 100644 index 00000000000..295d8db63d0 --- /dev/null +++ b/changelogs/unreleased/fixed-commit-timeago.yml @@ -0,0 +1,4 @@ +--- +title: Fixed commit timeago not rendering after initial page +merge_request: +author: -- cgit v1.2.1 From 8434a642f19798f8ed8ce283af6a1006920bef30 Mon Sep 17 00:00:00 2001 From: vrod <rodrigues.victor.vsr@gmail.com> Date: Sat, 19 Nov 2016 15:58:31 -0200 Subject: Simplify copy on "Create a new list" dropdown in Issue Boards --- app/assets/stylesheets/pages/boards.scss | 2 +- app/views/shared/issuable/_filter.html.haml | 4 ++-- .../shared/issuable/_label_page_default.html.haml | 8 +++----- .../simplify-create-new-list-issue-boards.yml | 4 ++++ doc/user/project/img/issue_board.png | Bin 81649 -> 90664 bytes doc/user/project/img/issue_board_add_list.png | Bin 9516 -> 23632 bytes .../project/img/issue_board_welcome_message.png | Bin 29956 -> 97419 bytes doc/user/project/issue_board.md | 2 +- spec/features/boards/boards_spec.rb | 16 ++++++++-------- 9 files changed, 19 insertions(+), 17 deletions(-) create mode 100644 changelogs/unreleased/simplify-create-new-list-issue-boards.yml diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 4f5753f6fc6..4327f8bf640 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -243,7 +243,7 @@ } .issue-boards-search { - width: 335px; + width: 290px; .form-control { display: inline-block; diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index ed93857e6d4..b7e5e928993 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -40,9 +40,9 @@ - if can?(current_user, :admin_list, @project) .dropdown.pull-right %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } } - Create new list + Add list .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable - = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" } + = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" } - if can?(current_user, :admin_label, @project) = render partial: "shared/issuable/label_page_create" = dropdown_loading diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml index c0dc63be2bf..a8f01026ca5 100644 --- a/app/views/shared/issuable/_label_page_default.html.haml +++ b/app/views/shared/issuable/_label_page_default.html.haml @@ -1,17 +1,15 @@ - title = local_assigns.fetch(:title, 'Assign labels') - show_create = local_assigns.fetch(:show_create, true) - show_footer = local_assigns.fetch(:show_footer, true) -- filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search labels') +- filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search') - show_boards_content = local_assigns.fetch(:show_boards_content, false) .dropdown-page-one = dropdown_title(title) - if show_boards_content .issue-board-dropdown-content %p - Each label that exists in your issue tracker can have its own dedicated - list. Select a label below to add a list to your Board and it will - automatically be populated with issues that have that label. To create - a list for a label that doesn't exist yet, simply create the label below. + Create lists from the labels you use in your project. Issues with that + label will automatically be added to the list. = dropdown_filter(filter_placeholder) = dropdown_content - if @project && show_footer diff --git a/changelogs/unreleased/simplify-create-new-list-issue-boards.yml b/changelogs/unreleased/simplify-create-new-list-issue-boards.yml new file mode 100644 index 00000000000..ca11e3b94a7 --- /dev/null +++ b/changelogs/unreleased/simplify-create-new-list-issue-boards.yml @@ -0,0 +1,4 @@ +--- +title: Simplify copy on "Create a new list" dropdown in Issue Boards +merge_request: 7605 +author: Victor Rodrigues diff --git a/doc/user/project/img/issue_board.png b/doc/user/project/img/issue_board.png index 2a35a615d70..95e8532e908 100644 Binary files a/doc/user/project/img/issue_board.png and b/doc/user/project/img/issue_board.png differ diff --git a/doc/user/project/img/issue_board_add_list.png b/doc/user/project/img/issue_board_add_list.png index aa1a4ca4cfa..cdfc466d23f 100644 Binary files a/doc/user/project/img/issue_board_add_list.png and b/doc/user/project/img/issue_board_add_list.png differ diff --git a/doc/user/project/img/issue_board_welcome_message.png b/doc/user/project/img/issue_board_welcome_message.png index aa25cfb5b37..5bfdac88dde 100644 Binary files a/doc/user/project/img/issue_board_welcome_message.png and b/doc/user/project/img/issue_board_welcome_message.png differ diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index 4a6c0d88241..d1ae57c00d7 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -72,7 +72,7 @@ the list will be created and filled with the issues that have that label. ## Creating a new list -Create a new list by clicking on the **Create new list** button at the upper +Create a new list by clicking on the **Add list** button at the upper right corner of the Issue Board. ![Issue Board welcome message](img/issue_board_add_list.png) diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 4aa84fb65d9..973d5b286e9 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -158,7 +158,7 @@ describe 'Issue Boards', feature: true, js: true do end it 'removes checkmark in new list dropdown after deleting' do - click_button 'Create new list' + click_button 'Add list' wait_for_ajax page.within(find('.board:nth-child(2)')) do @@ -304,7 +304,7 @@ describe 'Issue Boards', feature: true, js: true do context 'new list' do it 'shows all labels in new list dropdown' do - click_button 'Create new list' + click_button 'Add list' wait_for_ajax page.within('.dropdown-menu-issues-board-new') do @@ -315,7 +315,7 @@ describe 'Issue Boards', feature: true, js: true do end it 'creates new list for label' do - click_button 'Create new list' + click_button 'Add list' wait_for_ajax page.within('.dropdown-menu-issues-board-new') do @@ -328,7 +328,7 @@ describe 'Issue Boards', feature: true, js: true do end it 'creates new list for Backlog label' do - click_button 'Create new list' + click_button 'Add list' wait_for_ajax page.within('.dropdown-menu-issues-board-new') do @@ -341,7 +341,7 @@ describe 'Issue Boards', feature: true, js: true do end it 'creates new list for Done label' do - click_button 'Create new list' + click_button 'Add list' wait_for_ajax page.within('.dropdown-menu-issues-board-new') do @@ -354,7 +354,7 @@ describe 'Issue Boards', feature: true, js: true do end it 'keeps dropdown open after adding new list' do - click_button 'Create new list' + click_button 'Add list' wait_for_ajax page.within('.dropdown-menu-issues-board-new') do @@ -369,7 +369,7 @@ describe 'Issue Boards', feature: true, js: true do it 'moves issues from backlog into new list' do wait_for_board_cards(1, 6) - click_button 'Create new list' + click_button 'Add list' wait_for_ajax page.within('.dropdown-menu-issues-board-new') do @@ -382,7 +382,7 @@ describe 'Issue Boards', feature: true, js: true do end it 'creates new list from a new label' do - click_button 'Create new list' + click_button 'Add list' wait_for_ajax -- cgit v1.2.1 From 7a9b545fb9c443cef1a3bb6394223e6bbe0cef9a Mon Sep 17 00:00:00 2001 From: Patricio Cano <suprnova32@gmail.com> Date: Wed, 23 Nov 2016 13:32:38 -0600 Subject: Added test that checks the correct select box is there for the LFS enabled setting. --- spec/views/projects/edit.html.haml_spec.rb | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 spec/views/projects/edit.html.haml_spec.rb diff --git a/spec/views/projects/edit.html.haml_spec.rb b/spec/views/projects/edit.html.haml_spec.rb new file mode 100644 index 00000000000..d2575702ecc --- /dev/null +++ b/spec/views/projects/edit.html.haml_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe 'projects/edit' do + include Devise::Test::ControllerHelpers + + let(:project) { create(:empty_project) } + let(:user) { create(:admin) } + + before do + assign(:project, project) + + allow(controller).to receive(:current_user).and_return(user) + allow(view).to receive_messages(current_user: user, can?: true) + allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) + end + + context 'LFS enabled setting' do + it 'displays the correct elements' do + render + expect(rendered).to have_select('project_lfs_enabled') + expect(rendered).to have_content('Git Large File Storage') + end + end +end -- cgit v1.2.1 From 0fcb9cf854895d3515fd4dc295e8d6a44f9c2aa3 Mon Sep 17 00:00:00 2001 From: PotHix <pothix@pothix.com> Date: Wed, 23 Nov 2016 17:33:12 -0200 Subject: Add missing documentation. Build was being triggered but there was no documentation about it, so I'm adding what I was receiving from webhooks when implementing the Rocketchat integration (https://github.com/PotHix/rocketchat-gitlab-hook). --- doc/web_hooks/web_hooks.md | 58 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index 33c1a79d59c..cd37189fdd2 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -922,6 +922,64 @@ X-Gitlab-Event: Pipeline Hook } ``` + +## Build events + +Triggered on status change of a Build. + +**Request Header**: + +``` +X-Gitlab-Event: Build Hook +``` + +**Request Body**: + +``` +{ + "object_kind": "build", + "ref": "gitlab-script-trigger", + "tag": false, + "before_sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", + "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", + "build_id": 1977, + "build_name": "test", + "build_stage": "test", + "build_status": "created", + "build_started_at": null, + "build_finished_at": null, + "build_duration": null, + "build_allow_failure": false, + "project_id": 380, + "project_name": "gitlab-org/gitlab-test", + "user": { + "id": 3, + "name": "User", + "email": "user@gitlab.com" + }, + "commit": { + "id": 2366, + "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", + "message": "test\n", + "author_name": "User", + "author_email": "user@gitlab.com", + "status": "created", + "duration": null, + "started_at": null, + "finished_at": null + }, + "repository": { + "name": "gitlab_test", + "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git", + "description": "Atque in sunt eos similique dolores voluptatem.", + "homepage": "http://192.168.64.1:3005/gitlab-org/gitlab-test", + "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git", + "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git", + "visibility_level": 20 + } +} +``` + #### Example webhook receiver If you want to see GitLab's webhooks in action for testing purposes you can use -- cgit v1.2.1 From ad195be94d89e62eac256baea198797cb7f59fdb Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Wed, 23 Nov 2016 22:17:47 +0100 Subject: Remove header ids from University docs [ci skip] --- doc/university/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/university/README.md b/doc/university/README.md index 4569bc72797..8917636c59b 100644 --- a/doc/university/README.md +++ b/doc/university/README.md @@ -19,7 +19,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project --- -### 1. <a name="beginner"></a> GitLab Beginner +### 1. GitLab Beginner #### 1.1. Version Control and Git @@ -85,7 +85,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project --- -### 2. <a name="intermediate"></a> GitLab Intermediate +### 2. GitLab Intermediate #### 2.1 GitLab Pages @@ -141,7 +141,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project --- -### 3. <a name="advanced"></a> GitLab Advanced +### 3. GitLab Advanced #### 3.1. Dev Ops @@ -186,7 +186,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project 1. [GitLab Cycle Analytics Overview](https://about.gitlab.com/2016/09/21/cycle-analytics-feature-highlight/) 1. [GitLab Cycle Analytics - Product Page](https://about.gitlab.com/solutions/cycle-analytics/) -#### 3.9. <a name="integrations"></a> Integrations +#### 3.9. Integrations 1. [How to Integrate JIRA and Jenkins with GitLab - Video](https://gitlabmeetings.webex.com/gitlabmeetings/ldr.php?RCID=44b548147a67ab4d8a62274047146415) 1. [How to Integrate Jira with GitLab](https://docs.gitlab.com/ee/integration/jira.html) @@ -198,7 +198,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project --- -## 4. <a name="external"></a> External Articles +## 4. External Articles 1. [2011 WSJ article by Marc Andreessen - Software is Eating the World](http://www.wsj.com/articles/SB10001424053111903480904576512250915629460) 1. [2014 Blog post by Chris Dixon - Software eats software development](http://cdixon.org/2014/04/13/software-eats-software-development/) @@ -206,7 +206,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project --- -## 5. <a name="team"></a> Resources for GitLab Team Members +## 5. Resources for GitLab Team Members *Some content can only be accessed by GitLab team members* -- cgit v1.2.1 From 6a23fb305f830735b601d0362d4cf881ddc08629 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 23 Nov 2016 16:48:51 -0600 Subject: allow "." in group name validation regex --- app/views/shared/_group_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index ba25e09d638..fc1753ca082 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -13,7 +13,7 @@ .input-group-addon = root_url = f.text_field :path, placeholder: 'open-source', class: 'form-control', - autofocus: local_assigns[:autofocus] || false, pattern: "[a-zA-Z0-9-_]+", + autofocus: local_assigns[:autofocus] || false, pattern: "[a-zA-Z0-9-_\\.]+", required: true, title: 'Please choose a group name with no special characters.' - if @group.persisted? -- cgit v1.2.1 From 5b9c4f48724c8a8eb9556719239be996366fd005 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 23 Nov 2016 17:31:35 -0600 Subject: properly escape username validation error message flash --- app/views/profiles/update_username.js.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/profiles/update_username.js.haml b/app/views/profiles/update_username.js.haml index de1337a2a24..5307e0b48cb 100644 --- a/app/views/profiles/update_username.js.haml +++ b/app/views/profiles/update_username.js.haml @@ -2,5 +2,6 @@ :plain new Flash("Username successfully changed", "notice") - else + - error = @user.errors.full_messages.first :plain - new Flash("Username change failed - #{@user.errors.full_messages.first}", "alert") + new Flash("Username change failed - #{escape_javascript error.html_safe}", "alert") -- cgit v1.2.1 From 5e26745e312a5c792f5864d46fb0378d90790504 Mon Sep 17 00:00:00 2001 From: Luis Alonso Chavez Armendariz <lchavez@nearsoft.com> Date: Wed, 23 Nov 2016 16:34:58 -0700 Subject: Fix title case to sentence case --- app/views/shared/issuable/_sidebar.html.haml | 6 +++--- changelogs/unreleased/issue_24748.yml | 4 ++++ doc/workflow/img/todos_add_todo_sidebar.png | Bin 33350 -> 42360 bytes doc/workflow/img/todos_mark_done_sidebar.png | Bin 33703 -> 42317 bytes doc/workflow/todos.md | 4 ++-- spec/features/issues/todo_spec.rb | 8 ++++---- spec/javascripts/fixtures/right_sidebar.html.haml | 4 ++-- 7 files changed, 15 insertions(+), 11 deletions(-) create mode 100644 changelogs/unreleased/issue_24748.yml diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index f166fac105d..02427650219 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -9,12 +9,12 @@ %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", aria: { label: "Toggle sidebar" } } = sidebar_gutter_toggle_icon - if current_user - %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", aria: { label: (todo.nil? ? "Add Todo" : "Mark Done") }, data: { todo_text: "Add Todo", mark_text: "Mark Done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } } + %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", aria: { label: (todo.nil? ? "Add todo" : "Mark done") }, data: { todo_text: "Add todo", mark_text: "Mark done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } } %span.js-issuable-todo-text - if todo - Mark Done + Mark done - else - Add Todo + Add todo = icon('spin spinner', class: 'hidden js-issuable-todo-loading') = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f| diff --git a/changelogs/unreleased/issue_24748.yml b/changelogs/unreleased/issue_24748.yml new file mode 100644 index 00000000000..4c1df542c53 --- /dev/null +++ b/changelogs/unreleased/issue_24748.yml @@ -0,0 +1,4 @@ +--- +title: Fix title case to sentence case +merge_request: +author: Luis Alonso Chavez Armendariz diff --git a/doc/workflow/img/todos_add_todo_sidebar.png b/doc/workflow/img/todos_add_todo_sidebar.png index 59175ae44c5..3fa37067d1e 100644 Binary files a/doc/workflow/img/todos_add_todo_sidebar.png and b/doc/workflow/img/todos_add_todo_sidebar.png differ diff --git a/doc/workflow/img/todos_mark_done_sidebar.png b/doc/workflow/img/todos_mark_done_sidebar.png index aa35bb672ea..a8e756a71db 100644 Binary files a/doc/workflow/img/todos_mark_done_sidebar.png and b/doc/workflow/img/todos_mark_done_sidebar.png differ diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md index 54e7ae19ea5..1a8fc39bb33 100644 --- a/doc/workflow/todos.md +++ b/doc/workflow/todos.md @@ -35,7 +35,7 @@ A Todo appears in your Todos dashboard when: ### Manually creating a Todo You can also add an issue or merge request to your Todos dashboard by clicking -the "Add Todo" button in the issue or merge request sidebar. +the "Add todo" button in the issue or merge request sidebar. ![Adding a Todo from the issuable sidebar](img/todos_add_todo_sidebar.png) @@ -69,7 +69,7 @@ corresponding **Done** button, and it will disappear from your Todo list. ![A Todo in the Todos dashboard](img/todo_list_item.png) A Todo can also be marked as done from the issue or merge request sidebar using -the "Mark Done" button. +the "Mark done" button. ![Mark Done from the issuable sidebar](img/todos_mark_done_sidebar.png) diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb index de8fdda388d..41ff31d2b99 100644 --- a/spec/features/issues/todo_spec.rb +++ b/spec/features/issues/todo_spec.rb @@ -13,8 +13,8 @@ feature 'Manually create a todo item from issue', feature: true, js: true do it 'creates todo when clicking button' do page.within '.issuable-sidebar' do - click_button 'Add Todo' - expect(page).to have_content 'Mark Done' + click_button 'Add todo' + expect(page).to have_content 'Mark done' end page.within '.header-content .todos-pending-count' do @@ -30,8 +30,8 @@ feature 'Manually create a todo item from issue', feature: true, js: true do it 'marks a todo as done' do page.within '.issuable-sidebar' do - click_button 'Add Todo' - click_button 'Mark Done' + click_button 'Add todo' + click_button 'Mark done' end expect(page).to have_selector('.todos-pending-count', visible: false) diff --git a/spec/javascripts/fixtures/right_sidebar.html.haml b/spec/javascripts/fixtures/right_sidebar.html.haml index d48b77cf0ce..d259b58f235 100644 --- a/spec/javascripts/fixtures/right_sidebar.html.haml +++ b/spec/javascripts/fixtures/right_sidebar.html.haml @@ -5,9 +5,9 @@ %div.block.issuable-sidebar-header %a.gutter-toggle.pull-right.js-sidebar-toggle %i.fa.fa-angle-double-left - %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", data: { todo_text: "Add Todo", mark_text: "Mark Done", issuable_id: "1", issuable_type: "issue", url: "/todos" }} + %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", data: { todo_text: "Add todo", mark_text: "Mark done", issuable_id: "1", issuable_type: "issue", url: "/todos" }} %span.js-issuable-todo-text - Add Todo + Add todo %i.fa.fa-spin.fa-spinner.js-issuable-todo-loading.hidden %form.issuable-context-form -- cgit v1.2.1 From b437d305ca92f0f908d2cba4d681a8c8df9a348a Mon Sep 17 00:00:00 2001 From: Ruben Davila <rdavila84@gmail.com> Date: Wed, 23 Nov 2016 19:27:22 -0500 Subject: Add default_branch attr to Project API payload in docs. --- doc/api/projects.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/api/projects.md b/doc/api/projects.md index de5d3b07c21..b02a901d884 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -592,6 +592,7 @@ Parameters: | `name` | string | yes | The name of the new project | | `path` | string | no | Custom repository name for new project. By default generated based on name | | `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) | +| `default_branch` | string | no | `master` by default | | `description` | string | no | Short project description | | `issues_enabled` | boolean | no | Enable issues for this project | | `merge_requests_enabled` | boolean | no | Enable merge requests for this project | @@ -657,6 +658,7 @@ Parameters: | `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project | | `name` | string | yes | The name of the project | | `path` | string | no | Custom repository name for the project. By default generated based on name | +| `default_branch` | string | no | `master` by default | | `description` | string | no | Short project description | | `issues_enabled` | boolean | no | Enable issues for this project | | `merge_requests_enabled` | boolean | no | Enable merge requests for this project | -- cgit v1.2.1 From 4123a0ff75369e28a4722b087ef4243f5e521d52 Mon Sep 17 00:00:00 2001 From: Chris Wilson <chris@chrisjwilson.com> Date: Thu, 24 Nov 2016 13:36:15 +0800 Subject: Log mv_namespace parameters --- app/models/namespace.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/namespace.rb b/app/models/namespace.rb index b67049f0f55..dfd1633707e 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -103,6 +103,8 @@ class Namespace < ActiveRecord::Base gitlab_shell.add_namespace(repository_storage_path, path_was) unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path) + Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}" + # if we cannot move namespace directory we should rollback # db changes in order to prevent out of sync between db and fs raise Exception.new('namespace directory cannot be moved') -- cgit v1.2.1 From ba5e98bb701672d0cf1d98a80272c16a754ec83c Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Thu, 24 Nov 2016 14:32:32 +0800 Subject: Backport Note#commands_changes from EE --- app/controllers/projects/notes_controller.rb | 1 + app/models/note.rb | 3 +++ app/services/notes/create_service.rb | 2 ++ 3 files changed, 6 insertions(+) diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index f029fde2a2f..15ca080c696 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -197,6 +197,7 @@ class Projects::NotesController < Projects::ApplicationController ) end + attrs[:commands_changes] = note.commands_changes unless attrs[:award] attrs end diff --git a/app/models/note.rb b/app/models/note.rb index 9ff5e308ed2..ed4224e3046 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -19,6 +19,9 @@ class Note < ActiveRecord::Base # Banzai::ObjectRenderer attr_accessor :user_visible_reference_count + # Attribute used to store the attributes that have ben changed by slash commands. + attr_accessor :commands_changes + default_value_for :system, false attr_mentionable :note, pipeline: :note diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index 7935fabe2da..d75592e31f3 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -43,6 +43,8 @@ module Notes if only_commands note.errors.add(:commands_only, 'Your commands have been executed!') end + + note.commands_changes = command_params.keys end note -- cgit v1.2.1 From a43f71ec144c1a8ab9f9829414699cec062a8b92 Mon Sep 17 00:00:00 2001 From: Stan Hu <stanhu@gmail.com> Date: Thu, 24 Nov 2016 00:21:48 -0800 Subject: Hide project variables values by default Add a button to reveal/hide the values to help prevent accidental disclosure of sensitive information from wandering on a page. Closes #21358 --- app/assets/javascripts/dispatcher.js.es6 | 3 ++ app/assets/javascripts/project_variables.js.es6 | 44 +++++++++++++++++++++++++ app/assets/stylesheets/pages/projects.scss | 8 +++++ app/views/projects/variables/_table.html.haml | 4 +-- app/views/projects/variables/index.html.haml | 1 + 5 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 app/assets/javascripts/project_variables.js.es6 diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index c2d4670b7e9..16df4b0b005 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -208,6 +208,9 @@ new gl.ProtectedBranchCreate(); new gl.ProtectedBranchEditList(); break; + case 'projects:variables:index': + new gl.ProjectVariables(); + break; } switch (path.first()) { case 'admin': diff --git a/app/assets/javascripts/project_variables.js.es6 b/app/assets/javascripts/project_variables.js.es6 new file mode 100644 index 00000000000..51ee55946e4 --- /dev/null +++ b/app/assets/javascripts/project_variables.js.es6 @@ -0,0 +1,44 @@ +/* eslint-disable */ +((global) => { + const HIDDEN_VALUE_TEXT = '******'; + + class ProjectVariables { + constructor() { + this.$reveal = $('.js-btn-toggle-reveal-values'); + + this.$reveal.on('click', this.toggleRevealState.bind(this)); + } + + toggleRevealState(event) { + event.preventDefault(); + + const $btn = $(event.currentTarget); + const oldStatus = $btn.attr('data-status'); + + if (oldStatus == 'hidden') { + [newStatus, newAction] = ['revealed', 'Hide Values']; + } else { + [newStatus, newAction] = ['hidden', 'Reveal Values']; + } + + $btn.attr('data-status', newStatus); + + $variables = $('.variable-value'); + + for (let variable of $variables) { + let $variable = $(variable); + let newText = HIDDEN_VALUE_TEXT; + + if (newStatus == 'revealed') { + newText = $variable.attr('data-value'); + } + + $variable.text(newText); + } + + $btn.text(newAction); + } + } + + global.ProjectVariables = ProjectVariables; +})(window.gl || (window.gl = {})); diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 19a7a97ea0d..0562ee7b178 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -876,3 +876,11 @@ pre.light-well { pointer-events: none; } } + +.variables-table { + table-layout: fixed; + + .variable-key { + width: 30%; + } +} diff --git a/app/views/projects/variables/_table.html.haml b/app/views/projects/variables/_table.html.haml index 07cee86ba4c..c7cebf45160 100644 --- a/app/views/projects/variables/_table.html.haml +++ b/app/views/projects/variables/_table.html.haml @@ -12,8 +12,8 @@ - @project.variables.order_key_asc.each do |variable| - if variable.id? %tr - %td= variable.key - %td= variable.value + %td.variable-key= variable.key + %td.variable-value{ "data-value" => variable.value }****** %td = link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-edit" do %span.sr-only diff --git a/app/views/projects/variables/index.html.haml b/app/views/projects/variables/index.html.haml index 09bb54600af..39303700131 100644 --- a/app/views/projects/variables/index.html.haml +++ b/app/views/projects/variables/index.html.haml @@ -15,3 +15,4 @@ No variables found, add one with the form above. - else = render "table" + %button.btn.btn-info.js-btn-toggle-reveal-values{"data-status" => 'hidden'} Reveal Values -- cgit v1.2.1 From 3a99e36e4448409bf21a0258dde1a82f6494922e Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Thu, 24 Nov 2016 16:27:57 +0800 Subject: Avoid using random in the tests, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7333#note_18860042 --- spec/models/commit_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index a16b2e73dd7..62b77ef86c5 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -208,11 +208,11 @@ eos describe '#status' do context 'without arguments' do before do - 5.times do + %w[success failed created pending].each do |status| create(:ci_empty_pipeline, project: project, sha: commit.sha, - status: Ci::Pipeline.all_state_names.sample) + status: status) end end -- cgit v1.2.1 From 6192ea53fad0ea04e356e5a79a5a0e5359ba46ce Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Thu, 24 Nov 2016 16:50:37 +0800 Subject: Rename latest_for to latest, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7333/diffs#note_18819292 --- app/models/ci/pipeline.rb | 12 ++++-------- app/models/commit.rb | 2 +- app/services/ci/image_for_build_service.rb | 2 +- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 61d9316a5d3..d1ce43570ac 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -89,23 +89,19 @@ module Ci end end - scope :latest, -> do + # ref can't be HEAD or SHA, can only be branch/tag name + scope :latest, ->(ref = nil) do max_id = unscope(:select).select("max(#{quoted_table_name}.id)") - where(id: max_id.group(:ref, :sha)) - end - - # ref can't be HEAD or SHA, can only be branch/tag name - scope :latest_for, ->(ref) do if ref where(ref: ref) else self - end.latest + end.where(id: max_id.group(:ref, :sha)) end def self.latest_successful_for(ref) - success.latest_for(ref).first + success.latest(ref).first end def self.truncate_sha(sha) diff --git a/app/models/commit.rb b/app/models/commit.rb index b588b93b158..946bfc4712c 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -234,7 +234,7 @@ class Commit return @statuses[ref] if @statuses.key?(ref) - @statuses[ref] = pipelines.latest_for(ref).status + @statuses[ref] = pipelines.latest(ref).status end def revert_branch_name diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb index d5a07ef630b..1eeb0e2363a 100644 --- a/app/services/ci/image_for_build_service.rb +++ b/app/services/ci/image_for_build_service.rb @@ -5,7 +5,7 @@ module Ci sha = opts[:sha] || ref_sha(project, ref) pipelines = project.pipelines.where(sha: sha) - image_name = image_for_status(pipelines.latest_for(ref).status) + image_name = image_for_status(pipelines.latest(ref).status) image_path = Rails.root.join('public/ci', image_name) OpenStruct.new(path: image_path, name: image_name) -- cgit v1.2.1 From 4d2e7894efa36cc1b5de9432e25fcf22b6cf1d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Wed, 23 Nov 2016 19:18:19 +0100 Subject: Make API::Helpers find a project with only one query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- .../22373-reduce-queries-in-api-helpers-find_project.yml | 4 ++++ lib/api/helpers.rb | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/22373-reduce-queries-in-api-helpers-find_project.yml diff --git a/changelogs/unreleased/22373-reduce-queries-in-api-helpers-find_project.yml b/changelogs/unreleased/22373-reduce-queries-in-api-helpers-find_project.yml new file mode 100644 index 00000000000..7f1d40e7c21 --- /dev/null +++ b/changelogs/unreleased/22373-reduce-queries-in-api-helpers-find_project.yml @@ -0,0 +1,4 @@ +--- +title: 'Make API::Helpers find a project with only one query' +merge_request: 7714 +author: diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 2c593dbb4ea..60067758e95 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -76,7 +76,12 @@ module API end def find_project(id) - project = Project.find_with_namespace(id) || Project.find_by(id: id) + project = + if id =~ /^\d+$/ + Project.find_by(id: id) + else + Project.find_with_namespace(id) + end if can?(current_user, :read_project, project) project -- cgit v1.2.1 From 39378d0e976d9552e96b300cb7f43874424d13b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Wed, 23 Nov 2016 12:58:16 +0100 Subject: Document that we always use `do...end` for `before` in RSpec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [ci skip] Signed-off-by: Rémy Coutable <remy@rymai.me> --- doc/development/testing.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/development/testing.md b/doc/development/testing.md index 6106e47daa0..dbea6b9c9aa 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -63,6 +63,8 @@ the command line via `bundle exec teaspoon`, or via a web browser at - Use `.method` to describe class methods and `#method` to describe instance methods. - Use `context` to test branching logic. +- Use multi-line `do...end` blocks for `before` and `after`, even when it would + fit on a single line. - Don't `describe` symbols (see [Gotchas](gotchas.md#dont-describe-symbols)). - Don't assert against the absolute value of a sequence-generated attribute (see [Gotchas](gotchas.md#dont-assert-against-the-absolute-value-of-a-sequence-generated-attribute)). - Don't supply the `:each` argument to hooks since it's the default. -- cgit v1.2.1 From 391db2421eda4f69e73f18d45fa377f209670528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Thu, 24 Nov 2016 10:17:24 +0100 Subject: Fix documentation to create the `pg_trm` extension before creating the DB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [ci skip] Signed-off-by: Rémy Coutable <remy@rymai.me> --- doc/install/installation.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index b5e2640b380..dabd69d7466 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -175,17 +175,17 @@ We recommend using a PostgreSQL database. For MySQL check the ```bash sudo -u postgres psql -d template1 -c "CREATE USER git CREATEDB;" ``` - -1. Create the GitLab production database and grant all privileges on database: + +1. Create the `pg_trgm` extension (required for GitLab 8.6+): ```bash - sudo -u postgres psql -d template1 -c "CREATE DATABASE gitlabhq_production OWNER git;" + sudo -u postgres psql -d template1 -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;" ``` -1. Create the `pg_trgm` extension (required for GitLab 8.6+): +1. Create the GitLab production database and grant all privileges on database: ```bash - sudo -u postgres psql -d template1 -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;" + sudo -u postgres psql -d template1 -c "CREATE DATABASE gitlabhq_production OWNER git;" ``` 1. Try connecting to the new database with the new user: -- cgit v1.2.1 From 3341a9d80c884113b6208397717657dc99266683 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Thu, 24 Nov 2016 11:01:56 +0100 Subject: Add missing JIRA file that redirects to the new location [ci skip] --- doc/integration/jira.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 doc/integration/jira.md diff --git a/doc/integration/jira.md b/doc/integration/jira.md new file mode 100644 index 00000000000..e2f136bcc35 --- /dev/null +++ b/doc/integration/jira.md @@ -0,0 +1,3 @@ +# GitLab JIRA integration + +This document was moved to [project_services/jira](../project_services/jira.md). -- cgit v1.2.1 From 1fa55069745163e70f01349f71798e2a214ae28e Mon Sep 17 00:00:00 2001 From: Stan Hu <stanhu@gmail.com> Date: Thu, 24 Nov 2016 02:24:52 -0800 Subject: Add spec for hiding variables and remove the need for ES6 Symbol --- app/assets/javascripts/project_variables.js.es6 | 6 +++--- spec/features/variables_spec.rb | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/project_variables.js.es6 b/app/assets/javascripts/project_variables.js.es6 index 51ee55946e4..6c905f58c85 100644 --- a/app/assets/javascripts/project_variables.js.es6 +++ b/app/assets/javascripts/project_variables.js.es6 @@ -23,9 +23,9 @@ $btn.attr('data-status', newStatus); - $variables = $('.variable-value'); + let $variables = $('.variable-value'); - for (let variable of $variables) { + $variables.each(function (_, variable) { let $variable = $(variable); let newText = HIDDEN_VALUE_TEXT; @@ -34,7 +34,7 @@ } $variable.text(newText); - } + }); $btn.text(newAction); } diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb index d7880d5778f..ff30ffd7820 100644 --- a/spec/features/variables_spec.rb +++ b/spec/features/variables_spec.rb @@ -29,6 +29,31 @@ describe 'Project variables', js: true do end end + it 'reveals and hides new variable' do + fill_in('variable_key', with: 'key') + fill_in('variable_value', with: 'key value') + click_button('Add new variable') + + page.within('.variables-table') do + expect(page).to have_content('key') + expect(page).to have_content('******') + end + + click_button('Reveal Values') + + page.within('.variables-table') do + expect(page).to have_content('key') + expect(page).to have_content('key value') + end + + click_button('Hide Values') + + page.within('.variables-table') do + expect(page).to have_content('key') + expect(page).to have_content('******') + end + end + it 'deletes variable' do page.within('.variables-table') do find('.btn-variable-delete').click -- cgit v1.2.1 From 6df22f72c6c312199c547e017ce1f947cf88e34c Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Wed, 23 Nov 2016 14:55:23 +0800 Subject: Rephrase some system notes to be compatible with new system note style --- app/services/notification_service.rb | 1 - app/services/system_note_service.rb | 86 ++++++++++----------- changelogs/unreleased/rephrase-system-notes.yml | 4 + doc/api/notes.md | 2 +- features/steps/project/merge_requests.rb | 2 +- features/steps/shared/issuable.rb | 2 +- .../projects/milestones_controller_spec.rb | 2 +- spec/features/issues/move_spec.rb | 6 +- spec/features/issues/new_branch_button_spec.rb | 4 +- .../merge_when_build_succeeds_spec.rb | 2 +- spec/features/notes_on_merge_requests_spec.rb | 2 +- spec/models/note_spec.rb | 2 +- spec/requests/api/notes_spec.rb | 2 +- spec/services/issues/close_service_spec.rb | 2 +- spec/services/issues/move_service_spec.rb | 6 +- spec/services/issues/update_service_spec.rb | 28 +++---- spec/services/merge_requests/close_service_spec.rb | 2 +- spec/services/merge_requests/merge_service_spec.rb | 2 +- .../merge_when_build_succeeds_service_spec.rb | 4 +- .../merge_requests/refresh_service_spec.rb | 16 ++-- .../services/merge_requests/reopen_service_spec.rb | 2 +- .../services/merge_requests/update_service_spec.rb | 24 +++--- spec/services/system_note_service_spec.rb | 90 ++++++++++++++++------ 23 files changed, 169 insertions(+), 124 deletions(-) create mode 100644 changelogs/unreleased/rephrase-system-notes.yml diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index ecdcbf08ee1..9a7af5730d2 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -171,7 +171,6 @@ class NotificationService return true unless note.noteable_type.present? # ignore gitlab service messages - return true if note.note.start_with?('Status changed to closed') return true if note.cross_reference? && note.system? target = note.noteable diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 1ce66d50368..a33845848b4 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -21,7 +21,7 @@ module SystemNoteService total_count = new_commits.length + existing_commits.length commits_text = "#{total_count} commit".pluralize(total_count) - body = "Added #{commits_text}:\n\n" + body = "added #{commits_text}\n\n" body << existing_commit_summary(noteable, existing_commits, oldrev) body << new_commit_summary(new_commits).join("\n") body << "\n\n[Compare with previous version](#{diff_comparison_url(noteable, project, oldrev)})" @@ -38,13 +38,13 @@ module SystemNoteService # # Example Note text: # - # "Assignee removed" + # "removed assignee" # - # "Reassigned to @rspeicher" + # "assigned to @rspeicher" # # Returns the created Note object def change_assignee(noteable, project, author, assignee) - body = assignee.nil? ? 'Assignee removed' : "Reassigned to #{assignee.to_reference}" + body = assignee.nil? ? 'removed assignee' : "assigned to #{assignee.to_reference}" create_note(noteable: noteable, project: project, author: author, note: body) end @@ -59,11 +59,11 @@ module SystemNoteService # # Example Note text: # - # "Added ~1 and removed ~2 ~3 labels" + # "added ~1 and removed ~2 ~3 labels" # - # "Added ~4 label" + # "added ~4 label" # - # "Removed ~5 label" + # "removed ~5 label" # # Returns the created Note object def change_label(noteable, project, author, added_labels, removed_labels) @@ -85,7 +85,6 @@ module SystemNoteService end body << ' ' << 'label'.pluralize(labels_count) - body = body.capitalize create_note(noteable: noteable, project: project, author: author, note: body) end @@ -99,14 +98,13 @@ module SystemNoteService # # Example Note text: # - # "Milestone removed" + # "removed milestone" # - # "Miletone changed to 7.11" + # "changed milestone to 7.11" # # Returns the created Note object def change_milestone(noteable, project, author, milestone) - body = 'Milestone ' - body += milestone.nil? ? 'removed' : "changed to #{milestone.to_reference(project)}" + body = milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project)}" create_note(noteable: noteable, project: project, author: author, note: body) end @@ -121,46 +119,46 @@ module SystemNoteService # # Example Note text: # - # "Status changed to merged" + # "merged" # - # "Status changed to closed by bc17db76" + # "closed via bc17db76" # # Returns the created Note object def change_status(noteable, project, author, status, source) - body = "Status changed to #{status}" - body << " by #{source.gfm_reference(project)}" if source + body = status.dup + body << " via #{source.gfm_reference(project)}" if source create_note(noteable: noteable, project: project, author: author, note: body) end # Called when 'merge when build succeeds' is executed def merge_when_build_succeeds(noteable, project, author, last_commit) - body = "Enabled an automatic merge when the build for #{last_commit.to_reference(project)} succeeds" + body = "enabled an automatic merge when the build for #{last_commit.to_reference(project)} succeeds" create_note(noteable: noteable, project: project, author: author, note: body) end # Called when 'merge when build succeeds' is canceled def cancel_merge_when_build_succeeds(noteable, project, author) - body = 'Canceled the automatic merge' + body = 'canceled the automatic merge' create_note(noteable: noteable, project: project, author: author, note: body) end def remove_merge_request_wip(noteable, project, author) - body = 'Unmarked this merge request as a Work In Progress' + body = 'unmarked as a Work In Progress' create_note(noteable: noteable, project: project, author: author, note: body) end def add_merge_request_wip(noteable, project, author) - body = 'Marked this merge request as a **Work In Progress**' + body = 'marked as a **Work In Progress**' create_note(noteable: noteable, project: project, author: author, note: body) end def self.resolve_all_discussions(merge_request, project, author) - body = "Resolved all discussions" + body = "resolved all discussions" create_note(noteable: merge_request, project: project, author: author, note: body) end @@ -174,7 +172,7 @@ module SystemNoteService # # Example Note text: # - # "Title changed from **Old** to **New**" + # "changed title from **Old** to **New**" # # Returns the created Note object def change_title(noteable, project, author, old_title) @@ -185,7 +183,7 @@ module SystemNoteService marked_old_title = Gitlab::Diff::InlineDiffMarker.new(old_title).mark(old_diffs, mode: :deletion, markdown: true) marked_new_title = Gitlab::Diff::InlineDiffMarker.new(new_title).mark(new_diffs, mode: :addition, markdown: true) - body = "Changed title: **#{marked_old_title}** → **#{marked_new_title}**" + body = "changed title from **#{marked_old_title}** to **#{marked_new_title}**" create_note(noteable: noteable, project: project, author: author, note: body) end @@ -197,11 +195,11 @@ module SystemNoteService # # Example Note text: # - # "Made the issue confidential" + # "made the issue confidential" # # Returns the created Note object def change_issue_confidentiality(issue, project, author) - body = issue.confidential ? 'Made the issue confidential' : 'Made the issue visible' + body = issue.confidential ? 'made the issue confidential' : 'made the issue visible to everyone' create_note(noteable: issue, project: project, author: author, note: body) end @@ -216,11 +214,11 @@ module SystemNoteService # # Example Note text: # - # "Target branch changed from `Old` to `New`" + # "changed target branch from `Old` to `New`" # # Returns the created Note object def change_branch(noteable, project, author, branch_type, old_branch, new_branch) - body = "#{branch_type} branch changed from `#{old_branch}` to `#{new_branch}`".capitalize + body = "changed #{branch_type} branch from `#{old_branch}` to `#{new_branch}`" create_note(noteable: noteable, project: project, author: author, note: body) end @@ -235,7 +233,7 @@ module SystemNoteService # # Example Note text: # - # "Restored target branch `feature`" + # "restored target branch `feature`" # # Returns the created Note object def change_branch_presence(noteable, project, author, branch_type, branch, presence) @@ -246,18 +244,18 @@ module SystemNoteService 'deleted' end - body = "#{verb} #{branch_type} branch `#{branch}`".capitalize + body = "#{verb} #{branch_type} branch `#{branch}`" create_note(noteable: noteable, project: project, author: author, note: body) end # Called when a branch is created from the 'new branch' button on a issue # Example note text: # - # "Started branch `201-issue-branch-button`" + # "created branch `201-issue-branch-button`" def new_issue_branch(issue, project, author, branch) link = url_helpers.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch) - body = "Started branch [`#{branch}`](#{link})" + body = "created branch [`#{branch}`](#{link})" create_note(noteable: issue, project: project, author: author, note: body) end @@ -269,11 +267,11 @@ module SystemNoteService # # Example Note text: # - # "Mentioned in #1" + # "mentioned in #1" # - # "Mentioned in !2" + # "mentioned in !2" # - # "Mentioned in 54f7727c" + # "mentioned in 54f7727c" # # See cross_reference_note_content. # @@ -303,12 +301,12 @@ module SystemNoteService end def cross_reference?(note_text) - note_text.start_with?(cross_reference_note_prefix) + note_text =~ /\A#{cross_reference_note_prefix}/i end # Check if a cross-reference is disallowed # - # This method prevents adding a "Mentioned in !1" note on every single commit + # This method prevents adding a "mentioned in !1" note on every single commit # in a merge request. Additionally, it prevents the creation of references to # external issues (which would fail). # @@ -370,12 +368,12 @@ module SystemNoteService # # Example Note text: # - # "Soandso marked the task Whatever as completed." + # "marked the task Whatever as completed." # # Returns the created Note object def change_task_status(noteable, project, author, new_task) status_label = new_task.complete? ? Taskable::COMPLETED : Taskable::INCOMPLETE - body = "Marked the task **#{new_task.source}** as #{status_label}" + body = "marked the task **#{new_task.source}** as #{status_label}" create_note(noteable: noteable, project: project, author: author, note: body) end @@ -388,7 +386,7 @@ module SystemNoteService # # Example Note text: # - # "Moved to some_namespace/project_new#11" + # "moved to some_namespace/project_new#11" # # Returns the created Note object def noteable_moved(noteable, project, noteable_ref, author, direction:) @@ -397,7 +395,7 @@ module SystemNoteService end cross_reference = noteable_ref.to_reference(project) - body = "Moved #{direction} #{cross_reference}" + body = "moved #{direction} #{cross_reference}" create_note(noteable: noteable, project: project, author: author, note: body) end @@ -405,10 +403,12 @@ module SystemNoteService def notes_for_mentioner(mentioner, noteable, notes) if mentioner.is_a?(Commit) - notes.where('note LIKE ?', "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}") + text = "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}" + notes.where('(note LIKE ? OR note LIKE ?)', text, text.capitalize) else gfm_reference = mentioner.gfm_reference(noteable.project) - notes.where(note: cross_reference_note_content(gfm_reference)) + text = cross_reference_note_content(gfm_reference) + notes.where(note: [text, text.capitalize]) end end @@ -417,7 +417,7 @@ module SystemNoteService end def cross_reference_note_prefix - 'Mentioned in ' + 'mentioned in ' end def cross_reference_note_content(gfm_reference) diff --git a/changelogs/unreleased/rephrase-system-notes.yml b/changelogs/unreleased/rephrase-system-notes.yml new file mode 100644 index 00000000000..e77c3a31cb4 --- /dev/null +++ b/changelogs/unreleased/rephrase-system-notes.yml @@ -0,0 +1,4 @@ +--- +title: Rephrase some system notes to be compatible with new system note style +merge_request: 7692 +author: diff --git a/doc/api/notes.md b/doc/api/notes.md index 58d40eecf3e..9971806be56 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -21,7 +21,7 @@ Parameters: [ { "id": 302, - "body": "Status changed to closed", + "body": "closed", "attachment": null, "author": { "id": 1, diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index f728d243cdc..d2fa8cd39af 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -515,7 +515,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see new target branch changes' do expect(page).to have_content 'Request to merge fix into feature' - expect(page).to have_content 'Target branch changed from merge-test to feature' + expect(page).to have_content 'changed target branch from merge-test to feature' wait_for_ajax end diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb index df9845ba569..aa666a954bc 100644 --- a/features/steps/shared/issuable.rb +++ b/features/steps/shared/issuable.rb @@ -179,7 +179,7 @@ module SharedIssuable project = Project.find_by(name: from_project_name) expect(page).to have_content(user_name) - expect(page).to have_content("Mentioned in #{issuable.class.to_s.titleize.downcase} #{issuable.to_reference(project)}") + expect(page).to have_content("mentioned in #{issuable.class.to_s.titleize.downcase} #{issuable.to_reference(project)}") end def expect_sidebar_content(content) diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb index 7c5f33c63b8..6d30d085056 100644 --- a/spec/controllers/projects/milestones_controller_spec.rb +++ b/spec/controllers/projects/milestones_controller_spec.rb @@ -31,7 +31,7 @@ describe Projects::MilestonesController do # Check system note left for milestone removal last_note = project.issues.find(issue.id).notes[-1].note - expect(last_note).to eq('Milestone removed') + expect(last_note).to eq('removed milestone') end end end diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb index 055210399a7..c9bec05a9da 100644 --- a/spec/features/issues/move_spec.rb +++ b/spec/features/issues/move_spec.rb @@ -27,7 +27,7 @@ feature 'issue move to another project' do let!(:mr) { create(:merge_request, source_project: old_project) } let(:new_project) { create(:project) } let(:new_project_search) { create(:project) } - let(:text) { 'Text with !1' } + let(:text) { "Text with #{mr.to_reference}" } let(:cross_reference) { old_project.to_reference } background do @@ -43,8 +43,8 @@ feature 'issue move to another project' do expect(current_url).to include project_path(new_project) - expect(page).to have_content("Text with #{cross_reference}!1") - expect(page).to have_content("Moved from #{cross_reference}#1") + expect(page).to have_content("Text with #{cross_reference}#{mr.to_reference}") + expect(page).to have_content("moved from #{cross_reference}#{issue.to_reference}") expect(page).to have_content(issue.title) end diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb index ab901e74617..a4d3053d10c 100644 --- a/spec/features/issues/new_branch_button_spec.rb +++ b/spec/features/issues/new_branch_button_spec.rb @@ -20,12 +20,12 @@ feature 'Start new branch from an issue', feature: true do context "when there is a referenced merge request" do let!(:note) do create(:note, :on_issue, :system, project: project, noteable: issue, - note: "Mentioned in !#{referenced_mr.iid}") + note: "mentioned in #{referenced_mr.to_reference}") end let(:referenced_mr) do create(:merge_request, :simple, source_project: project, target_project: project, - description: "Fixes ##{issue.iid}", author: user) + description: "Fixes #{issue.to_reference}", author: user) end before do diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb index 8eceaad2457..9ad225e3a1b 100644 --- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb @@ -44,7 +44,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do expect(page).to have_content "The source branch will not be removed." visit_merge_request(merge_request) # Needed to refresh the page - expect(page).to have_content /Enabled an automatic merge when the build for [0-9a-f]{8} succeeds/i + expect(page).to have_content /enabled an automatic merge when the build for \h{8} succeeds/i end end end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 5d7247e2a62..9fffbb43e87 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -141,7 +141,7 @@ describe 'Comments', feature: true do let(:project2) { create(:project, :private) } let(:issue) { create(:issue, project: project2) } let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'markdown') } - let!(:note) { create(:note_on_merge_request, :system, noteable: merge_request, project: project, note: "Mentioned in #{issue.to_reference(project)}") } + let!(:note) { create(:note_on_merge_request, :system, noteable: merge_request, project: project, note: "mentioned in #{issue.to_reference(project)}") } it 'shows the system note' do login_as :admin diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index e6b6e7c0634..17a15b12dcb 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -223,7 +223,7 @@ describe Note, models: true do let(:note) do create :note, noteable: ext_issue, project: ext_proj, - note: "Mentioned in issue #{private_issue.to_reference(ext_proj)}", + note: "mentioned in issue #{private_issue.to_reference(ext_proj)}", system: true end diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 0124b7271b3..b71a4c5a56e 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -25,7 +25,7 @@ describe API::API, api: true do let!(:cross_reference_note) do create :note, noteable: ext_issue, project: ext_proj, - note: "Mentioned in issue #{private_issue.to_reference(ext_proj)}", + note: "mentioned in issue #{private_issue.to_reference(ext_proj)}", system: true end diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index 4465f22a001..7a54373963e 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -62,7 +62,7 @@ describe Issues::CloseService, services: true do it 'creates system note about issue reassign' do note = issue.notes.last - expect(note.note).to include "Status changed to closed" + expect(note.note).to include "closed" end it 'marks todos as done' do diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index f0ded06b785..c7de0d0c534 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -81,11 +81,11 @@ describe Issues::MoveService, services: true do end it 'adds system note to old issue at the end' do - expect(old_issue.notes.last.note).to match /^Moved to/ + expect(old_issue.notes.last.note).to start_with 'moved to' end it 'adds system note to new issue at the end' do - expect(new_issue.notes.last.note).to match /^Moved from/ + expect(new_issue.notes.last.note).to start_with 'moved from' end it 'closes old issue' do @@ -151,7 +151,7 @@ describe Issues::MoveService, services: true do end it 'adds a system note about move after rewritten notes' do - expect(system_notes.last.note).to match /^Moved from/ + expect(system_notes.last.note).to match /^moved from/ end it 'preserves orignal author of comment' do diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 4777a90639e..4c878d748c0 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -91,24 +91,24 @@ describe Issues::UpdateService, services: true do end it 'creates system note about issue reassign' do - note = find_note('Reassigned to') + note = find_note('assigned to') expect(note).not_to be_nil - expect(note.note).to include "Reassigned to \@#{user2.username}" + expect(note.note).to include "assigned to #{user2.to_reference}" end it 'creates system note about issue label edit' do - note = find_note('Added ~') + note = find_note('added ~') expect(note).not_to be_nil - expect(note.note).to include "Added ~#{label.id} label" + expect(note.note).to include "added #{label.to_reference} label" end it 'creates system note about title change' do - note = find_note('Changed title:') + note = find_note('changed title') expect(note).not_to be_nil - expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**' + expect(note.note).to eq 'changed title from **{-Old-} title** to **{+New+} title**' end end end @@ -128,10 +128,10 @@ describe Issues::UpdateService, services: true do it 'creates system note about confidentiality change' do update_issue(confidential: true) - note = find_note('Made the issue confidential') + note = find_note('made the issue confidential') expect(note).not_to be_nil - expect(note.note).to eq 'Made the issue confidential' + expect(note.note).to eq 'made the issue confidential' end it 'executes confidential issue hooks' do @@ -269,8 +269,8 @@ describe Issues::UpdateService, services: true do before { update_issue(description: "- [x] Task 1\n- [X] Task 2") } it 'creates system note about task status change' do - note1 = find_note('Marked the task **Task 1** as completed') - note2 = find_note('Marked the task **Task 2** as completed') + note1 = find_note('marked the task **Task 1** as completed') + note2 = find_note('marked the task **Task 2** as completed') expect(note1).not_to be_nil expect(note2).not_to be_nil @@ -284,8 +284,8 @@ describe Issues::UpdateService, services: true do end it 'creates system note about task status change' do - note1 = find_note('Marked the task **Task 1** as incomplete') - note2 = find_note('Marked the task **Task 2** as incomplete') + note1 = find_note('marked the task **Task 1** as incomplete') + note2 = find_note('marked the task **Task 2** as incomplete') expect(note1).not_to be_nil expect(note2).not_to be_nil @@ -299,7 +299,7 @@ describe Issues::UpdateService, services: true do end it 'does not create a system note' do - note = find_note('Marked the task **Task 2** as incomplete') + note = find_note('marked the task **Task 2** as incomplete') expect(note).to be_nil end @@ -312,7 +312,7 @@ describe Issues::UpdateService, services: true do end it 'does not create a system note referencing the position the old item' do - note = find_note('Marked the task **Two** as incomplete') + note = find_note('marked the task **Two** as incomplete') expect(note).to be_nil end diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index 24c25e4350f..5f6a7716beb 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -42,7 +42,7 @@ describe MergeRequests::CloseService, services: true do it 'creates system note about merge_request reassign' do note = @merge_request.notes.last - expect(note.note).to include 'Status changed to closed' + expect(note.note).to include 'closed' end it 'marks todos as done' do diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 7db32a33c93..dff1781d2aa 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -34,7 +34,7 @@ describe MergeRequests::MergeService, services: true do it 'creates system note about merge_request merge' do note = merge_request.notes.last - expect(note.note).to include 'Status changed to merged' + expect(note.note).to include 'merged' end end diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb index 1f90efdbd6a..c0164138713 100644 --- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb @@ -34,7 +34,7 @@ describe MergeRequests::MergeWhenBuildSucceedsService do it 'creates a system note' do note = merge_request.notes.last - expect(note.note).to match /Enabled an automatic merge when the build for (\w+\/\w+@)?[0-9a-z]{8}/ + expect(note.note).to match /enabled an automatic merge when the build for (\w+\/\w+@)?\h{8}/ end end @@ -113,7 +113,7 @@ describe MergeRequests::MergeWhenBuildSucceedsService do it 'Posts a system note' do note = mr_merge_if_green_enabled.notes.last - expect(note.note).to include 'Canceled the automatic merge' + expect(note.note).to include 'canceled the automatic merge' end end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index e515bc9f89c..bc340ff9d3c 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -76,10 +76,10 @@ describe MergeRequests::RefreshService, services: true do reload_mrs end - it { expect(@merge_request.notes.last.note).to include('changed to merged') } + it { expect(@merge_request.notes.last.note).to include('merged') } it { expect(@merge_request).to be_merged } it { expect(@fork_merge_request).to be_merged } - it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') } + it { expect(@fork_merge_request.notes.last.note).to include('merged') } it { expect(@build_failed_todo).to be_done } it { expect(@fork_build_failed_todo).to be_done } end @@ -95,11 +95,11 @@ describe MergeRequests::RefreshService, services: true do reload_mrs end - it { expect(@merge_request.notes.last.note).to include('changed to merged') } + it { expect(@merge_request.notes.last.note).to include('merged') } it { expect(@merge_request).to be_merged } it { expect(@merge_request.diffs.size).to be > 0 } it { expect(@fork_merge_request).to be_merged } - it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') } + it { expect(@fork_merge_request.notes.last.note).to include('merged') } it { expect(@build_failed_todo).to be_done } it { expect(@fork_build_failed_todo).to be_done } end @@ -119,7 +119,7 @@ describe MergeRequests::RefreshService, services: true 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('Added 28 commits') } + it { expect(@fork_merge_request.notes.last.note).to include('added 28 commits') } it { expect(@fork_merge_request).to be_open } it { expect(@build_failed_todo).to be_pending } it { expect(@fork_build_failed_todo).to be_pending } @@ -146,7 +146,7 @@ describe MergeRequests::RefreshService, services: true do reload_mrs end - it { expect(@merge_request.notes.last.note).to include('changed to merged') } + it { expect(@merge_request.notes.last.note).to include('merged') } it { expect(@merge_request).to be_merged } it { expect(@fork_merge_request).to be_open } it { expect(@fork_merge_request.notes).to be_empty } @@ -169,8 +169,8 @@ describe MergeRequests::RefreshService, services: true do expect(@merge_request).to be_open notes = @fork_merge_request.notes.reorder(:created_at).map(&:note) - expect(notes[0]).to include('Restored source branch `master`') - expect(notes[1]).to include('Added 28 commits') + expect(notes[0]).to include('restored source branch `master`') + expect(notes[1]).to include('added 28 commits') expect(@fork_merge_request).to be_open end end diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb index af7424a76a9..a99d4eac9bd 100644 --- a/spec/services/merge_requests/reopen_service_spec.rb +++ b/spec/services/merge_requests/reopen_service_spec.rb @@ -41,7 +41,7 @@ describe MergeRequests::ReopenService, services: true do it 'creates system note about merge_request reopen' do note = merge_request.notes.last - expect(note.note).to include 'Status changed to reopened' + expect(note.note).to include 'reopened' end end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index cb5d7cdb467..0bd6db1810a 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -79,31 +79,31 @@ describe MergeRequests::UpdateService, services: true do end it 'creates system note about merge_request reassign' do - note = find_note('Reassigned to') + note = find_note('assigned to') expect(note).not_to be_nil - expect(note.note).to include "Reassigned to \@#{user2.username}" + expect(note.note).to include "assigned to #{user2.to_reference}" end it 'creates system note about merge_request label edit' do - note = find_note('Added ~') + note = find_note('added ~') expect(note).not_to be_nil - expect(note.note).to include "Added ~#{label.id} label" + expect(note.note).to include "added #{label.to_reference} label" end it 'creates system note about title change' do - note = find_note('Changed title:') + note = find_note('changed title') expect(note).not_to be_nil - expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**' + expect(note.note).to eq 'changed title from **{-Old-} title** to **{+New+} title**' end it 'creates system note about branch change' do - note = find_note('Target') + note = find_note('changed target') expect(note).not_to be_nil - expect(note.note).to eq 'Target branch changed from `master` to `target`' + expect(note.note).to eq 'changed target branch from `master` to `target`' end context 'when not including source branch removal options' do @@ -258,8 +258,8 @@ describe MergeRequests::UpdateService, services: true do before { update_merge_request({ description: "- [x] Task 1\n- [X] Task 2" }) } it 'creates system note about task status change' do - note1 = find_note('Marked the task **Task 1** as completed') - note2 = find_note('Marked the task **Task 2** as completed') + note1 = find_note('marked the task **Task 1** as completed') + note2 = find_note('marked the task **Task 2** as completed') expect(note1).not_to be_nil expect(note2).not_to be_nil @@ -273,8 +273,8 @@ describe MergeRequests::UpdateService, services: true do end it 'creates system note about task status change' do - note1 = find_note('Marked the task **Task 1** as incomplete') - note2 = find_note('Marked the task **Task 2** as incomplete') + note1 = find_note('marked the task **Task 1** as incomplete') + note2 = find_note('marked the task **Task 2** as incomplete') expect(note1).not_to be_nil expect(note2).not_to be_nil diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 2a5709c6322..4a8f6c321aa 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -50,7 +50,7 @@ describe SystemNoteService, services: true do context 'without existing commits' do it 'adds a message header' do - expect(note_lines[0]).to eq "Added #{new_commits.size} commits:" + expect(note_lines[0]).to eq "added #{new_commits.size} commits" end it 'adds a message line for each commit' do @@ -120,7 +120,7 @@ describe SystemNoteService, services: true do context 'when assignee added' do it 'sets the note text' do - expect(subject.note).to eq "Reassigned to @#{assignee.username}" + expect(subject.note).to eq "assigned to @#{assignee.username}" end end @@ -128,7 +128,7 @@ describe SystemNoteService, services: true do let(:assignee) { nil } it 'sets the note text' do - expect(subject.note).to eq 'Assignee removed' + expect(subject.note).to eq 'removed assignee' end end end @@ -147,7 +147,7 @@ describe SystemNoteService, services: true do let(:removed) { [] } it 'sets the note text' do - expect(subject.note).to eq "Added ~#{labels[0].id} ~#{labels[1].id} labels" + expect(subject.note).to eq "added ~#{labels[0].id} ~#{labels[1].id} labels" end end @@ -156,7 +156,7 @@ describe SystemNoteService, services: true do let(:removed) { labels } it 'sets the note text' do - expect(subject.note).to eq "Removed ~#{labels[0].id} ~#{labels[1].id} labels" + expect(subject.note).to eq "removed ~#{labels[0].id} ~#{labels[1].id} labels" end end @@ -165,7 +165,7 @@ describe SystemNoteService, services: true do let(:removed) { [labels[1]] } it 'sets the note text' do - expect(subject.note).to eq "Added ~#{labels[0].id} and removed ~#{labels[1].id} labels" + expect(subject.note).to eq "added ~#{labels[0].id} and removed ~#{labels[1].id} labels" end end end @@ -179,7 +179,7 @@ describe SystemNoteService, services: true do context 'when milestone added' do it 'sets the note text' do - expect(subject.note).to eq "Milestone changed to #{milestone.to_reference}" + expect(subject.note).to eq "changed milestone to #{milestone.to_reference}" end end @@ -187,7 +187,7 @@ describe SystemNoteService, services: true do let(:milestone) { nil } it 'sets the note text' do - expect(subject.note).to eq 'Milestone removed' + expect(subject.note).to eq 'removed milestone' end end end @@ -204,13 +204,13 @@ describe SystemNoteService, services: true do let(:source) { double('commit', gfm_reference: 'commit 123456') } it 'sets the note text' do - expect(subject.note).to eq "Status changed to #{status} by commit 123456" + expect(subject.note).to eq "#{status} via commit 123456" end end context 'without a source' do it 'sets the note text' do - expect(subject.note).to eq "Status changed to #{status}" + expect(subject.note).to eq status end end end @@ -226,7 +226,7 @@ describe SystemNoteService, services: true do it_behaves_like 'a system note' it "posts the Merge When Build Succeeds system note" do - expect(subject.note).to match /Enabled an automatic merge when the build for (\w+\/\w+@)?[0-9a-f]{40} succeeds/ + expect(subject.note).to match /enabled an automatic merge when the build for (\w+\/\w+@)?\h{40} succeeds/ end end @@ -240,7 +240,7 @@ describe SystemNoteService, services: true do it_behaves_like 'a system note' it "posts the Merge When Build Succeeds system note" do - expect(subject.note).to eq "Canceled the automatic merge" + expect(subject.note).to eq "canceled the automatic merge" end end @@ -252,7 +252,7 @@ describe SystemNoteService, services: true do it 'sets the note text' do expect(subject.note). - to eq "Changed title: **{-Old title-}** → **{+#{noteable.title}+}**" + to eq "changed title from **{-Old title-}** to **{+#{noteable.title}+}**" end end end @@ -264,7 +264,7 @@ describe SystemNoteService, services: true do it_behaves_like 'a system note' it 'sets the note text' do - expect(subject.note).to eq 'Made the issue visible' + expect(subject.note).to eq 'made the issue visible to everyone' end end end @@ -278,7 +278,7 @@ describe SystemNoteService, services: true do context 'when target branch name changed' do it 'sets the note text' do - expect(subject.note).to eq "Target branch changed from `#{old_branch}` to `#{new_branch}`" + expect(subject.note).to eq "changed target branch from `#{old_branch}` to `#{new_branch}`" end end end @@ -290,7 +290,7 @@ describe SystemNoteService, services: true do context 'when source branch deleted' do it 'sets the note text' do - expect(subject.note).to eq "Deleted source branch `feature`" + expect(subject.note).to eq "deleted source branch `feature`" end end end @@ -302,7 +302,7 @@ describe SystemNoteService, services: true do context 'when a branch is created from the new branch button' do it 'sets the note text' do - expect(subject.note).to match /\AStarted branch [`1-mepmep`]/ + expect(subject.note).to match /\Acreated branch [`1-mepmep`]/ end end end @@ -338,13 +338,13 @@ describe SystemNoteService, services: true do let(:mentioner) { project2.repository.commit } it 'references the mentioning commit' do - expect(subject.note).to eq "Mentioned in commit #{mentioner.to_reference(project)}" + expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference(project)}" end end context 'from non-Commit' do it 'references the mentioning object' do - expect(subject.note).to eq "Mentioned in issue #{mentioner.to_reference(project)}" + expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference(project)}" end end end @@ -354,13 +354,13 @@ describe SystemNoteService, services: true do let(:mentioner) { project.repository.commit } it 'references the mentioning commit' do - expect(subject.note).to eq "Mentioned in commit #{mentioner.to_reference}" + expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference}" end end context 'from non-Commit' do it 'references the mentioning object' do - expect(subject.note).to eq "Mentioned in issue #{mentioner.to_reference}" + expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference}" end end end @@ -370,7 +370,11 @@ describe SystemNoteService, services: true do describe '.cross_reference?' do it 'is truthy when text begins with expected text' do - expect(described_class.cross_reference?('Mentioned in something')).to be_truthy + expect(described_class.cross_reference?('mentioned in something')).to be_truthy + end + + it 'is truthy when text begins with legacy capitalized expected text' do + expect(described_class.cross_reference?('mentioned in something')).to be_truthy end it 'is falsey when text does not begin with expected text' do @@ -433,6 +437,19 @@ describe SystemNoteService, services: true do expect(described_class.cross_reference_exists?(noteable, commit1)). to be_falsey end + + context 'legacy capitalized cross reference' do + before do + # Mention issue (noteable) from commit0 + system_note = described_class.cross_reference(noteable, commit0, author) + system_note.update(note: system_note.note.capitalize) + end + + it 'is truthy when already mentioned' do + expect(described_class.cross_reference_exists?(noteable, commit0)). + to be_truthy + end + end end context 'commit from commit' do @@ -450,6 +467,19 @@ describe SystemNoteService, services: true do expect(described_class.cross_reference_exists?(commit1, commit0)). to be_falsey end + + context 'legacy capitalized cross reference' do + before do + # Mention commit1 from commit0 + system_note = described_class.cross_reference(commit0, commit1, author) + system_note.update(note: system_note.note.capitalize) + end + + it 'is truthy when already mentioned' do + expect(described_class.cross_reference_exists?(commit0, commit1)). + to be_truthy + end + end end context 'commit with cross-reference from fork' do @@ -465,6 +495,18 @@ describe SystemNoteService, services: true do expect(described_class.cross_reference_exists?(noteable, commit2)). to be true end + + context 'legacy capitalized cross reference' do + before do + system_note = described_class.cross_reference(noteable, commit0, author2) + system_note.update(note: system_note.note.capitalize) + end + + it 'is true when a fork mentions an external issue' do + expect(described_class.cross_reference_exists?(noteable, commit2)). + to be true + end + end end end @@ -498,7 +540,7 @@ describe SystemNoteService, services: true do it_behaves_like 'cross project mentionable' it 'notifies about noteable being moved to' do - expect(subject.note).to match /Moved to/ + expect(subject.note).to match /moved to/ end end @@ -508,7 +550,7 @@ describe SystemNoteService, services: true do it_behaves_like 'cross project mentionable' it 'notifies about noteable being moved from' do - expect(subject.note).to match /Moved from/ + expect(subject.note).to match /moved from/ end end -- cgit v1.2.1 From 058c652904b13cea73e92615776f29fd1a8a1ded Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 24 Nov 2016 10:32:55 +0000 Subject: Fixed issue boards issue sorting when dragging issue into list Currently it just appends the new issue to the end of list & then sorts by priority which can cause some strange effects. For example if you drag the issue to the top of the list & then vue re-renders, the issue actually goes to the bottom. --- app/assets/javascripts/boards/components/board_list.js.es6 | 2 +- app/assets/javascripts/boards/models/list.js.es6 | 8 ++++++-- app/assets/javascripts/boards/stores/boards_store.js.es6 | 4 ++-- changelogs/unreleased/boards-issue-sorting.yml | 4 ++++ 4 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 changelogs/unreleased/boards-issue-sorting.yml diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index 8e91cbfac75..8996ca438a6 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -94,7 +94,7 @@ gl.issueBoards.onStart(); }, onAdd: (e) => { - gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue); + gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex); this.$nextTick(() => { e.item.remove(); diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index 8a7dc67409e..429bd27c3fb 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -106,9 +106,13 @@ class List { }); } - addIssue (issue, listFrom) { + addIssue (issue, listFrom, newIndex) { if (!this.findIssue(issue.id)) { - this.issues.push(issue); + if (newIndex !== undefined) { + this.issues.splice(newIndex, 0, issue); + } else { + this.issues.push(issue); + } if (this.label) { issue.addLabel(this.label); diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index 6bc95aa60f2..bb2a4de8b18 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -89,14 +89,14 @@ }); listFrom.update(); }, - moveIssueToList (listFrom, listTo, issue) { + moveIssueToList (listFrom, listTo, issue, newIndex) { const issueTo = listTo.findIssue(issue.id), issueLists = issue.getLists(), listLabels = issueLists.map( listIssue => listIssue.label ); // Add to new lists issues if it doesn't already exist if (!issueTo) { - listTo.addIssue(issue, listFrom); + listTo.addIssue(issue, listFrom, newIndex); } if (listTo.type === 'done' && listFrom.type !== 'backlog') { diff --git a/changelogs/unreleased/boards-issue-sorting.yml b/changelogs/unreleased/boards-issue-sorting.yml new file mode 100644 index 00000000000..fb7dc2f9190 --- /dev/null +++ b/changelogs/unreleased/boards-issue-sorting.yml @@ -0,0 +1,4 @@ +--- +title: Fixed issue boards issue sorting when dragging issue into list +merge_request: +author: -- cgit v1.2.1 From 18a54116e2f1d2ac88d03ad7ed48213dab580854 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 24 Nov 2016 10:38:24 +0000 Subject: Fixed dragging issue moving wrong issue after multiple drags of issue --- app/assets/javascripts/boards/components/board_list.js.es6 | 2 +- app/views/projects/boards/components/_card.html.haml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index 8996ca438a6..48e37fa64ea 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -88,8 +88,8 @@ const card = this.$refs.issue[e.oldIndex]; card.showDetail = false; - Store.moving.issue = card.issue; Store.moving.list = card.list; + Store.moving.issue = Store.moving.list.findIssue(+e.item.dataset.issueId); gl.issueBoards.onStart(); }, diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml index 34effac17b2..1f31496e73f 100644 --- a/app/views/projects/boards/components/_card.html.haml +++ b/app/views/projects/boards/components/_card.html.haml @@ -1,5 +1,6 @@ %li.card{ ":class" => '{ "user-can-drag": !disabled && issue.id, "is-disabled": disabled || !issue.id, "is-active": issueDetailVisible }', ":index" => "index", + ":data-issue-id" => "issue.id", "@mousedown" => "mouseDown", "@mousemove" => "mouseMove", "@mouseup" => "showIssue($event)" } -- cgit v1.2.1 From 101cde38cf6d5506ea37c5f912fb4c37af50c541 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Thu, 24 Nov 2016 17:00:37 +0800 Subject: Use Ci::Pipeline#latest for finding pipelines Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7333#note_18861407 --- lib/gitlab/badge/build/status.rb | 3 ++- spec/lib/gitlab/badge/build/status_spec.rb | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/badge/build/status.rb b/lib/gitlab/badge/build/status.rb index 50aa45e5406..f78dfd5b83a 100644 --- a/lib/gitlab/badge/build/status.rb +++ b/lib/gitlab/badge/build/status.rb @@ -20,7 +20,8 @@ module Gitlab def status @project.pipelines - .where(sha: @sha, ref: @ref) + .where(sha: @sha) + .latest(@ref) .status || 'unknown' end diff --git a/spec/lib/gitlab/badge/build/status_spec.rb b/spec/lib/gitlab/badge/build/status_spec.rb index 38eebb2a176..70f03021d36 100644 --- a/spec/lib/gitlab/badge/build/status_spec.rb +++ b/spec/lib/gitlab/badge/build/status_spec.rb @@ -69,8 +69,8 @@ describe Gitlab::Badge::Build::Status do new_build.success! end - it 'reports the compound status' do - expect(badge.status).to eq 'failed' + it 'does not take outdated pipeline into account' do + expect(badge.status).to eq 'success' end end end -- cgit v1.2.1 From 2f5637d6e39efa4b1a1cd2acdffa478e903ecd98 Mon Sep 17 00:00:00 2001 From: tiagonbotelho <tiagonbotelho@hotmail.com> Date: Wed, 23 Nov 2016 13:37:42 +0000 Subject: renames some of the specs and adds changelog entry --- .../24779-last-deployment-call-on-nil-environment-fix.yml | 4 ++++ spec/views/projects/builds/show.html.haml_spec.rb | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/24779-last-deployment-call-on-nil-environment-fix.yml diff --git a/changelogs/unreleased/24779-last-deployment-call-on-nil-environment-fix.yml b/changelogs/unreleased/24779-last-deployment-call-on-nil-environment-fix.yml new file mode 100644 index 00000000000..5e7580fb8f2 --- /dev/null +++ b/changelogs/unreleased/24779-last-deployment-call-on-nil-environment-fix.yml @@ -0,0 +1,4 @@ +--- +title: fixes last_deployment call environment is nil +merge_request: 7671 +author: diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb index 869dcb417e6..745d0c745bd 100644 --- a/spec/views/projects/builds/show.html.haml_spec.rb +++ b/spec/views/projects/builds/show.html.haml_spec.rb @@ -88,7 +88,7 @@ describe 'projects/builds/show', :view do create(:ci_build, :running, environment: 'staging', pipeline: pipeline) end - context 'and environment does exist' do + context 'when environment exists' do let!(:environment) do create(:environment, name: 'staging', project: project) end @@ -101,7 +101,7 @@ describe 'projects/builds/show', :view do '.environment-information', text: expected_text) end - context 'and has deployment' do + context 'when it has deployment' do let!(:deployment) do create(:deployment, environment: environment) end @@ -118,7 +118,7 @@ describe 'projects/builds/show', :view do end end - context 'and environment does not exist' do + context 'when environment does not exist' do it 'shows deployment message' do expected_text = 'This build is creating a deployment to staging' render -- cgit v1.2.1 From 2749c1b5687bb50b980dc7bfc1d3471ea16f1ccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Thu, 24 Nov 2016 13:05:00 +0100 Subject: Stop supporting Google and Azure as backup strategies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The amount of gems required is quite high compared to the usefulness of the features. Related to !4928, !6713 Signed-off-by: Rémy Coutable <remy@rymai.me> --- Gemfile | 2 -- Gemfile.lock | 31 ---------------------- changelogs/unreleased/remove-backup-strategies.yml | 4 +++ doc/raketasks/backup_restore.md | 2 +- 4 files changed, 5 insertions(+), 34 deletions(-) create mode 100644 changelogs/unreleased/remove-backup-strategies.yml diff --git a/Gemfile b/Gemfile index 9e815925a1f..e7f248ee766 100644 --- a/Gemfile +++ b/Gemfile @@ -85,10 +85,8 @@ gem 'dropzonejs-rails', '~> 0.7.1' # for backups gem 'fog-aws', '~> 0.9' -gem 'fog-azure', '~> 0.0' gem 'fog-core', '~> 1.40' gem 'fog-local', '~> 0.3' -gem 'fog-google', '~> 0.3' gem 'fog-openstack', '~> 0.1' gem 'fog-rackspace', '~> 0.1.1' diff --git a/Gemfile.lock b/Gemfile.lock index bdc60552480..3684974a766 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -66,21 +66,6 @@ GEM descendants_tracker (~> 0.0.4) ice_nine (~> 0.11.0) thread_safe (~> 0.3, >= 0.3.1) - azure (0.7.5) - addressable (~> 2.3) - azure-core (~> 0.1) - faraday (~> 0.9) - faraday_middleware (~> 0.10) - json (~> 1.8) - mime-types (>= 1, < 3.0) - nokogiri (~> 1.6) - systemu (~> 2.6) - thor (~> 0.19) - uuid (~> 2.0) - azure-core (0.1.2) - faraday (~> 0.9) - faraday_middleware (~> 0.10) - nokogiri (~> 1.6) babel-source (5.8.35) babel-transpiler (0.7.0) babel-source (>= 4.0, < 6) @@ -217,19 +202,10 @@ GEM fog-json (~> 1.0) fog-xml (~> 0.1) ipaddress (~> 0.8) - fog-azure (0.0.2) - azure (~> 0.6) - fog-core (~> 1.27) - fog-json (~> 1.0) - fog-xml (~> 0.1) fog-core (1.42.0) builder excon (~> 0.49) formatador (~> 0.2) - fog-google (0.3.2) - fog-core - fog-json - fog-xml fog-json (1.0.2) fog-core (~> 1.0) multi_json (~> 1.10) @@ -397,8 +373,6 @@ GEM rb-inotify (>= 0.9) loofah (2.0.3) nokogiri (>= 1.5.9) - macaddr (1.7.1) - systemu (~> 2.6.2) mail (2.6.4) mime-types (>= 1.16, < 4) mail_room (0.9.0) @@ -728,7 +702,6 @@ GEM sys-filesystem (1.1.6) ffi sysexits (1.2.0) - systemu (2.6.5) teaspoon (1.1.5) railties (>= 3.2.5, < 6) teaspoon-jasmine (2.2.0) @@ -768,8 +741,6 @@ GEM get_process_mem (~> 0) unicorn (>= 4, < 6) uniform_notifier (1.10.0) - uuid (2.3.8) - macaddr (~> 1.0) version_sorter (2.1.0) virtus (1.0.5) axiom-types (~> 0.1) @@ -849,9 +820,7 @@ DEPENDENCIES ffaker (~> 2.0.0) flay (~> 2.6.1) fog-aws (~> 0.9) - fog-azure (~> 0.0) fog-core (~> 1.40) - fog-google (~> 0.3) fog-local (~> 0.3) fog-openstack (~> 0.1) fog-rackspace (~> 0.1.1) diff --git a/changelogs/unreleased/remove-backup-strategies.yml b/changelogs/unreleased/remove-backup-strategies.yml new file mode 100644 index 00000000000..9f034613c2c --- /dev/null +++ b/changelogs/unreleased/remove-backup-strategies.yml @@ -0,0 +1,4 @@ +--- +title: Stop supporting Google and Azure as backup strategies +merge_request: +author: diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 7484bc2295e..17485b11c09 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -88,7 +88,7 @@ It uses the [Fog library](http://fog.io/) to perform the upload. In the example below we use Amazon S3 for storage, but Fog also lets you use [other storage providers](http://fog.io/storage/). GitLab [imports cloud drivers](https://gitlab.com/gitlab-org/gitlab-ce/blob/30f5b9a5b711b46f1065baf755e413ceced5646b/Gemfile#L88) -for AWS, Azure, Google, OpenStack Swift and Rackspace as well. A local driver is +for AWS, OpenStack Swift and Rackspace as well. A local driver is [also available](#uploading-to-locally-mounted-shares). For omnibus packages: -- cgit v1.2.1 From 304163becba3610a99dfff644c13972a2f54ed3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Thu, 24 Nov 2016 13:22:38 +0100 Subject: API: Use `#find_project` in API::Triggers and API::Services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- lib/api/services.rb | 2 +- lib/api/triggers.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/api/services.rb b/lib/api/services.rb index 4d23499aa39..bc427705777 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -65,7 +65,7 @@ module API detail 'Added in GitLab 8.13' end post ':id/services/:service_slug/trigger' do - project = Project.find_with_namespace(params[:id]) || Project.find_by(id: params[:id]) + project = find_project(params[:id]) # This is not accurate, but done to prevent leakage of the project names not_found!('Service') unless project diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index 569598fbd2c..bb4de39def1 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -13,7 +13,7 @@ module API optional :variables, type: Hash, desc: 'The list of variables to be injected into build' end post ":id/(ref/:ref/)trigger/builds" do - project = Project.find_with_namespace(params[:id]) || Project.find_by(id: params[:id]) + project = find_project(params[:id]) trigger = Ci::Trigger.find_by_token(params[:token].to_s) not_found! unless project && trigger unauthorized! unless trigger.project == project -- cgit v1.2.1 From 9dfbfbb2d10dfc6297acff1b59dfbaf43b848d96 Mon Sep 17 00:00:00 2001 From: Robert Schilling <rschilling@student.tugraz.at> Date: Thu, 24 Nov 2016 13:30:53 +0100 Subject: Don't convert data which already is the target type --- lib/api/commit_statuses.rb | 2 +- lib/api/commits.rb | 2 +- lib/api/groups.rb | 2 +- lib/api/variables.rb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index f54d4f06627..492884d162b 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -77,7 +77,7 @@ module API ) begin - case params[:state].to_s + case params[:state] when 'pending' status.enqueue! when 'running' diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 0319d076ecb..2670a2d413a 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -48,7 +48,7 @@ module API requires :id, type: Integer, desc: 'The project ID' requires :branch_name, type: String, desc: 'The name of branch' requires :commit_message, type: String, desc: 'Commit message' - requires :actions, type: Array, desc: 'Actions to perform in commit' + requires :actions, type: Array[Hash], desc: 'Actions to perform in commit' optional :author_email, type: String, desc: 'Author email for commit' optional :author_name, type: String, desc: 'Author name for commit' end diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 48ad3b80ae0..fc39fdf4b67 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -33,7 +33,7 @@ module API groups = groups.search(params[:search]) if params[:search].present? groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present? - groups = groups.reorder(params[:order_by] => params[:sort].to_sym) + groups = groups.reorder(params[:order_by] => params[:sort]) present paginate(groups), with: Entities::Group end diff --git a/lib/api/variables.rb b/lib/api/variables.rb index 90f904b8a12..f623b1dfe9f 100644 --- a/lib/api/variables.rb +++ b/lib/api/variables.rb @@ -30,7 +30,7 @@ module API end get ':id/variables/:key' do key = params[:key] - variable = user_project.variables.find_by(key: key.to_s) + variable = user_project.variables.find_by(key: key) return not_found!('Variable') unless variable -- cgit v1.2.1 From 4f5ed812325845f263fc9b566651c1179b5c24bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Thu, 24 Nov 2016 14:40:35 +0100 Subject: API: Introduce `#find_project!` which also check access permission MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- lib/api/helpers.rb | 17 ++++++++++------- lib/api/projects.rb | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 60067758e95..42f4c2ccf9d 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -68,7 +68,7 @@ module API end def user_project - @project ||= find_project(params[:id]) + @project ||= find_project!(params[:id]) end def available_labels @@ -76,12 +76,15 @@ module API end def find_project(id) - project = - if id =~ /^\d+$/ - Project.find_by(id: id) - else - Project.find_with_namespace(id) - end + if id =~ /^\d+$/ + Project.find_by(id: id) + else + Project.find_with_namespace(id) + end + end + + def find_project!(id) + project = find_project(id) if can?(current_user, :read_project, project) project diff --git a/lib/api/projects.rb b/lib/api/projects.rb index ddfde178d30..2ea3c433ae2 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -379,7 +379,7 @@ module API # POST /projects/:id/fork/:forked_from_id post ":id/fork/:forked_from_id" do authenticated_as_admin! - forked_from_project = find_project(params[:forked_from_id]) + forked_from_project = find_project!(params[:forked_from_id]) unless forked_from_project.nil? if user_project.forked_from_project.nil? user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id) -- cgit v1.2.1 From 28f4d7aa28c6ce2765fce7922058c82f7633dae3 Mon Sep 17 00:00:00 2001 From: Robert Schilling <rschilling@student.tugraz.at> Date: Thu, 24 Nov 2016 15:29:22 +0100 Subject: You can only assign default_branch when editing a project or when creating a project for a specified user [ci skip] You can only assign default_branch when editing a project [ci skip] --- doc/api/projects.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/projects.md b/doc/api/projects.md index b02a901d884..bd27a0a6fae 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -592,7 +592,6 @@ Parameters: | `name` | string | yes | The name of the new project | | `path` | string | no | Custom repository name for new project. By default generated based on name | | `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) | -| `default_branch` | string | no | `master` by default | | `description` | string | no | Short project description | | `issues_enabled` | boolean | no | Enable issues for this project | | `merge_requests_enabled` | boolean | no | Enable merge requests for this project | @@ -625,6 +624,7 @@ Parameters: | `user_id` | integer | yes | The user ID of the project owner | | `name` | string | yes | The name of the new project | | `path` | string | no | Custom repository name for new project. By default generated based on name | +| `default_branch` | string | no | `master` by default | | `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) | | `description` | string | no | Short project description | | `issues_enabled` | boolean | no | Enable issues for this project | -- cgit v1.2.1 From 785d5c8ed15edbabb5704072051569d8db61f4a1 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Thu, 24 Nov 2016 15:58:31 +0100 Subject: Create pipeline along with builds in the transation --- app/services/ci/create_pipeline_service.rb | 12 +++++++++--- app/services/ci/process_pipeline_service.rb | 9 --------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index cde856b0186..e3bc9847200 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -45,9 +45,15 @@ module Ci return error('No builds for this pipeline.') end - pipeline.save - pipeline.process! - pipeline + Ci::Pipeline.transaction do + pipeline.save + + Ci::CreatePipelineBuildsService + .new(project, current_user) + .execute(pipeline) + end + + pipeline.tap(&:process!) end private diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 8face432d97..e6bd1d1460c 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -5,11 +5,6 @@ module Ci def execute(pipeline) @pipeline = pipeline - # This method will ensure that our pipeline does have all builds for all stages created - if created_builds.empty? - create_builds! - end - new_builds = stage_indexes_of_created_builds.map do |index| process_stage(index) @@ -22,10 +17,6 @@ module Ci private - def create_builds! - Ci::CreatePipelineBuildsService.new(project, current_user).execute(pipeline) - end - def process_stage(index) current_status = status_for_prior_stages(index) -- cgit v1.2.1 From 81ba3f9177fcfd76f6b3b715c572ce4920398345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Thu, 24 Nov 2016 16:58:32 +0100 Subject: API: Introduce `#find_group!` which also check access permission MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- lib/api/groups.rb | 8 ++++---- lib/api/helpers.rb | 10 +++++++++- lib/api/helpers/members_helpers.rb | 2 +- lib/api/issues.rb | 2 +- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 48ad3b80ae0..a3489c4eb92 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -82,7 +82,7 @@ module API :lfs_enabled, :request_access_enabled end put ':id' do - group = find_group(params[:id]) + group = find_group!(params[:id]) authorize! :admin_group, group if ::Groups::UpdateService.new(group, current_user, declared_params(include_missing: false)).execute @@ -96,13 +96,13 @@ module API success Entities::GroupDetail end get ":id" do - group = find_group(params[:id]) + group = find_group!(params[:id]) present group, with: Entities::GroupDetail end desc 'Remove a group.' delete ":id" do - group = find_group(params[:id]) + group = find_group!(params[:id]) authorize! :admin_group, group DestroyGroupService.new(group, current_user).execute end @@ -111,7 +111,7 @@ module API success Entities::Project end get ":id/projects" do - group = find_group(params[:id]) + group = find_group!(params[:id]) projects = GroupProjectsFinder.new(group).execute(current_user) projects = paginate projects present projects, with: Entities::Project, user: current_user diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 42f4c2ccf9d..0d3ddb89dc3 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -105,7 +105,15 @@ module API end def find_group(id) - group = Group.find_by(path: id) || Group.find_by(id: id) + if id =~ /^\d+$/ + Group.find_by(id: id) + else + Group.find_by(path: id) + end + end + + def find_group!(id) + group = find_group(id) if can?(current_user, :read_group, group) group diff --git a/lib/api/helpers/members_helpers.rb b/lib/api/helpers/members_helpers.rb index 90114f6f667..d9cae1501f8 100644 --- a/lib/api/helpers/members_helpers.rb +++ b/lib/api/helpers/members_helpers.rb @@ -2,7 +2,7 @@ module API module Helpers module MembersHelpers def find_source(source_type, id) - public_send("find_#{source_type}", id) + public_send("find_#{source_type}!", id) end def authorize_admin_source!(source_type, source) diff --git a/lib/api/issues.rb b/lib/api/issues.rb index eea5b91d4f9..2fea71870b8 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -68,7 +68,7 @@ module API # GET /groups/:id/issues?milestone=1.0.0 # GET /groups/:id/issues?milestone=1.0.0&state=closed get ":id/issues" do - group = find_group(params[:id]) + group = find_group!(params[:id]) params[:state] ||= 'opened' params[:group_id] = group.id -- cgit v1.2.1 From 141b4d28c615db0a3e25868efd47fd0f2946fdbe Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 24 Nov 2016 16:54:24 +0000 Subject: Fixed issue boards scrolling with a lot of lists & issues When a board has a lot of lists & issues scrolling stops the user from moving the issue to the lsat list (or any list not on screen). This changes that by making the scrollable element the board-list element. This will need re-thinking when sorting in lists is possible. --- app/assets/javascripts/boards/components/board_list.js.es6 | 1 + changelogs/unreleased/issue-boards-scrollable-element.yml | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 changelogs/unreleased/issue-boards-scrollable-element.yml diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index 8e91cbfac75..1e2bf445d75 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -80,6 +80,7 @@ }, mounted () { const options = gl.issueBoards.getBoardSortableDefaultOptions({ + scroll: document.querySelectorAll('.boards-list')[0], group: 'issues', sort: false, disabled: this.disabled, diff --git a/changelogs/unreleased/issue-boards-scrollable-element.yml b/changelogs/unreleased/issue-boards-scrollable-element.yml new file mode 100644 index 00000000000..90edc30e791 --- /dev/null +++ b/changelogs/unreleased/issue-boards-scrollable-element.yml @@ -0,0 +1,4 @@ +--- +title: Fixed issue boards scrolling with a lot of lists & issues +merge_request: +author: -- cgit v1.2.1 From ed61d44e1edfd41c36ec9085aa95f470bb5699fa Mon Sep 17 00:00:00 2001 From: Luis Alonso Chavez Armendariz <lchavez@nearsoft.com> Date: Thu, 24 Nov 2016 10:28:52 -0700 Subject: Remove unnecessary sentences for status codes in the API documentation --- changelogs/unreleased/issue-24534.yml | 4 ++++ doc/api/access_requests.md | 8 ------- doc/api/award_emoji.md | 4 ++-- doc/api/boards.md | 10 -------- doc/api/branches.md | 7 +----- doc/api/broadcast_messages.md | 4 ---- doc/api/issues.md | 31 ++++++------------------- doc/api/labels.md | 25 ++++---------------- doc/api/members.md | 10 -------- doc/api/merge_requests.md | 43 +++++++++++------------------------ doc/api/notes.md | 9 +++----- doc/api/projects.md | 19 +++------------- doc/api/system_hooks.md | 3 +-- doc/api/tags.md | 21 ++++++----------- doc/api/users.md | 8 ++----- 15 files changed, 48 insertions(+), 158 deletions(-) create mode 100644 changelogs/unreleased/issue-24534.yml diff --git a/changelogs/unreleased/issue-24534.yml b/changelogs/unreleased/issue-24534.yml new file mode 100644 index 00000000000..14d6730d3f6 --- /dev/null +++ b/changelogs/unreleased/issue-24534.yml @@ -0,0 +1,4 @@ +--- +title: Remove unnecessary sentences for status codes in the API documentation +merge_request: +author: Luis Alonso Chavez Armendariz diff --git a/doc/api/access_requests.md b/doc/api/access_requests.md index ea308b54d62..dee3e384080 100644 --- a/doc/api/access_requests.md +++ b/doc/api/access_requests.md @@ -18,8 +18,6 @@ Gets a list of access requests viewable by the authenticated user. -Returns `200` if the request succeeds. - ``` GET /groups/:id/access_requests GET /projects/:id/access_requests @@ -61,8 +59,6 @@ Example response: Requests access for the authenticated user to a group or project. -Returns `201` if the request succeeds. - ``` POST /groups/:id/access_requests POST /projects/:id/access_requests @@ -94,8 +90,6 @@ Example response: Approves an access request for the given user. -Returns `201` if the request succeeds. - ``` PUT /groups/:id/access_requests/:user_id/approve PUT /projects/:id/access_requests/:user_id/approve @@ -129,8 +123,6 @@ Example response: Denies an access request for the given user. -Returns `200` if the request succeeds. - ``` DELETE /groups/:id/access_requests/:user_id DELETE /projects/:id/access_requests/:user_id diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md index 06111f4ab67..58092bdd400 100644 --- a/doc/api/award_emoji.md +++ b/doc/api/award_emoji.md @@ -158,7 +158,7 @@ Example Response: ### Delete an award emoji Sometimes its just not meant to be, and you'll have to remove your award. Only available to -admins or the author of the award. Status code 200 on success, 401 if unauthorized. +admins or the author of the award. ``` DELETE /projects/:id/issues/:issue_id/award_emoji/:award_id @@ -331,7 +331,7 @@ Example Response: ### Delete an award emoji Sometimes its just not meant to be, and you'll have to remove your award. Only available to -admins or the author of the award. Status code 200 on success, 401 if unauthorized. +admins or the author of the award. ``` DELETE /projects/:id/issues/:issue_id/notes/:note_id/award_emoji/:award_id diff --git a/doc/api/boards.md b/doc/api/boards.md index 28681719f43..c83db6df80c 100644 --- a/doc/api/boards.md +++ b/doc/api/boards.md @@ -148,10 +148,6 @@ Example response: Creates a new Issue Board list. -If the operation is successful, a status code of `200` and the newly-created -list is returned. If an error occurs, an error number and a message explaining -the reason is returned. - ``` POST /projects/:id/boards/:board_id/lists ``` @@ -184,10 +180,6 @@ Example response: Updates an existing Issue Board list. This call is used to change list position. -If the operation is successful, a code of `200` and the updated board list is -returned. If an error occurs, an error number and a message explaining the -reason is returned. - ``` PUT /projects/:id/boards/:board_id/lists/:list_id ``` @@ -220,8 +212,6 @@ Example response: ## Delete a board list Only for admins and project owners. Soft deletes the board list in question. -If the operation is successful, a status code `200` is returned. In case you cannot -destroy this board list, or it is not present, code `404` is given. ``` DELETE /projects/:id/boards/:board_id/lists/:list_id diff --git a/doc/api/branches.md b/doc/api/branches.md index f68eeb9f86b..07dfa5d4d7f 100644 --- a/doc/api/branches.md +++ b/doc/api/branches.md @@ -212,9 +212,6 @@ Example response: } ``` -It returns `200` if it succeeds or `400` if failed with an error message -explaining the reason. - ## Delete repository branch ``` @@ -226,8 +223,7 @@ DELETE /projects/:id/repository/branches/:branch | `id` | integer | yes | The ID of a project | | `branch` | string | yes | The name of the branch | -It returns `200` if it succeeds, `404` if the branch to be deleted does not exist -or `400` for other reasons. In case of an error, an explaining message is provided. +In case of an error, an explaining message is provided. ```bash curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches/newbranch" @@ -253,7 +249,6 @@ DELETE /projects/:id/repository/merged_branches | --------- | ---- | -------- | ----------- | | `id` | integer | yes | The ID of a project | -It returns `200` to indicate deletion of all merged branches was started. ```bash curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/merged_branches" diff --git a/doc/api/broadcast_messages.md b/doc/api/broadcast_messages.md index c3a9207a3ae..a3e9c01f335 100644 --- a/doc/api/broadcast_messages.md +++ b/doc/api/broadcast_messages.md @@ -62,10 +62,6 @@ Example response: ## Create a broadcast message -Responds with `400 Bad request` when the `message` parameter is missing or the -`color` or `font` values are invalid, and `201 Created` when the broadcast -message was successfully created. - ``` POST /broadcast_messages ``` diff --git a/doc/api/issues.md b/doc/api/issues.md index 134263d27b4..16f8e32c82a 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -315,10 +315,6 @@ Example response: Creates a new project issue. -If the operation is successful, a status code of `200` and the newly-created -issue is returned. If an error occurs, an error number and a message explaining -the reason is returned. - ``` POST /projects/:id/issues ``` @@ -377,10 +373,6 @@ Example response: Updates an existing project issue. This call is also used to mark an issue as closed. -If the operation is successful, a code of `200` and the updated issue is -returned. If an error occurs, an error number and a message explaining the -reason is returned. - ``` PUT /projects/:id/issues/:issue_id ``` @@ -439,8 +431,6 @@ Example response: ## Delete an issue Only for admins and project owners. Soft deletes the issue in question. -If the operation is successful, a status code `200` is returned. In case you cannot -destroy this issue, or it is not present, code `404` is given. ``` DELETE /projects/:id/issues/:issue_id @@ -457,9 +447,7 @@ curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://git ## Move an issue -Moves an issue to a different project. If the operation is successful, a status -code `201` together with moved issue is returned. If the project, issue, or -target project is not found, error `404` is returned. If the target project +Moves an issue to a different project. If the target project equals the source project or the user has insufficient permissions to move an issue, error `400` together with an explaining error message is returned. @@ -518,11 +506,9 @@ Example response: ## Subscribe to an issue -Subscribes the authenticated user to an issue to receive notifications. If the -operation is successful, status code `201` together with the updated issue is -returned. If the user is already subscribed to the issue, the status code `304` -is returned. If the project or issue is not found, status code `404` is -returned. +Subscribes the authenticated user to an issue to receive notifications. +If the user is already subscribed to the issue, the status code `304` +is returned. ``` POST /projects/:id/issues/:issue_id/subscription @@ -576,10 +562,8 @@ Example response: ## Unsubscribe from an issue Unsubscribes the authenticated user from the issue to not receive notifications -from it. If the operation is successful, status code `200` together with the -updated issue is returned. If the user is not subscribed to the issue, the -status code `304` is returned. If the project or issue is not found, status code -`404` is returned. +from it. If the user is not subscribed to the issue, the +status code `304` is returned. ``` DELETE /projects/:id/issues/:issue_id/subscription @@ -633,8 +617,7 @@ Example response: ## Create a todo -Manually creates a todo for the current user on an issue. If the request is -successful, status code `200` together with the created todo is returned. If +Manually creates a todo for the current user on an issue. If there already exists a todo for the user on that issue, status code `304` is returned. diff --git a/doc/api/labels.md b/doc/api/labels.md index 78686fdcad4..863b28c23b7 100644 --- a/doc/api/labels.md +++ b/doc/api/labels.md @@ -82,9 +82,6 @@ Example response: Creates a new label for the given repository with the given name and color. -It returns 200 if the label was successfully created, 400 for wrong parameters -and 409 if the label already exists. - ``` POST /projects/:id/labels ``` @@ -121,10 +118,6 @@ Example response: Deletes a label with a given name. -It returns 200 if the label was successfully deleted, 400 for wrong parameters -and 404 if the label does not exist. -In case of an error, an additional error message is returned. - ``` DELETE /projects/:id/labels ``` @@ -159,10 +152,6 @@ Example response: Updates an existing label with new name or new color. At least one parameter is required, to update the label. -It returns 200 if the label was successfully deleted, 400 for wrong parameters -and 404 if the label does not exist. -In case of an error, an additional error message is returned. - ``` PUT /projects/:id/labels ``` @@ -199,11 +188,9 @@ Example response: ## Subscribe to a label -Subscribes the authenticated user to a label to receive notifications. If the -operation is successful, status code `201` together with the updated label is -returned. If the user is already subscribed to the label, the status code `304` -is returned. If the project or label is not found, status code `404` is -returned. +Subscribes the authenticated user to a label to receive notifications. +If the user is already subscribed to the label, the status code `304` +is returned. ``` POST /projects/:id/labels/:label_id/subscription @@ -237,10 +224,8 @@ Example response: ## Unsubscribe from a label Unsubscribes the authenticated user from a label to not receive notifications -from it. If the operation is successful, status code `200` together with the -updated label is returned. If the user is not subscribed to the label, the -status code `304` is returned. If the project or label is not found, status code -`404` is returned. +from it. If the user is not subscribed to the label, the +status code `304` is returned. ``` DELETE /projects/:id/labels/:label_id/subscription diff --git a/doc/api/members.md b/doc/api/members.md index 6535e9a7801..5dcb2a5f60a 100644 --- a/doc/api/members.md +++ b/doc/api/members.md @@ -16,8 +16,6 @@ The access levels are defined in the `Gitlab::Access` module. Currently, these l Gets a list of group or project members viewable by the authenticated user. -Returns `200` if the request succeeds. - ``` GET /groups/:id/members GET /projects/:id/members @@ -60,8 +58,6 @@ Example response: Gets a member of a group or project. -Returns `200` if the request succeeds. - ``` GET /groups/:id/members/:user_id GET /projects/:id/members/:user_id @@ -95,8 +91,6 @@ Example response: Adds a member to a group or project. -Returns `201` if the request succeeds. - ``` POST /groups/:id/members POST /projects/:id/members @@ -131,8 +125,6 @@ Example response: Updates a member of a group or project. -Returns `200` if the request succeeds. - ``` PUT /groups/:id/members/:user_id PUT /projects/:id/members/:user_id @@ -167,8 +159,6 @@ Example response: Removes a user from a group or project. -Returns `200` if the request succeeds. - ``` DELETE /groups/:id/members/:user_id DELETE /projects/:id/members/:user_id diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index f4167403c2c..1df661369a4 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -337,9 +337,6 @@ Parameters: } ``` -If the operation is successful, 200 and the newly created merge request is returned. -If an error occurs, an error number and a message explaining the reason is returned. - ## Update MR Updates an existing merge request. You can change the target branch, title, or even close the MR. @@ -414,14 +411,9 @@ Parameters: } ``` -If the operation is successful, 200 and the updated merge request is returned. -If an error occurs, an error number and a message explaining the reason is returned. - ## Delete a merge request Only for admins and project owners. Soft deletes the merge request in question. -If the operation is successful, a status code `200` is returned. In case you cannot -destroy this merge request, or it is not present, code `404` is given. ``` DELETE /projects/:id/merge_requests/:merge_request_id @@ -440,15 +432,14 @@ curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://git Merge changes submitted with MR using this API. -If the merge succeeds you'll get a `200 OK`. -If it has some conflicts and can not be merged - you'll get a 405 and the error message 'Branch cannot be merged' +If it has some conflicts and can not be merged - you'll get a `405` and the error message 'Branch cannot be merged' -If merge request is already merged or closed - you'll get a 406 and the error message 'Method Not Allowed' +If merge request is already merged or closed - you'll get a `406` and the error message 'Method Not Allowed' -If the `sha` parameter is passed and does not match the HEAD of the source - you'll get a 409 and the error message 'SHA does not match HEAD of source branch' +If the `sha` parameter is passed and does not match the HEAD of the source - you'll get a `409` and the error message 'SHA does not match HEAD of source branch' -If you don't have permissions to accept this merge request - you'll get a 401 +If you don't have permissions to accept this merge request - you'll get a `401` ``` PUT /projects/:id/merge_requests/:merge_request_id/merge @@ -520,13 +511,11 @@ Parameters: ## Cancel Merge When Build Succeeds -If successful you'll get `200 OK`. +If you don't have permissions to accept this merge request - you'll get a `401` -If you don't have permissions to accept this merge request - you'll get a 401 +If the merge request is already merged or closed - you get `405` and error message 'Method Not Allowed' -If the merge request is already merged or closed - you get 405 and error message 'Method Not Allowed' - -In case the merge request is not set to be merged when the build succeeds, you'll also get a 406 error. +In case the merge request is not set to be merged when the build succeeds, you'll also get a `406` error. ``` PUT /projects/:id/merge_requests/:merge_request_id/cancel_merge_when_build_succeeds ``` @@ -670,11 +659,8 @@ Example response when an external issue tracker (e.g. JIRA) is used: ## Subscribe to a merge request -Subscribes the authenticated user to a merge request to receive notification. If -the operation is successful, status code `201` together with the updated merge -request is returned. If the user is already subscribed to the merge request, the -status code `304` is returned. If the project or merge request is not found, -status code `404` is returned. +Subscribes the authenticated user to a merge request to receive notification. If the user is already subscribed to the merge request, the +status code `304` is returned. ``` POST /projects/:id/merge_requests/:merge_request_id/subscription @@ -747,10 +733,8 @@ Example response: ## Unsubscribe from a merge request Unsubscribes the authenticated user from a merge request to not receive -notifications from that merge request. If the operation is successful, status -code `200` together with the updated merge request is returned. If the user is -not subscribed to the merge request, the status code `304` is returned. If the -project or merge request is not found, status code `404` is returned. +notifications from that merge request. If the user is +not subscribed to the merge request, the status code `304` is returned. ``` DELETE /projects/:id/merge_requests/:merge_request_id/subscription @@ -822,9 +806,8 @@ Example response: ## Create a todo -Manually creates a todo for the current user on a merge request. If the -request is successful, status code `200` together with the created todo is -returned. If there already exists a todo for the user on that merge request, +Manually creates a todo for the current user on a merge request. +If there already exists a todo for the user on that merge request, status code `304` is returned. ``` diff --git a/doc/api/notes.md b/doc/api/notes.md index 58d40eecf3e..ba20a885fc8 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -109,8 +109,7 @@ Parameters: ### Delete an issue note -Deletes an existing note of an issue. On success, this API method returns 200 -and the deleted note. If the note does not exist, the API returns 404. +Deletes an existing note of an issue. ``` DELETE /projects/:id/issues/:issue_id/notes/:note_id @@ -234,8 +233,7 @@ Parameters: ### Delete a snippet note -Deletes an existing note of a snippet. On success, this API method returns 200 -and the deleted note. If the note does not exist, the API returns 404. +Deletes an existing note of a snippet. ``` DELETE /projects/:id/snippets/:snippet_id/notes/:note_id @@ -364,8 +362,7 @@ Parameters: ### Delete a merge request note -Deletes an existing note of a merge request. On success, this API method returns -200 and the deleted note. If the note does not exist, the API returns 404. +Deletes an existing note of a merge request. ``` DELETE /projects/:id/merge_requests/:merge_request_id/notes/:note_id diff --git a/doc/api/projects.md b/doc/api/projects.md index 467a880ac13..4993448ff25 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -644,7 +644,7 @@ Parameters: ### Edit project -Updates an existing project +Updates an existing project. ``` PUT /projects/:id @@ -674,9 +674,6 @@ Parameters: | `lfs_enabled` | boolean | no | Enable LFS | | `request_access_enabled` | boolean | no | Allow users to request member access | -On success, method returns 200 with the updated project. If parameters are -invalid, 400 is returned. - ### Fork project Forks a project into the user namespace of the authenticated user or the one provided. @@ -694,8 +691,7 @@ Parameters: ### Star a project -Stars a given project. Returns status code `201` and the project on success and -`304` if the project is already starred. +Stars a given project. Returns status code `304` if the project is already starred. ``` POST /projects/:id/star @@ -765,8 +761,7 @@ Example response: ### Unstar a project -Unstars a given project. Returns status code `200` and the project on success -and `304` if the project is not starred. +Unstars a given project. Returns status code `304` if the project is not starred. ``` DELETE /projects/:id/star @@ -837,10 +832,6 @@ Example response: Archives the project if the user is either admin or the project owner of this project. This action is idempotent, thus archiving an already archived project will not change the project. -Status code 201 with the project as body is given when successful, in case the user doesn't -have the proper access rights, code 403 is returned. Status 404 is returned if the project -doesn't exist, or is hidden to the user. - ``` POST /projects/:id/archive ``` @@ -926,10 +917,6 @@ Example response: Unarchives the project if the user is either admin or the project owner of this project. This action is idempotent, thus unarchiving an non-archived project will not change the project. -Status code 201 with the project as body is given when successful, in case the user doesn't -have the proper access rights, code 403 is returned. Status 404 is returned if the project -doesn't exist, or is hidden to the user. - ``` POST /projects/:id/unarchive ``` diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md index efd23d514bc..3fb8b73be6d 100644 --- a/doc/api/system_hooks.md +++ b/doc/api/system_hooks.md @@ -108,8 +108,7 @@ Example response: ## Delete system hook -Deletes a system hook. It returns `200 OK` if the hooks is deleted and -`404 Not Found` if the hook is not found. +Deletes a system hook. --- diff --git a/doc/api/tags.md b/doc/api/tags.md index 398b080e3f6..14573d48fe4 100644 --- a/doc/api/tags.md +++ b/doc/api/tags.md @@ -40,9 +40,7 @@ Parameters: ## Get a single repository tag -Get a specific repository tag determined by its name. It returns `200` together -with the tag information if the tag exists. It returns `404` if the tag does not -exist. +Get a specific repository tag determined by its name. ``` GET /projects/:id/repository/tags/:tag_name @@ -124,14 +122,12 @@ Parameters: The message will be `nil` when creating a lightweight tag otherwise it will contain the annotation. -It returns 201 if the operation succeed. In case of an error, -405 with an explaining error message is returned. +In case of an error, +status code `405` with an explaining error message is returned. ## Delete a tag -Deletes a tag of a repository with given name. On success, this API method -returns 200 with the name of the deleted tag. If the tag does not exist, the -API returns 404. +Deletes a tag of a repository with given name. ``` DELETE /projects/:id/repository/tags/:tag_name @@ -150,9 +146,8 @@ Parameters: ## Create a new release -Add release notes to the existing git tag. It returns 201 if the release is -created successfully. If the tag does not exist, 404 is returned. If there -already exists a release for the given tag, 409 is returned. +Add release notes to the existing git tag. If there +already exists a release for the given tag, status code `409` is returned. ``` POST /projects/:id/repository/tags/:tag_name/release @@ -173,9 +168,7 @@ Parameters: ## Update a release -Updates the release notes of a given release. It returns 200 if the release is -successfully updated. If the tag or the release does not exist, it returns 404 -with a proper error message. +Updates the release notes of a given release. ``` PUT /projects/:id/repository/tags/:tag_name/release diff --git a/doc/api/users.md b/doc/api/users.md index b38c335490a..52a6b691610 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -271,8 +271,8 @@ Parameters: - `can_create_group` (optional) - User can create groups - true or false - `external` (optional) - Flags the user as external - true or false(default) -Note, at the moment this method does only return a 404 error, -even in cases where a 409 (Conflict) would be more appropriate, +Note, at the moment this method does only return a `404` error, +even in cases where a `409` (Conflict) would be more appropriate, e.g. when renaming the email address to some existing one. ## User deletion @@ -449,8 +449,6 @@ Parameters: - `title` (required) - new SSH Key's title - `key` (required) - new SSH key -Will return created key with status `201 Created` on success, or `404 Not found` on fail. - ## Delete SSH key for current user Deletes key owned by currently authenticated user. @@ -581,8 +579,6 @@ Parameters: - `id` (required) - id of specified user - `email` (required) - email address -Will return created email with status `201 Created` on success, or `404 Not found` on fail. - ## Delete email for current user Deletes email owned by currently authenticated user. -- cgit v1.2.1 From 60cd47bc8ede7411cc2eea7c1fb72dd290eb679a Mon Sep 17 00:00:00 2001 From: Luis Alonso Chavez Armendariz <lchavez@nearsoft.com> Date: Fri, 25 Nov 2016 00:29:26 -0700 Subject: Fix bad selection on dropdown menu for tags filter --- app/views/projects/tags/index.html.haml | 7 ++++--- changelogs/unreleased/issue_24958.yml | 4 ++++ 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/issue_24958.yml diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index 7a0d9dcc94f..b43b13de4ca 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -10,15 +10,16 @@ .nav-controls = form_tag(filter_tags_path, method: :get) do = search_field_tag :search, params[:search], { placeholder: 'Filter by tag name', id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false } + .dropdown.inline %button.dropdown-toggle.btn{ type: 'button', data: { toggle: 'dropdown'} } %span.light - = @sort.humanize + = projects_sort_options_hash[@sort] = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right %li - = link_to filter_tags_path(sort: nil) do - Name + = link_to filter_tags_path(sort: sort_value_name) do + = sort_title_name = link_to filter_tags_path(sort: sort_value_recently_updated) do = sort_title_recently_updated = link_to filter_tags_path(sort: sort_value_oldest_updated) do diff --git a/changelogs/unreleased/issue_24958.yml b/changelogs/unreleased/issue_24958.yml new file mode 100644 index 00000000000..dbbbbf9d28d --- /dev/null +++ b/changelogs/unreleased/issue_24958.yml @@ -0,0 +1,4 @@ +--- +title: Fix bad selection on dropdown menu for tags filter +merge_request: +author: Luis Alonso Chavez Armendariz -- cgit v1.2.1 From fda61998bb18e493936cf1c6dfb368bbba9f9424 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Fri, 25 Nov 2016 10:10:55 +0100 Subject: Update pipeline processing specs for creating builds --- spec/services/ci/process_pipeline_service_spec.rb | 63 ++++++++++------------- 1 file changed, 28 insertions(+), 35 deletions(-) diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index ff113efd916..f8b7e7488c2 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -327,62 +327,55 @@ describe Ci::ProcessPipelineService, services: true do end end - context 'creates a builds from .gitlab-ci.yml' do - let(:config) do - YAML.dump({ - rspec: { - stage: 'test', - script: 'rspec' - }, - rubocop: { - stage: 'test', - script: 'rubocop' - }, - deploy: { - stage: 'deploy', - script: 'deploy' - } - }) - end - - # Using stubbed .gitlab-ci.yml created in commit factory - # - + context 'when there are builds in multiple stages' do before do - stub_ci_pipeline_yaml_file(config) create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage: 'build', stage_idx: 0) create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage: 'build', stage_idx: 0) + create(:ci_build, :created, pipeline: pipeline, name: 'rspec', stage: 'test', stage_idx: 1) + create(:ci_build, :created, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 1) + create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 2) end - it 'when processing a pipeline' do - # Currently we have two builds with state created + it 'processes the pipeline' do + # Currently we have five builds with state created + # expect(builds.count).to eq(0) - expect(all_builds.count).to eq(2) + expect(all_builds.count).to eq(5) + + # Process builds will mark the created as pending + # + process_pipeline - # Create builds will mark the created as pending - expect(process_pipeline).to be_truthy expect(builds.count).to eq(2) - expect(all_builds.count).to eq(2) + expect(all_builds.count).to eq(5) - # When we builds succeed we will create a rest of pipeline from .gitlab-ci.yml - # We will have 2 succeeded, 2 pending (from stage test), total 5 (one more build from deploy) + # When builds succeed we will enqueue remaining builds + # We will have 2 succeeded, 2 pending (from stage test), + # total 5 (one more build from deploy) + # succeed_pending - expect(process_pipeline).to be_truthy + process_pipeline + expect(builds.success.count).to eq(2) expect(builds.pending.count).to eq(2) expect(all_builds.count).to eq(5) # When we succeed the 2 pending from stage test, - # We will queue a deploy stage, no new builds will be created + # We will queue a deploy stage. + # succeed_pending - expect(process_pipeline).to be_truthy + process_pipeline + expect(builds.pending.count).to eq(1) expect(builds.success.count).to eq(4) expect(all_builds.count).to eq(5) - # When we succeed last pending build, we will have a total of 5 succeeded builds, no new builds will be created + # When we succeed last pending build, we will have + # a total of 5 succeeded builds + # succeed_pending - expect(process_pipeline).to be_falsey + process_pipeline + expect(builds.success.count).to eq(5) expect(all_builds.count).to eq(5) end -- cgit v1.2.1 From c1cc252bbd9f47ab8c611df37098ad25833402ea Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Fri, 25 Nov 2016 10:44:13 +0100 Subject: Move helpers to the end of process pipeline specs --- spec/services/ci/process_pipeline_service_spec.rb | 58 +++++++++++------------ 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index f8b7e7488c2..544c88d024f 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -10,22 +10,6 @@ describe Ci::ProcessPipelineService, services: true do end describe '#execute' do - def all_builds - pipeline.builds - end - - def builds - all_builds.where.not(status: [:created, :skipped]) - end - - def process_pipeline - described_class.new(pipeline.project, user).execute(pipeline) - end - - def succeed_pending - builds.pending.update_all(status: 'success') - end - context 'start queuing next builds' do before do create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage_idx: 0) @@ -223,10 +207,6 @@ describe Ci::ProcessPipelineService, services: true do pipeline.builds.running_or_pending.each(&:success) expect(manual_actions).to be_many # production and clear cache end - - def manual_actions - pipeline.manual_actions - end end end @@ -282,15 +262,6 @@ describe Ci::ProcessPipelineService, services: true do expect(builds.map(&:status)).to eq(%w[success skipped pending]) end end - - def create_build(name, stage_idx, when_value = nil) - create(:ci_build, - :created, - pipeline: pipeline, - name: name, - stage_idx: stage_idx, - when: when_value) - end end context 'when failed build in the middle stage is retried' do @@ -381,4 +352,33 @@ describe Ci::ProcessPipelineService, services: true do end end end + + def all_builds + pipeline.builds + end + + def builds + all_builds.where.not(status: [:created, :skipped]) + end + + def process_pipeline + described_class.new(pipeline.project, user).execute(pipeline) + end + + def succeed_pending + builds.pending.update_all(status: 'success') + end + + def manual_actions + pipeline.manual_actions + end + + def create_build(name, stage_idx, when_value = nil) + create(:ci_build, + :created, + pipeline: pipeline, + name: name, + stage_idx: stage_idx, + when: when_value) + end end -- cgit v1.2.1 From 5131f8dedf3584597f417b936d3397b54e56c2e1 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Fri, 25 Nov 2016 10:45:36 +0100 Subject: Remove remaining calls to CI yaml in pipeline specs --- spec/services/ci/process_pipeline_service_spec.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index 544c88d024f..b130f876259 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -3,11 +3,6 @@ require 'spec_helper' describe Ci::ProcessPipelineService, services: true do let(:pipeline) { create(:ci_pipeline, ref: 'master') } let(:user) { create(:user) } - let(:config) { nil } - - before do - allow(pipeline).to receive(:ci_yaml_file).and_return(config) - end describe '#execute' do context 'start queuing next builds' do -- cgit v1.2.1 From 94100d4e722ad61fec3e8ab3f9889ffabba1100f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Fri, 25 Nov 2016 10:57:03 +0100 Subject: Add Changelog entry for pipeline creation improvements --- .../unreleased/fix-create-pipeline-with-builds-in-transaction.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-create-pipeline-with-builds-in-transaction.yml diff --git a/changelogs/unreleased/fix-create-pipeline-with-builds-in-transaction.yml b/changelogs/unreleased/fix-create-pipeline-with-builds-in-transaction.yml new file mode 100644 index 00000000000..e37841e80c3 --- /dev/null +++ b/changelogs/unreleased/fix-create-pipeline-with-builds-in-transaction.yml @@ -0,0 +1,4 @@ +--- +title: Create builds in transaction to avoid empty pipelines +merge_request: 7742 +author: -- cgit v1.2.1 From 0bf14cb0b510cd1c74b0ef94c109d519983fddd0 Mon Sep 17 00:00:00 2001 From: winniehell <git@winniehell.de> Date: Fri, 18 Nov 2016 21:31:11 +0100 Subject: Create dynamic fixture for build_spec (!7589) --- .../create-dynamic-fixture-for-build_spec.yml | 4 ++ spec/javascripts/build_spec.js.es6 | 12 +---- spec/javascripts/fixtures/build.html.haml | 62 ---------------------- spec/javascripts/fixtures/builds.rb | 32 +++++++++++ 4 files changed, 38 insertions(+), 72 deletions(-) create mode 100644 changelogs/unreleased/create-dynamic-fixture-for-build_spec.yml delete mode 100644 spec/javascripts/fixtures/build.html.haml create mode 100644 spec/javascripts/fixtures/builds.rb diff --git a/changelogs/unreleased/create-dynamic-fixture-for-build_spec.yml b/changelogs/unreleased/create-dynamic-fixture-for-build_spec.yml new file mode 100644 index 00000000000..f0d9ff0c34f --- /dev/null +++ b/changelogs/unreleased/create-dynamic-fixture-for-build_spec.yml @@ -0,0 +1,4 @@ +--- +title: Create dynamic fixture for build_spec +merge_request: 7589 +author: winniehell diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6 index ee192c4f18a..1a56819724d 100644 --- a/spec/javascripts/build_spec.js.es6 +++ b/spec/javascripts/build_spec.js.es6 @@ -11,10 +11,10 @@ (() => { describe('Build', () => { - fixture.preload('build.html'); + fixture.preload('builds/build-with-artifacts.html.raw'); beforeEach(function () { - fixture.load('build.html'); + fixture.load('builds/build-with-artifacts.html.raw'); spyOn($, 'ajax'); }); @@ -28,15 +28,7 @@ }); describe('setup', function () { - const removeDate = new Date(); - removeDate.setUTCFullYear(removeDate.getUTCFullYear() + 1); - // give the test three days to run - removeDate.setTime(removeDate.getTime() + (3 * 24 * 60 * 60 * 1000)); - beforeEach(function () { - const removeDateElement = document.querySelector('.js-artifacts-remove'); - removeDateElement.innerText = removeDate.toString(); - this.build = new Build(); }); diff --git a/spec/javascripts/fixtures/build.html.haml b/spec/javascripts/fixtures/build.html.haml deleted file mode 100644 index 06b49516e5c..00000000000 --- a/spec/javascripts/fixtures/build.html.haml +++ /dev/null @@ -1,62 +0,0 @@ -.build-page - .prepend-top-default - .autoscroll-container - %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll - #js-build-scroll.scroll-controls - %a.btn{href: '#build-trace'} - %i.fa.fa-angle-up - %a.btn{href: '#down-build-trace'} - %i.fa.fa-angle-down - %pre.build-trace#build-trace - %code.bash.js-build-output - %i.fa.fa-refresh.fa-spin.js-build-refresh - -%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar - .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default - Build - %strong #1 - %a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" } - %i.fa.fa-angle-double-right - .blocks-container - .dropdown.build-dropdown - .title Stage - %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'} - %span.stage-selection More - %i.fa.fa-caret-down - %ul.dropdown-menu - %li - %a.stage-item build - %li - %a.stage-item test - %li - %a.stage-item deploy - .builds-container - .build-job{data: {stage: 'build'}} - %a{href: 'http://example.com/root/test-build/builds/1'} - %i.fa.fa-check - %i.fa.fa-check-circle-o - %span - Setup - .build-job{data: {stage: 'test'}} - %a{href: 'http://example.com/root/test-build/builds/2'} - %i.fa.fa-check - %i.fa.fa-check-circle-o - %span - Tests - .build-job{data: {stage: 'deploy'}} - %a{href: 'http://example.com/root/test-build/builds/3'} - %i.fa.fa-check - %i.fa.fa-check-circle-o - %span - Deploy - -.js-build-options{ data: { page_url: 'http://example.com/root/test-build/builds/2', - build_url: 'http://example.com/root/test-build/builds/2.json', - build_status: 'passed', - build_stage: 'test', - log_state: 'buildstate' }} - -%p.build-detail-row - The artifacts will be removed in - %span.js-artifacts-remove - 2016-12-19 09:02:12 UTC diff --git a/spec/javascripts/fixtures/builds.rb b/spec/javascripts/fixtures/builds.rb new file mode 100644 index 00000000000..e47698f71ed --- /dev/null +++ b/spec/javascripts/fixtures/builds.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Projects::BuildsController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:project) { create(:project_empty_repo) } + let(:pipeline) { create(:ci_empty_pipeline, project: project) } + let!(:build_with_artifacts) { create(:ci_build, :success, :artifacts, :trace, pipeline: pipeline, stage: 'test', artifacts_expire_at: Time.now + 18.months) } + let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline, stage: 'build') } + let!(:pending_build) { create(:ci_build, :pending, pipeline: pipeline, stage: 'deploy') } + + render_views + + before(:all) do + clean_frontend_fixtures('builds/') + end + + before(:each) do + sign_in(admin) + end + + it 'builds/build-with-artifacts.html.raw' do |example| + get :show, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: build_with_artifacts.to_param + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end -- cgit v1.2.1 From aae82d766bd3fcbd85bf3e6da9c910b937ac5e72 Mon Sep 17 00:00:00 2001 From: winniehell <git@winniehell.de> Date: Sat, 19 Nov 2016 02:06:49 +0100 Subject: Adjust build_spec to match fixture --- spec/javascripts/build_spec.js.es6 | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6 index 1a56819724d..d8253c1d0d5 100644 --- a/spec/javascripts/build_spec.js.es6 +++ b/spec/javascripts/build_spec.js.es6 @@ -11,6 +11,13 @@ (() => { describe('Build', () => { + // see spec/factories/ci/builds.rb + const BUILD_TRACE = 'BUILD TRACE'; + // see lib/ci/ansi2html.rb + const INITIAL_BUILD_TRACE_STATE = window.btoa(JSON.stringify({ + offset: BUILD_TRACE.length, n_open_tags: 0, fg_color: null, bg_color: null, style_mask: 0, + })); + fixture.preload('builds/build-with-artifacts.html.raw'); beforeEach(function () { @@ -33,11 +40,11 @@ }); it('copies build options', function () { - expect(this.build.pageUrl).toBe('http://example.com/root/test-build/builds/2'); - expect(this.build.buildUrl).toBe('http://example.com/root/test-build/builds/2.json'); - expect(this.build.buildStatus).toBe('passed'); + expect(this.build.pageUrl).toBe('http://test.host/namespace1/project1/builds/1'); + expect(this.build.buildUrl).toBe('http://test.host/namespace1/project1/builds/1.json'); + expect(this.build.buildStatus).toBe('success'); expect(this.build.buildStage).toBe('test'); - expect(this.build.state).toBe('buildstate'); + expect(this.build.state).toBe(INITIAL_BUILD_TRACE_STATE); }); it('only shows the jobs matching the current stage', function () { @@ -73,7 +80,7 @@ it('displays the initial build trace', function () { expect($.ajax.calls.count()).toBe(1); const [{ url, dataType, success, context }] = $.ajax.calls.argsFor(0); - expect(url).toBe('http://example.com/root/test-build/builds/2.json'); + expect(url).toBe('http://test.host/namespace1/project1/builds/1.json'); expect(dataType).toBe('json'); expect(success).toEqual(jasmine.any(Function)); @@ -95,7 +102,7 @@ $('.js-build-options').data('buildStatus', 'running'); this.build = new Build(); spyOn(this.build, 'location') - .and.returnValue('http://example.com/root/test-build/builds/2'); + .and.returnValue('http://test.host/namespace1/project1/builds/1'); }); it('updates the build trace on an interval', function () { @@ -104,7 +111,7 @@ expect($.ajax.calls.count()).toBe(2); let [{ url, dataType, success, context }] = $.ajax.calls.argsFor(1); expect(url).toBe( - 'http://example.com/root/test-build/builds/2/trace.json?state=buildstate' + `http://test.host/namespace1/project1/builds/1/trace.json?state=${encodeURIComponent(INITIAL_BUILD_TRACE_STATE)}` ); expect(dataType).toBe('json'); expect(success).toEqual(jasmine.any(Function)); @@ -124,7 +131,7 @@ expect($.ajax.calls.count()).toBe(3); [{ url, dataType, success, context }] = $.ajax.calls.argsFor(2); expect(url).toBe( - 'http://example.com/root/test-build/builds/2/trace.json?state=newstate' + 'http://test.host/namespace1/project1/builds/1/trace.json?state=newstate' ); expect(dataType).toBe('json'); expect(success).toEqual(jasmine.any(Function)); @@ -175,7 +182,7 @@ }); expect(Turbolinks.visit).toHaveBeenCalledWith( - 'http://example.com/root/test-build/builds/2' + 'http://test.host/namespace1/project1/builds/1' ); }); }); -- cgit v1.2.1 From d100f843d784b64b1b73ad8090b855e2ffd985dd Mon Sep 17 00:00:00 2001 From: winniehell <git@winniehell.de> Date: Sat, 19 Nov 2016 22:48:49 +0100 Subject: Remove unnecessary IIFE from build_spec --- spec/javascripts/build_spec.js.es6 | 284 ++++++++++++++++++------------------- 1 file changed, 141 insertions(+), 143 deletions(-) diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6 index d8253c1d0d5..c9a314d2a82 100644 --- a/spec/javascripts/build_spec.js.es6 +++ b/spec/javascripts/build_spec.js.es6 @@ -9,183 +9,181 @@ //= require jquery.nicescroll //= require turbolinks -(() => { - describe('Build', () => { - // see spec/factories/ci/builds.rb - const BUILD_TRACE = 'BUILD TRACE'; - // see lib/ci/ansi2html.rb - const INITIAL_BUILD_TRACE_STATE = window.btoa(JSON.stringify({ - offset: BUILD_TRACE.length, n_open_tags: 0, fg_color: null, bg_color: null, style_mask: 0, - })); - - fixture.preload('builds/build-with-artifacts.html.raw'); +describe('Build', () => { + // see spec/factories/ci/builds.rb + const BUILD_TRACE = 'BUILD TRACE'; + // see lib/ci/ansi2html.rb + const INITIAL_BUILD_TRACE_STATE = window.btoa(JSON.stringify({ + offset: BUILD_TRACE.length, n_open_tags: 0, fg_color: null, bg_color: null, style_mask: 0, + })); + + fixture.preload('builds/build-with-artifacts.html.raw'); + + beforeEach(function () { + fixture.load('builds/build-with-artifacts.html.raw'); + spyOn($, 'ajax'); + }); + describe('constructor', () => { beforeEach(function () { - fixture.load('builds/build-with-artifacts.html.raw'); - spyOn($, 'ajax'); + jasmine.clock().install(); + }); + + afterEach(() => { + jasmine.clock().uninstall(); }); - describe('constructor', () => { + describe('setup', function () { beforeEach(function () { - jasmine.clock().install(); + this.build = new Build(); }); - afterEach(() => { - jasmine.clock().uninstall(); + it('copies build options', function () { + expect(this.build.pageUrl).toBe('http://test.host/namespace1/project1/builds/1'); + expect(this.build.buildUrl).toBe('http://test.host/namespace1/project1/builds/1.json'); + expect(this.build.buildStatus).toBe('success'); + expect(this.build.buildStage).toBe('test'); + expect(this.build.state).toBe(INITIAL_BUILD_TRACE_STATE); }); - describe('setup', function () { - beforeEach(function () { - this.build = new Build(); - }); + it('only shows the jobs matching the current stage', function () { + expect($('.build-job[data-stage="build"]').is(':visible')).toBe(false); + expect($('.build-job[data-stage="test"]').is(':visible')).toBe(true); + expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false); + }); - it('copies build options', function () { - expect(this.build.pageUrl).toBe('http://test.host/namespace1/project1/builds/1'); - expect(this.build.buildUrl).toBe('http://test.host/namespace1/project1/builds/1.json'); - expect(this.build.buildStatus).toBe('success'); - expect(this.build.buildStage).toBe('test'); - expect(this.build.state).toBe(INITIAL_BUILD_TRACE_STATE); - }); + it('selects the current stage in the build dropdown menu', function () { + expect($('.stage-selection').text()).toBe('test'); + }); - it('only shows the jobs matching the current stage', function () { - expect($('.build-job[data-stage="build"]').is(':visible')).toBe(false); - expect($('.build-job[data-stage="test"]').is(':visible')).toBe(true); - expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false); - }); + it('updates the jobs when the build dropdown changes', function () { + $('.stage-item:contains("build")').click(); - it('selects the current stage in the build dropdown menu', function () { - expect($('.stage-selection').text()).toBe('test'); - }); + expect($('.stage-selection').text()).toBe('build'); + expect($('.build-job[data-stage="build"]').is(':visible')).toBe(true); + expect($('.build-job[data-stage="test"]').is(':visible')).toBe(false); + expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false); + }); - it('updates the jobs when the build dropdown changes', function () { - $('.stage-item:contains("build")').click(); + it('displays the remove date correctly', function () { + const removeDateElement = document.querySelector('.js-artifacts-remove'); + expect(removeDateElement.innerText.trim()).toBe('1 year'); + }); + }); - expect($('.stage-selection').text()).toBe('build'); - expect($('.build-job[data-stage="build"]').is(':visible')).toBe(true); - expect($('.build-job[data-stage="test"]').is(':visible')).toBe(false); - expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false); - }); + describe('initial build trace', function () { + beforeEach(function () { + new Build(); + }); - it('displays the remove date correctly', function () { - const removeDateElement = document.querySelector('.js-artifacts-remove'); - expect(removeDateElement.innerText.trim()).toBe('1 year'); - }); + it('displays the initial build trace', function () { + expect($.ajax.calls.count()).toBe(1); + const [{ url, dataType, success, context }] = $.ajax.calls.argsFor(0); + expect(url).toBe('http://test.host/namespace1/project1/builds/1.json'); + expect(dataType).toBe('json'); + expect(success).toEqual(jasmine.any(Function)); + + success.call(context, { trace_html: '<span>Example</span>', status: 'running' }); + + expect($('#build-trace .js-build-output').text()).toMatch(/Example/); }); - describe('initial build trace', function () { - beforeEach(function () { - new Build(); - }); + it('removes the spinner', function () { + const [{ success, context }] = $.ajax.calls.argsFor(0); + success.call(context, { trace_html: '<span>Example</span>', status: 'success' }); - it('displays the initial build trace', function () { - expect($.ajax.calls.count()).toBe(1); - const [{ url, dataType, success, context }] = $.ajax.calls.argsFor(0); - expect(url).toBe('http://test.host/namespace1/project1/builds/1.json'); - expect(dataType).toBe('json'); - expect(success).toEqual(jasmine.any(Function)); + expect($('.js-build-refresh').length).toBe(0); + }); + }); - success.call(context, { trace_html: '<span>Example</span>', status: 'running' }); + describe('running build', function () { + beforeEach(function () { + $('.js-build-options').data('buildStatus', 'running'); + this.build = new Build(); + spyOn(this.build, 'location') + .and.returnValue('http://test.host/namespace1/project1/builds/1'); + }); - expect($('#build-trace .js-build-output').text()).toMatch(/Example/); + it('updates the build trace on an interval', function () { + jasmine.clock().tick(4001); + + expect($.ajax.calls.count()).toBe(2); + let [{ url, dataType, success, context }] = $.ajax.calls.argsFor(1); + expect(url).toBe( + `http://test.host/namespace1/project1/builds/1/trace.json?state=${encodeURIComponent(INITIAL_BUILD_TRACE_STATE)}` + ); + expect(dataType).toBe('json'); + expect(success).toEqual(jasmine.any(Function)); + + success.call(context, { + html: '<span>Update<span>', + status: 'running', + state: 'newstate', + append: true, }); - it('removes the spinner', function () { - const [{ success, context }] = $.ajax.calls.argsFor(0); - success.call(context, { trace_html: '<span>Example</span>', status: 'success' }); + expect($('#build-trace .js-build-output').text()).toMatch(/Update/); + expect(this.build.state).toBe('newstate'); + + jasmine.clock().tick(4001); - expect($('.js-build-refresh').length).toBe(0); + expect($.ajax.calls.count()).toBe(3); + [{ url, dataType, success, context }] = $.ajax.calls.argsFor(2); + expect(url).toBe( + 'http://test.host/namespace1/project1/builds/1/trace.json?state=newstate' + ); + expect(dataType).toBe('json'); + expect(success).toEqual(jasmine.any(Function)); + + success.call(context, { + html: '<span>More</span>', + status: 'running', + state: 'finalstate', + append: true, }); + + expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/); + expect(this.build.state).toBe('finalstate'); }); - describe('running build', function () { - beforeEach(function () { - $('.js-build-options').data('buildStatus', 'running'); - this.build = new Build(); - spyOn(this.build, 'location') - .and.returnValue('http://test.host/namespace1/project1/builds/1'); + it('replaces the entire build trace', function () { + jasmine.clock().tick(4001); + let [{ success, context }] = $.ajax.calls.argsFor(1); + success.call(context, { + html: '<span>Update</span>', + status: 'running', + append: true, }); - it('updates the build trace on an interval', function () { - jasmine.clock().tick(4001); - - expect($.ajax.calls.count()).toBe(2); - let [{ url, dataType, success, context }] = $.ajax.calls.argsFor(1); - expect(url).toBe( - `http://test.host/namespace1/project1/builds/1/trace.json?state=${encodeURIComponent(INITIAL_BUILD_TRACE_STATE)}` - ); - expect(dataType).toBe('json'); - expect(success).toEqual(jasmine.any(Function)); - - success.call(context, { - html: '<span>Update<span>', - status: 'running', - state: 'newstate', - append: true, - }); - - expect($('#build-trace .js-build-output').text()).toMatch(/Update/); - expect(this.build.state).toBe('newstate'); - - jasmine.clock().tick(4001); - - expect($.ajax.calls.count()).toBe(3); - [{ url, dataType, success, context }] = $.ajax.calls.argsFor(2); - expect(url).toBe( - 'http://test.host/namespace1/project1/builds/1/trace.json?state=newstate' - ); - expect(dataType).toBe('json'); - expect(success).toEqual(jasmine.any(Function)); - - success.call(context, { - html: '<span>More</span>', - status: 'running', - state: 'finalstate', - append: true, - }); - - expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/); - expect(this.build.state).toBe('finalstate'); - }); + expect($('#build-trace .js-build-output').text()).toMatch(/Update/); - it('replaces the entire build trace', function () { - jasmine.clock().tick(4001); - let [{ success, context }] = $.ajax.calls.argsFor(1); - success.call(context, { - html: '<span>Update</span>', - status: 'running', - append: true, - }); - - expect($('#build-trace .js-build-output').text()).toMatch(/Update/); - - jasmine.clock().tick(4001); - [{ success, context }] = $.ajax.calls.argsFor(2); - success.call(context, { - html: '<span>Different</span>', - status: 'running', - append: false, - }); - - expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/); - expect($('#build-trace .js-build-output').text()).toMatch(/Different/); + jasmine.clock().tick(4001); + [{ success, context }] = $.ajax.calls.argsFor(2); + success.call(context, { + html: '<span>Different</span>', + status: 'running', + append: false, }); - it('reloads the page when the build is done', function () { - spyOn(Turbolinks, 'visit'); + expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/); + expect($('#build-trace .js-build-output').text()).toMatch(/Different/); + }); - jasmine.clock().tick(4001); - const [{ success, context }] = $.ajax.calls.argsFor(1); - success.call(context, { - html: '<span>Final</span>', - status: 'passed', - append: true, - }); + it('reloads the page when the build is done', function () { + spyOn(Turbolinks, 'visit'); - expect(Turbolinks.visit).toHaveBeenCalledWith( - 'http://test.host/namespace1/project1/builds/1' - ); + jasmine.clock().tick(4001); + const [{ success, context }] = $.ajax.calls.argsFor(1); + success.call(context, { + html: '<span>Final</span>', + status: 'passed', + append: true, }); + + expect(Turbolinks.visit).toHaveBeenCalledWith( + 'http://test.host/namespace1/project1/builds/1' + ); }); }); }); -})(); +}); -- cgit v1.2.1 From 918bc207c680bed46e82bef996aea027ac72b38d Mon Sep 17 00:00:00 2001 From: winniehell <git@winniehell.de> Date: Sat, 19 Nov 2016 22:59:32 +0100 Subject: Use Rails test host name for frontend fixtures --- spec/javascripts/build_spec.js.es6 | 20 ++++++++------------ spec/javascripts/issue_spec.js | 2 +- spec/javascripts/spec_helper.js | 5 +++++ spec/support/javascript_fixtures_helpers.rb | 4 +++- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6 index c9a314d2a82..50411695925 100644 --- a/spec/javascripts/build_spec.js.es6 +++ b/spec/javascripts/build_spec.js.es6 @@ -10,6 +10,7 @@ //= require turbolinks describe('Build', () => { + const BUILD_URL = `${gl.TEST_HOST}/namespace1/project1/builds/1`; // see spec/factories/ci/builds.rb const BUILD_TRACE = 'BUILD TRACE'; // see lib/ci/ansi2html.rb @@ -39,8 +40,8 @@ describe('Build', () => { }); it('copies build options', function () { - expect(this.build.pageUrl).toBe('http://test.host/namespace1/project1/builds/1'); - expect(this.build.buildUrl).toBe('http://test.host/namespace1/project1/builds/1.json'); + expect(this.build.pageUrl).toBe(BUILD_URL); + expect(this.build.buildUrl).toBe(`${BUILD_URL}.json`); expect(this.build.buildStatus).toBe('success'); expect(this.build.buildStage).toBe('test'); expect(this.build.state).toBe(INITIAL_BUILD_TRACE_STATE); @@ -79,7 +80,7 @@ describe('Build', () => { it('displays the initial build trace', function () { expect($.ajax.calls.count()).toBe(1); const [{ url, dataType, success, context }] = $.ajax.calls.argsFor(0); - expect(url).toBe('http://test.host/namespace1/project1/builds/1.json'); + expect(url).toBe(`${BUILD_URL}.json`); expect(dataType).toBe('json'); expect(success).toEqual(jasmine.any(Function)); @@ -100,8 +101,7 @@ describe('Build', () => { beforeEach(function () { $('.js-build-options').data('buildStatus', 'running'); this.build = new Build(); - spyOn(this.build, 'location') - .and.returnValue('http://test.host/namespace1/project1/builds/1'); + spyOn(this.build, 'location').and.returnValue(BUILD_URL); }); it('updates the build trace on an interval', function () { @@ -110,7 +110,7 @@ describe('Build', () => { expect($.ajax.calls.count()).toBe(2); let [{ url, dataType, success, context }] = $.ajax.calls.argsFor(1); expect(url).toBe( - `http://test.host/namespace1/project1/builds/1/trace.json?state=${encodeURIComponent(INITIAL_BUILD_TRACE_STATE)}` + `${BUILD_URL}/trace.json?state=${encodeURIComponent(INITIAL_BUILD_TRACE_STATE)}` ); expect(dataType).toBe('json'); expect(success).toEqual(jasmine.any(Function)); @@ -129,9 +129,7 @@ describe('Build', () => { expect($.ajax.calls.count()).toBe(3); [{ url, dataType, success, context }] = $.ajax.calls.argsFor(2); - expect(url).toBe( - 'http://test.host/namespace1/project1/builds/1/trace.json?state=newstate' - ); + expect(url).toBe(`${BUILD_URL}/trace.json?state=newstate`); expect(dataType).toBe('json'); expect(success).toEqual(jasmine.any(Function)); @@ -180,9 +178,7 @@ describe('Build', () => { append: true, }); - expect(Turbolinks.visit).toHaveBeenCalledWith( - 'http://test.host/namespace1/project1/builds/1' - ); + expect(Turbolinks.visit).toHaveBeenCalledWith(BUILD_URL); }); }); }); diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index beef46122ab..ab92bdf01fd 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -74,7 +74,7 @@ it('submits an ajax request on tasklist:changed', function() { spyOn(jQuery, 'ajax').and.callFake(function(req) { expect(req.type).toBe('PATCH'); - expect(req.url).toBe('https://fixture.invalid/namespace3/project3/issues/1.json'); + expect(req.url).toBe(gl.TEST_HOST + '/namespace3/project3/issues/1.json'); expect(req.data.issue.description).not.toBe(null); }); diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js index 8a64de4dd43..831dfada952 100644 --- a/spec/javascripts/spec_helper.js +++ b/spec/javascripts/spec_helper.js @@ -41,3 +41,8 @@ }).call(this); + +// defined in ActionDispatch::TestRequest +// see https://github.com/rails/rails/blob/v4.2.7.1/actionpack/lib/action_dispatch/testing/test_request.rb#L7 +window.gl = window.gl || {}; +gl.TEST_HOST = 'http://test.host'; diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/javascript_fixtures_helpers.rb index adc3f48b434..7e066aa115d 100644 --- a/spec/support/javascript_fixtures_helpers.rb +++ b/spec/support/javascript_fixtures_helpers.rb @@ -1,3 +1,4 @@ +require 'action_dispatch/testing/test_request' require 'fileutils' require 'gitlab/popen' @@ -36,7 +37,8 @@ module JavaScriptFixturesHelpers fixture = doc.to_html # replace relative links - fixture.gsub!(%r{="/}, '="https://fixture.invalid/') + test_host = ActionDispatch::TestRequest::DEFAULT_ENV['HTTP_HOST'] + fixture.gsub!(%r{="/}, "=\"http://#{test_host}/") end FileUtils.mkdir_p(File.dirname(fixture_file_name)) -- cgit v1.2.1 From 82429b6978361daaf703a3438c7822ac956a3aa4 Mon Sep 17 00:00:00 2001 From: winniehell <git@winniehell.de> Date: Sun, 20 Nov 2016 00:40:37 +0100 Subject: Explicitly name namespace and projects for frontend fixtures --- spec/javascripts/build_spec.js.es6 | 2 +- spec/javascripts/fixtures/builds.rb | 3 ++- spec/javascripts/fixtures/issues.rb | 3 ++- spec/javascripts/issue_spec.js | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6 index 50411695925..304c4d4e29d 100644 --- a/spec/javascripts/build_spec.js.es6 +++ b/spec/javascripts/build_spec.js.es6 @@ -10,7 +10,7 @@ //= require turbolinks describe('Build', () => { - const BUILD_URL = `${gl.TEST_HOST}/namespace1/project1/builds/1`; + const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`; // see spec/factories/ci/builds.rb const BUILD_TRACE = 'BUILD TRACE'; // see lib/ci/ansi2html.rb diff --git a/spec/javascripts/fixtures/builds.rb b/spec/javascripts/fixtures/builds.rb index e47698f71ed..978e25a1c32 100644 --- a/spec/javascripts/fixtures/builds.rb +++ b/spec/javascripts/fixtures/builds.rb @@ -4,7 +4,8 @@ describe Projects::BuildsController, '(JavaScript fixtures)', type: :controller include JavaScriptFixturesHelpers let(:admin) { create(:admin) } - let(:project) { create(:project_empty_repo) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project_empty_repo, namespace: namespace, path: 'builds-project') } let(:pipeline) { create(:ci_empty_pipeline, project: project) } let!(:build_with_artifacts) { create(:ci_build, :success, :artifacts, :trace, pipeline: pipeline, stage: 'test', artifacts_expire_at: Time.now + 18.months) } let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline, stage: 'build') } diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb index d95eb851421..c10784fe5ae 100644 --- a/spec/javascripts/fixtures/issues.rb +++ b/spec/javascripts/fixtures/issues.rb @@ -4,7 +4,8 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller include JavaScriptFixturesHelpers let(:admin) { create(:admin) } - let(:project) { create(:project_empty_repo) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project_empty_repo, namespace: namespace, path: 'issues-project') } render_views diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index ab92bdf01fd..14af6644de1 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -74,7 +74,7 @@ it('submits an ajax request on tasklist:changed', function() { spyOn(jQuery, 'ajax').and.callFake(function(req) { expect(req.type).toBe('PATCH'); - expect(req.url).toBe(gl.TEST_HOST + '/namespace3/project3/issues/1.json'); + expect(req.url).toBe(gl.TEST_HOST + '/frontend-fixtures/issues-project/issues/1.json'); // eslint-disable-line prefer-template expect(req.data.issue.description).not.toBe(null); }); -- cgit v1.2.1 From d9fe5c259d8880fcb9f8c1cc3322836d4d35ef93 Mon Sep 17 00:00:00 2001 From: winniehell <git@winniehell.de> Date: Wed, 23 Nov 2016 21:36:34 +0100 Subject: Strip <link> tags from fixtures to ignore CSS --- spec/support/javascript_fixtures_helpers.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/javascript_fixtures_helpers.rb index 7e066aa115d..99e98eebdb4 100644 --- a/spec/support/javascript_fixtures_helpers.rb +++ b/spec/support/javascript_fixtures_helpers.rb @@ -31,6 +31,9 @@ module JavaScriptFixturesHelpers if response_mime_type.html? doc = Nokogiri::HTML::DocumentFragment.parse(fixture) + link_tags = doc.css('link') + link_tags.remove + scripts = doc.css('script') scripts.remove -- cgit v1.2.1 From 31a5ed97a74e250887721413f5a956a93dc2e1b1 Mon Sep 17 00:00:00 2001 From: winniehell <git@winniehell.de> Date: Wed, 23 Nov 2016 21:45:39 +0100 Subject: Prefer arrow functions in build_spec.js.es6 --- spec/javascripts/build_spec.js.es6 | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6 index 304c4d4e29d..d92cc433670 100644 --- a/spec/javascripts/build_spec.js.es6 +++ b/spec/javascripts/build_spec.js.es6 @@ -20,13 +20,13 @@ describe('Build', () => { fixture.preload('builds/build-with-artifacts.html.raw'); - beforeEach(function () { + beforeEach(() => { fixture.load('builds/build-with-artifacts.html.raw'); spyOn($, 'ajax'); }); describe('constructor', () => { - beforeEach(function () { + beforeEach(() => { jasmine.clock().install(); }); @@ -34,7 +34,7 @@ describe('Build', () => { jasmine.clock().uninstall(); }); - describe('setup', function () { + describe('setup', () => { beforeEach(function () { this.build = new Build(); }); @@ -47,17 +47,17 @@ describe('Build', () => { expect(this.build.state).toBe(INITIAL_BUILD_TRACE_STATE); }); - it('only shows the jobs matching the current stage', function () { + it('only shows the jobs matching the current stage', () => { expect($('.build-job[data-stage="build"]').is(':visible')).toBe(false); expect($('.build-job[data-stage="test"]').is(':visible')).toBe(true); expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false); }); - it('selects the current stage in the build dropdown menu', function () { + it('selects the current stage in the build dropdown menu', () => { expect($('.stage-selection').text()).toBe('test'); }); - it('updates the jobs when the build dropdown changes', function () { + it('updates the jobs when the build dropdown changes', () => { $('.stage-item:contains("build")').click(); expect($('.stage-selection').text()).toBe('build'); @@ -66,18 +66,18 @@ describe('Build', () => { expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false); }); - it('displays the remove date correctly', function () { + it('displays the remove date correctly', () => { const removeDateElement = document.querySelector('.js-artifacts-remove'); expect(removeDateElement.innerText.trim()).toBe('1 year'); }); }); - describe('initial build trace', function () { - beforeEach(function () { + describe('initial build trace', () => { + beforeEach(() => { new Build(); }); - it('displays the initial build trace', function () { + it('displays the initial build trace', () => { expect($.ajax.calls.count()).toBe(1); const [{ url, dataType, success, context }] = $.ajax.calls.argsFor(0); expect(url).toBe(`${BUILD_URL}.json`); @@ -89,7 +89,7 @@ describe('Build', () => { expect($('#build-trace .js-build-output').text()).toMatch(/Example/); }); - it('removes the spinner', function () { + it('removes the spinner', () => { const [{ success, context }] = $.ajax.calls.argsFor(0); success.call(context, { trace_html: '<span>Example</span>', status: 'success' }); @@ -97,7 +97,7 @@ describe('Build', () => { }); }); - describe('running build', function () { + describe('running build', () => { beforeEach(function () { $('.js-build-options').data('buildStatus', 'running'); this.build = new Build(); @@ -144,7 +144,7 @@ describe('Build', () => { expect(this.build.state).toBe('finalstate'); }); - it('replaces the entire build trace', function () { + it('replaces the entire build trace', () => { jasmine.clock().tick(4001); let [{ success, context }] = $.ajax.calls.argsFor(1); success.call(context, { @@ -167,7 +167,7 @@ describe('Build', () => { expect($('#build-trace .js-build-output').text()).toMatch(/Different/); }); - it('reloads the page when the build is done', function () { + it('reloads the page when the build is done', () => { spyOn(Turbolinks, 'visit'); jasmine.clock().tick(4001); -- cgit v1.2.1 From 9c49fa2d92a3ab1051df87e4d75d9802a1b7cfc7 Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Thu, 24 Nov 2016 12:38:54 +0100 Subject: fix for builds with no start date and spec --- app/serializers/entity_date_helper.rb | 2 ++ spec/serializers/analytics_build_entity_spec.rb | 31 ++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/app/serializers/entity_date_helper.rb b/app/serializers/entity_date_helper.rb index 918abba8d99..3cc98fb18a1 100644 --- a/app/serializers/entity_date_helper.rb +++ b/app/serializers/entity_date_helper.rb @@ -2,6 +2,8 @@ module EntityDateHelper include ActionView::Helpers::DateHelper def interval_in_words(diff) + return 'not started' unless diff + "#{distance_of_time_in_words(Time.now, diff)} ago" end diff --git a/spec/serializers/analytics_build_entity_spec.rb b/spec/serializers/analytics_build_entity_spec.rb index c0b7e86b17c..ba562353661 100644 --- a/spec/serializers/analytics_build_entity_spec.rb +++ b/spec/serializers/analytics_build_entity_spec.rb @@ -7,7 +7,9 @@ describe AnalyticsBuildEntity do context 'build with an author' do let(:user) { create(:user) } - let(:build) { create(:ci_build, author: user, started_at: 2.hours.ago, finished_at: 1.hour.ago) } + let(:started_at) { 2.hours.ago } + let(:finished_at) { 1.hour.ago } + let(:build) { create(:ci_build, author: user, started_at: started_at, finished_at: finished_at) } subject { entity.as_json } @@ -31,5 +33,32 @@ describe AnalyticsBuildEntity do it 'contains the duration' do expect(subject[:total_time]).to eq(hours: 1 ) end + + context 'no started at or finished at date' do + let(:started_at) { nil } + let(:finished_at) { nil } + + it 'does not blow up' do + expect{ subject[:date] }.not_to raise_error + end + + it '' + end + + context 'no started at date' do + let(:started_at) { nil } + + it 'does not blow up' do + expect{ subject[:date] }.not_to raise_error + end + end + + context 'no finished at date' do + let(:finished_at) { nil } + + it 'does not blow up' do + expect{ subject[:date] }.not_to raise_error + end + end end end -- cgit v1.2.1 From aa895a64d928f8292ff9df526831ae74d250f748 Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Thu, 24 Nov 2016 12:42:25 +0100 Subject: Add changelog entry --- changelogs/unreleased/fix-ca-no-date.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-ca-no-date.yml diff --git a/changelogs/unreleased/fix-ca-no-date.yml b/changelogs/unreleased/fix-ca-no-date.yml new file mode 100644 index 00000000000..6de4a56ac0d --- /dev/null +++ b/changelogs/unreleased/fix-ca-no-date.yml @@ -0,0 +1,4 @@ +--- +title: Fix for error thrown in cycle analytics events if build has not started +merge_request: +author: -- cgit v1.2.1 From 830f739b99b36f8862dadc524e4aa72ec5a3366e Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Fri, 25 Nov 2016 11:22:44 +0100 Subject: use an empty total time when the build has not started yet so the UI knows --- app/serializers/analytics_build_entity.rb | 6 +++++- app/serializers/entity_date_helper.rb | 2 +- spec/serializers/analytics_build_entity_spec.rb | 24 +++++++++++++++++++++++- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/app/serializers/analytics_build_entity.rb b/app/serializers/analytics_build_entity.rb index abefcd5cc02..206a7eadbcf 100644 --- a/app/serializers/analytics_build_entity.rb +++ b/app/serializers/analytics_build_entity.rb @@ -13,7 +13,7 @@ class AnalyticsBuildEntity < Grape::Entity end expose :duration, as: :total_time do |build| - distance_of_time_as_hash(build.duration.to_f) + build_started?(build) ? distance_of_time_as_hash(build.duration.to_f) : {} end expose :branch do @@ -37,4 +37,8 @@ class AnalyticsBuildEntity < Grape::Entity def url_to(route, build, id = nil) public_send("#{route}_url", build.project.namespace, build.project, id || build) end + + def build_started?(build) + build.duration && build[:started_at] + end end diff --git a/app/serializers/entity_date_helper.rb b/app/serializers/entity_date_helper.rb index 3cc98fb18a1..9607ad55a8b 100644 --- a/app/serializers/entity_date_helper.rb +++ b/app/serializers/entity_date_helper.rb @@ -2,7 +2,7 @@ module EntityDateHelper include ActionView::Helpers::DateHelper def interval_in_words(diff) - return 'not started' unless diff + return 'Not started' unless diff "#{distance_of_time_in_words(Time.now, diff)} ago" end diff --git a/spec/serializers/analytics_build_entity_spec.rb b/spec/serializers/analytics_build_entity_spec.rb index ba562353661..1a9ad1968bd 100644 --- a/spec/serializers/analytics_build_entity_spec.rb +++ b/spec/serializers/analytics_build_entity_spec.rb @@ -42,7 +42,13 @@ describe AnalyticsBuildEntity do expect{ subject[:date] }.not_to raise_error end - it '' + it 'shows the right message' do + expect(subject[:date]).to eq('Not started') + end + + it 'shows the right total time' do + expect(subject[:total_time]).to eq({}) + end end context 'no started at date' do @@ -51,6 +57,14 @@ describe AnalyticsBuildEntity do it 'does not blow up' do expect{ subject[:date] }.not_to raise_error end + + it 'shows the right message' do + expect(subject[:date]).to eq('Not started') + end + + it 'shows the right total time' do + expect(subject[:total_time]).to eq({}) + end end context 'no finished at date' do @@ -59,6 +73,14 @@ describe AnalyticsBuildEntity do it 'does not blow up' do expect{ subject[:date] }.not_to raise_error end + + it 'shows the right message' do + expect(subject[:date]).to eq('about 2 hours ago') + end + + it 'shows the right total time' do + expect(subject[:total_time]).to eq({hours: 2}) + end end end end -- cgit v1.2.1 From 94a74e79cb087cc499add60ab638d999bc7e3815 Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Fri, 25 Nov 2016 11:46:13 +0100 Subject: fix rubocop warning --- spec/serializers/analytics_build_entity_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/serializers/analytics_build_entity_spec.rb b/spec/serializers/analytics_build_entity_spec.rb index 1a9ad1968bd..6b33fe66a63 100644 --- a/spec/serializers/analytics_build_entity_spec.rb +++ b/spec/serializers/analytics_build_entity_spec.rb @@ -79,7 +79,7 @@ describe AnalyticsBuildEntity do end it 'shows the right total time' do - expect(subject[:total_time]).to eq({hours: 2}) + expect(subject[:total_time]).to eq({ hours: 2 }) end end end -- cgit v1.2.1 From b8e10e327fa2b51a7f645f10576b26dca0b8341e Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Fri, 25 Nov 2016 11:59:12 +0100 Subject: Fix tests for allowing merged if pipeline succeeded --- .../merge_requests/only_allow_merge_if_build_succeeds_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb index 1ec3103feef..7e2907cd26f 100644 --- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb +++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb @@ -38,7 +38,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature: it 'does not allow to merge immediately' do visit_merge_request(merge_request) - expect(page).to have_button 'Merge When Build Succeeds' + expect(page).to have_button 'Merge When Pipeline Succeeds' expect(page).not_to have_button 'Select Merge Moment' end end @@ -97,7 +97,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature: it 'allows MR to be merged immediately', js: true do visit_merge_request(merge_request) - expect(page).to have_button 'Merge When Build Succeeds' + expect(page).to have_button 'Merge When Pipeline Succeeds' click_button 'Select Merge Moment' expect(page).to have_content 'Merge Immediately' -- cgit v1.2.1 From d71ad49fc570ef617d0bbf99af53596ef5d48892 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Mon, 21 Nov 2016 22:27:10 +0100 Subject: Accept a valid ref for issue show For example, now we support `/gitlab issue show #1`. Where the # used to trip the regex. --- lib/gitlab/chat_commands/issue_show.rb | 2 +- spec/lib/gitlab/chat_commands/issue_show_spec.rb | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/chat_commands/issue_show.rb b/lib/gitlab/chat_commands/issue_show.rb index f5bceb038e5..2a45d49cf6b 100644 --- a/lib/gitlab/chat_commands/issue_show.rb +++ b/lib/gitlab/chat_commands/issue_show.rb @@ -2,7 +2,7 @@ module Gitlab module ChatCommands class IssueShow < IssueCommand def self.match(text) - /\Aissue\s+show\s+(?<iid>\d+)/.match(text) + /\Aissue\s+show\s+#{Issue.reference_prefix}?(?<iid>\d+)/.match(text) end def self.help_message diff --git a/spec/lib/gitlab/chat_commands/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/issue_show_spec.rb index 331a4604e9b..2eab73e49e5 100644 --- a/spec/lib/gitlab/chat_commands/issue_show_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_show_spec.rb @@ -19,6 +19,14 @@ describe Gitlab::ChatCommands::IssueShow, service: true do it 'returns the issue' do expect(subject.iid).to be issue.iid end + + context 'when its reference is given' do + let(:regex_match) { described_class.match("issue show #{issue.to_reference}") } + + it 'shows the issue' do + expect(subject.iid).to be issue.iid + end + end end context 'the issue does not exist' do -- cgit v1.2.1 From 92b2c74ce14238c1032bd9faac6d178d25433532 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Thu, 24 Nov 2016 10:40:44 +0100 Subject: Refresh project authorizations using a Redis lease When I proposed using serializable transactions I was hoping we would be able to refresh data of individual users concurrently. Unfortunately upon closer inspection it was revealed this was not the case. This could result in a lot of queries failing due to serialization errors, overloading the database in the process (given enough workers trying to update the target table). To work around this we're now using a Redis lease that is cancelled upon completion. This ensures we can update the data of different users concurrently without overloading the database. The code will try to obtain the lease until it succeeds, waiting at least 1 second between retries. This is necessary as we may otherwise end up _not_ updating the data which is not an option. --- app/models/user.rb | 36 +++++++++------------- app/workers/authorized_projects_worker.rb | 23 ++++++++++++-- .../refresh-authorizations-with-lease.yml | 4 +++ db/fixtures/development/04_project.rb | 1 - db/fixtures/development/06_teams.rb | 1 - db/fixtures/development/17_cycle_analytics.rb | 1 - db/fixtures/support/serialized_transaction.rb | 9 ------ lib/gitlab/database.rb | 7 ----- spec/models/user_spec.rb | 27 ++++++++++++++++ spec/workers/authorized_projects_worker_spec.rb | 23 ++++++++++---- 10 files changed, 84 insertions(+), 48 deletions(-) create mode 100644 changelogs/unreleased/refresh-authorizations-with-lease.yml delete mode 100644 db/fixtures/support/serialized_transaction.rb diff --git a/app/models/user.rb b/app/models/user.rb index 513a19d81d2..ad4b4d9381b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -445,27 +445,21 @@ class User < ActiveRecord::Base end def refresh_authorized_projects - loop do - begin - Gitlab::Database.serialized_transaction do - project_authorizations.delete_all - - # project_authorizations_union can return multiple records for the same project/user with - # different access_level so we take row with the maximum access_level - project_authorizations.connection.execute <<-SQL - INSERT INTO project_authorizations (user_id, project_id, access_level) - SELECT user_id, project_id, MAX(access_level) AS access_level - FROM (#{project_authorizations_union.to_sql}) sub - GROUP BY user_id, project_id - SQL - - update_column(:authorized_projects_populated, true) unless authorized_projects_populated - end - - break - # In the event of a concurrent modification Rails raises StatementInvalid. - # In this case we want to keep retrying until the transaction succeeds - rescue ActiveRecord::StatementInvalid + transaction do + project_authorizations.delete_all + + # project_authorizations_union can return multiple records for the same + # project/user with different access_level so we take row with the maximum + # access_level + project_authorizations.connection.execute <<-SQL + INSERT INTO project_authorizations (user_id, project_id, access_level) + SELECT user_id, project_id, MAX(access_level) AS access_level + FROM (#{project_authorizations_union.to_sql}) sub + GROUP BY user_id, project_id + SQL + + unless authorized_projects_populated + update_column(:authorized_projects_populated, true) end end end diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb index 331727ba9d8..fccddb70d18 100644 --- a/app/workers/authorized_projects_worker.rb +++ b/app/workers/authorized_projects_worker.rb @@ -2,14 +2,33 @@ class AuthorizedProjectsWorker include Sidekiq::Worker include DedicatedSidekiqQueue + LEASE_TIMEOUT = 1.minute.to_i + def self.bulk_perform_async(args_list) Sidekiq::Client.push_bulk('class' => self, 'args' => args_list) end def perform(user_id) user = User.find_by(id: user_id) - return unless user - user.refresh_authorized_projects + refresh(user) if user + end + + def refresh(user) + lease_key = "refresh_authorized_projects:#{user.id}" + lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT) + + until uuid = lease.try_obtain + # Keep trying until we obtain the lease. If we don't do so we may end up + # not updating the list of authorized projects properly. To prevent + # hammering Redis too much we'll wait for a bit between retries. + sleep(1) + end + + begin + user.refresh_authorized_projects + ensure + Gitlab::ExclusiveLease.cancel(lease_key, uuid) + end end end diff --git a/changelogs/unreleased/refresh-authorizations-with-lease.yml b/changelogs/unreleased/refresh-authorizations-with-lease.yml new file mode 100644 index 00000000000..bb9b77018e3 --- /dev/null +++ b/changelogs/unreleased/refresh-authorizations-with-lease.yml @@ -0,0 +1,4 @@ +--- +title: Use a Redis lease for updating authorized projects +merge_request: 7733 +author: diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb index 18a2df7c059..a984eda5ab5 100644 --- a/db/fixtures/development/04_project.rb +++ b/db/fixtures/development/04_project.rb @@ -1,5 +1,4 @@ require 'sidekiq/testing' -require './db/fixtures/support/serialized_transaction' Sidekiq::Testing.inline! do Gitlab::Seeder.quiet do diff --git a/db/fixtures/development/06_teams.rb b/db/fixtures/development/06_teams.rb index 04c3690e152..5c2a03fec3f 100644 --- a/db/fixtures/development/06_teams.rb +++ b/db/fixtures/development/06_teams.rb @@ -1,5 +1,4 @@ require 'sidekiq/testing' -require './db/fixtures/support/serialized_transaction' Sidekiq::Testing.inline! do Gitlab::Seeder.quiet do diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb index 7b3908fae98..916ee8dbac8 100644 --- a/db/fixtures/development/17_cycle_analytics.rb +++ b/db/fixtures/development/17_cycle_analytics.rb @@ -1,6 +1,5 @@ require 'sidekiq/testing' require './spec/support/test_env' -require './db/fixtures/support/serialized_transaction' class Gitlab::Seeder::CycleAnalytics def initialize(project, perf: false) diff --git a/db/fixtures/support/serialized_transaction.rb b/db/fixtures/support/serialized_transaction.rb deleted file mode 100644 index d3305b661e5..00000000000 --- a/db/fixtures/support/serialized_transaction.rb +++ /dev/null @@ -1,9 +0,0 @@ -require 'gitlab/database' - -module Gitlab - module Database - def self.serialized_transaction - connection.transaction { yield } - end - end -end diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index 2d5c9232425..55b8f888d53 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -35,13 +35,6 @@ module Gitlab order end - def self.serialized_transaction - opts = {} - opts[:isolation] = :serializable unless Rails.env.test? && connection.transaction_open? - - connection.transaction(opts) { yield } - end - def self.random Gitlab::Database.postgresql? ? "RANDOM()" : "RAND()" end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 91826e5884d..14c891994d0 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1349,4 +1349,31 @@ describe User, models: true do expect(projects).to be_empty end end + + describe '#refresh_authorized_projects', redis: true do + let(:project1) { create(:empty_project) } + let(:project2) { create(:empty_project) } + let(:user) { create(:user) } + + before do + project1.team << [user, :reporter] + project2.team << [user, :guest] + + user.project_authorizations.delete_all + user.refresh_authorized_projects + end + + it 'refreshes the list of authorized projects' do + expect(user.project_authorizations.count).to eq(2) + end + + it 'sets the authorized_projects_populated column' do + expect(user.authorized_projects_populated).to eq(true) + end + + it 'stores the correct access levels' do + expect(user.project_authorizations.where(access_level: Gitlab::Access::GUEST).exists?).to eq(true) + expect(user.project_authorizations.where(access_level: Gitlab::Access::REPORTER).exists?).to eq(true) + end + end end diff --git a/spec/workers/authorized_projects_worker_spec.rb b/spec/workers/authorized_projects_worker_spec.rb index 18a1aab766c..95e2458da35 100644 --- a/spec/workers/authorized_projects_worker_spec.rb +++ b/spec/workers/authorized_projects_worker_spec.rb @@ -1,22 +1,33 @@ require 'spec_helper' describe AuthorizedProjectsWorker do + let(:worker) { described_class.new } + describe '#perform' do it "refreshes user's authorized projects" do user = create(:user) - expect(User).to receive(:find_by).with(id: user.id).and_return(user) - expect(user).to receive(:refresh_authorized_projects) + expect(worker).to receive(:refresh).with(an_instance_of(User)) - described_class.new.perform(user.id) + worker.perform(user.id) end - context "when user is not found" do + context "when the user is not found" do it "does nothing" do - expect_any_instance_of(User).not_to receive(:refresh_authorized_projects) + expect(worker).not_to receive(:refresh) - described_class.new.perform(999_999) + described_class.new.perform(-1) end end end + + describe '#refresh', redis: true do + it 'refreshes the authorized projects of the user' do + user = create(:user) + + expect(user).to receive(:refresh_authorized_projects) + + worker.refresh(user) + end + end end -- cgit v1.2.1 From 0ba03d7eb1d80e019b9b8266f0e14356d32e7d69 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Thu, 24 Nov 2016 11:25:23 +0100 Subject: Removed data-user-is view code With events no longer being cached this is no longer needed. --- app/models/event.rb | 4 ++++ app/views/events/event/_push.html.haml | 6 +++--- app/views/layouts/_head.html.haml | 2 -- app/views/layouts/_user_styles.html.haml | 24 ------------------------ spec/models/event_spec.rb | 18 ++++++++++++++++++ spec/views/layouts/_head.html.haml_spec.rb | 4 ---- 6 files changed, 25 insertions(+), 33 deletions(-) delete mode 100644 app/views/layouts/_user_styles.html.haml diff --git a/app/models/event.rb b/app/models/event.rb index 216dba46e74..2662f170765 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -347,6 +347,10 @@ class Event < ActiveRecord::Base update_all(last_activity_at: created_at) end + def authored_by?(user) + user ? author_id == user.id : false + end + private def recent_update? diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index 44fff49d99c..64ca3c32e01 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -18,7 +18,7 @@ - few_commits.each do |commit| = render "events/commit", commit: commit, project: project, event: event - - create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project) + - create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project) && event.authored_by?(current_user) - if event.commits_count > 1 %li.commits-stat - if event.commits_count > 2 @@ -35,12 +35,12 @@ Compare #{from_label}...#{truncate_sha(event.commit_to)} - if create_mr - %span{"data-user-is" => event.author_id, "data-display" => "inline"} + %span or = link_to create_mr_path(project.default_branch, event.ref_name, project) do create a merge request - elsif create_mr - %li.commits-stat{"data-user-is" => event.author_id} + %li.commits-stat = link_to create_mr_path(project.default_branch, event.ref_name, project) do Create Merge Request - elsif event.rm_ref? diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 757de92d6d4..3e488cf73b9 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -56,5 +56,3 @@ = render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id') = render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id') = render 'layouts/bootlint' if Rails.env.development? - - = render 'layouts/user_styles' diff --git a/app/views/layouts/_user_styles.html.haml b/app/views/layouts/_user_styles.html.haml deleted file mode 100644 index b76b3cb5510..00000000000 --- a/app/views/layouts/_user_styles.html.haml +++ /dev/null @@ -1,24 +0,0 @@ -:css - [data-user-is] { - display: none !important; - } - - [data-user-is="#{current_user.try(:id)}"] { - display: block !important; - } - - [data-user-is="#{current_user.try(:id)}"][data-display="inline"] { - display: inline !important; - } - - [data-user-is-not] { - display: block !important; - } - - [data-user-is-not][data-display="inline"] { - display: inline !important; - } - - [data-user-is-not="#{current_user.try(:id)}"] { - display: none !important; - } diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index b684053cd02..f8660da031d 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -260,6 +260,24 @@ describe Event, models: true do end end + describe '#authored_by?' do + let(:event) { build(:event) } + + it 'returns true when the event author and user are the same' do + expect(event.authored_by?(event.author)).to eq(true) + end + + it 'returns false when passing nil as an argument' do + expect(event.authored_by?(nil)).to eq(false) + end + + it 'returns false when the given user is not the author of the event' do + user = double(:user, id: -1) + + expect(event.authored_by?(user)).to eq(false) + end + end + def create_event(project, user, attrs = {}) data = { before: Gitlab::Git::BLANK_SHA, diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb index 3fddfb3b62f..8020faa1f9c 100644 --- a/spec/views/layouts/_head.html.haml_spec.rb +++ b/spec/views/layouts/_head.html.haml_spec.rb @@ -1,10 +1,6 @@ require 'spec_helper' describe 'layouts/_head' do - before do - stub_template 'layouts/_user_styles.html.haml' => '' - end - it 'escapes HTML-safe strings in page_title' do stub_helper_with_safe_string(:page_title) -- cgit v1.2.1 From 847ada36c48107442f69338eda4c0b601ab98b48 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Wed, 23 Nov 2016 19:18:34 +0200 Subject: Fix: Timeout creating and viewing merge request for binary file --- .../unreleased/timeout-merge-request-for-binary-file.yml | 4 ++++ lib/gitlab/diff/file_collection/merge_request_diff.rb | 6 +++--- .../gitlab/diff/file_collection/merge_request_diff_spec.rb | 13 +++++++++++++ .../merge_requests/merge_request_diff_cache_service_spec.rb | 1 + 4 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/timeout-merge-request-for-binary-file.yml create mode 100644 spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb diff --git a/changelogs/unreleased/timeout-merge-request-for-binary-file.yml b/changelogs/unreleased/timeout-merge-request-for-binary-file.yml new file mode 100644 index 00000000000..5161265d1bd --- /dev/null +++ b/changelogs/unreleased/timeout-merge-request-for-binary-file.yml @@ -0,0 +1,4 @@ +--- +title: Timeout creating and viewing merge request for binary file +merge_request: +author: diff --git a/lib/gitlab/diff/file_collection/merge_request_diff.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb index fe7adb7bed6..26bb0bc16f5 100644 --- a/lib/gitlab/diff/file_collection/merge_request_diff.rb +++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb @@ -20,7 +20,7 @@ module Gitlab # Extracted method to highlight in the same iteration to the diff_collection. def decorate_diff!(diff) diff_file = super - cache_highlight!(diff_file) if cacheable? + cache_highlight!(diff_file) if cacheable?(diff_file) diff_file end @@ -60,8 +60,8 @@ module Gitlab Rails.cache.write(cache_key, highlight_cache) if @highlight_cache_was_empty end - def cacheable? - @merge_request_diff.present? + def cacheable?(diff_file) + @merge_request_diff.present? && diff_file.blob.text? end def cache_key diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb new file mode 100644 index 00000000000..c863a5f04cc --- /dev/null +++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +describe Gitlab::Diff::FileCollection::MergeRequestDiff do + let(:merge_request) { create :merge_request } + + it 'does not hightlight binary files' do + allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(double("text?" => false)) + + expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines) + + described_class.new(merge_request.merge_request_diff, diff_options: nil).diff_files + end +end diff --git a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb index 807f89e80b7..05cdbe5287a 100644 --- a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb +++ b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb @@ -10,6 +10,7 @@ describe MergeRequests::MergeRequestDiffCacheService do expect(Rails.cache).to receive(:read).with(cache_key).and_return({}) expect(Rails.cache).to receive(:write).with(cache_key, anything) + allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(double("text?" => true)) subject.execute(merge_request) end -- cgit v1.2.1 From 07c3d2cafab4045f19849217f7e2e1a2b3a708d0 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Fri, 25 Nov 2016 14:41:16 +0100 Subject: Refactor CI variables docs --- doc/ci/README.md | 6 ++- doc/ci/variables/README.md | 113 ++++++++++++++++++++++++++++----------------- doc/ci/yaml/README.md | 16 +++++-- 3 files changed, 86 insertions(+), 49 deletions(-) diff --git a/doc/ci/README.md b/doc/ci/README.md index 545cc72682d..f7fb56cb76a 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -11,7 +11,7 @@ - [Configure a Runner, the application that runs your builds](runners/README.md) - [Use Docker images with GitLab Runner](docker/using_docker_images.md) - [Use CI to build Docker images](docker/using_docker_build.md) -- [Use variables in your `.gitlab-ci.yml`](variables/README.md) +- [Learn how to use variables in your build scripts](variables/README.md) - [Use SSH keys in your build environment](ssh_keys/README.md) - [Trigger builds through the API](triggers/README.md) - [Build artifacts](../user/project/builds/artifacts.md) @@ -24,4 +24,6 @@ ## Breaking changes -- [New CI build permissions model](../user/project/new_ci_build_permissions_model.md) Read about what changed in GitLab 8.12 and how that affects your builds. There's a new way to access your Git submodules and LFS objects in builds. +- [New CI build permissions model](../user/project/new_ci_build_permissions_model.md) + Read about what changed in GitLab 8.12 and how that affects your builds. + There's a new way to access your Git submodules and LFS objects in builds. diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index a4c3a731a20..bb0b171838b 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -1,22 +1,27 @@ -## Variables +# Variables When receiving a build from GitLab CI, the runner prepares the build environment. -It starts by setting a list of **predefined variables** (Environment Variables) and a list of **user-defined variables** +It starts by setting a list of **predefined variables** (Environment variables) +and a list of **user-defined variables**. -The variables can be overwritten. They take precedence over each other in this order: -1. Trigger variables +The variables can be overwritten. They take precedence over each other in this +order: + +1. Trigger variables (take precedence over all) 1. Secure variables 1. YAML-defined job-level variables 1. YAML-defined global variables -1. Predefined variables +1. Predefined variables (are the lowest in the chain) -For example, if you define: -1. `API_TOKEN=SECURE` as Secure Variable -1. `API_TOKEN=YAML` as YAML-defined variable +For example, if you define `API_TOKEN=secure` as a secure variable and +`API_TOKEN=yaml` as YAML-defined variable, the `API_TOKEN` will take the value +`secure` as the secure variables are higher in the chain. -The `API_TOKEN` will take the Secure Variable value: `SECURE`. +## Predefined variables (Environment variables) -### Predefined variables (Environment Variables) +>**Note:** +Some of the variables are available only if a minimum version of [GitLab Runner] +is used. | Variable | GitLab | Runner | Description | |-------------------------|--------|--------|-------------| @@ -52,7 +57,6 @@ The `API_TOKEN` will take the Secure Variable value: `SECURE`. | **GITLAB_USER_ID** | 8.12 | all | The id of the user who started the build | | **GITLAB_USER_EMAIL** | 8.12 | all | The email of the user who started the build | -**Some of the variables are only available when using runner with at least defined version.** Example values: @@ -87,27 +91,61 @@ export GITLAB_USER_ID="42" export GITLAB_USER_EMAIL="alexzander@sporer.com" ``` -### YAML-defined variables -**This feature requires GitLab Runner 0.5.0 or higher and GitLab CI 7.14 or higher ** +## YAML-defined variables + +>**Note:** +This feature requires GitLab Runner 0.5.0 or higher and GitLab CI 7.14 or higher. -GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in build environment. -The variables are stored in repository and are meant to store non-sensitive project configuration, ie. RAILS_ENV or DATABASE_URL. +GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in the +build environment. The variables are hence saved in the repository, and they +are meant to store non-sensitive project configuration, e.g., `RAILS_ENV` or +`DATABASE_URL`. + +For example, if you set the variable below globally (not inside a job), it will +be used in all executed commands and scripts: ```yaml variables: DATABASE_URL: "postgres://postgres@postgres/my_database" ``` -These variables can be later used in all executed commands and scripts. +The YAML-defined variables are also set to all created +[service containers](../docker/using_docker_images.md), thus allowing to fine +tune them. + +Variables can be defined at a global level, but also at a job level. To turn off +global defined variables in your job, define an empty array: + +```yaml +job_name: + variables: [] +``` + +## User-defined variables (secure variables) -The YAML-defined variables are also set to all created service containers, thus allowing to fine tune them. +>**Notes:** +- This feature requires GitLab Runner 0.4.0 or higher. +- Be aware that secure variables are not masked, and their values can be shown + in the build logs if explicitly asked to do so. If your project is public or + internal, you can set the pipelines private from your project's Pipelines + settings. Follow the discussion in issue [#13784][ce-13784] for masking the + secure variables. -Variables can be defined at a global level, but also at a job level. +GitLab CI allows you to define per-project **Secure variables** that are set in +the build environment. The secure variables are stored out of the repository +(`.gitlab-ci.yml`) and are securely passed to GitLab Runner making them +available in the build environment. It's the recommended method to use for +storing things like passwords, secret keys and credentials. -More information about Docker integration can be found in [Using Docker Images](../docker/using_docker_images.md). +Secure Variables can added by going to your project's +**Settings ➔ Variables ➔ Add variable**. -#### Debug tracing +Once you set them, they will be available for all subsequent builds. +## Debug tracing + +> Introduced in GitLab Runner 1.7. +> > **WARNING:** Enabling debug tracing can have severe security implications. The output **will** contain the content of all your secure variables and any other secrets! The output **will** be uploaded to the GitLab server and made visible @@ -131,7 +169,7 @@ before making them visible again. To enable debug traces, set the `CI_DEBUG_TRACE` variable to `true`: ```yaml -job1: +job_name: variables: CI_DEBUG_TRACE: "true" ``` @@ -139,40 +177,31 @@ job1: The [example project](https://gitlab.com/gitlab-examples/ci-debug-trace) demonstrates a working configuration, including build trace examples. -### User-defined variables (Secure Variables) -**This feature requires GitLab Runner 0.4.0 or higher** +## Using the CI variables in your job scripts -GitLab CI allows you to define per-project **Secure Variables** that are set in -the build environment. -The secure variables are stored out of the repository (the `.gitlab-ci.yml`). -The variables are securely passed to GitLab Runner and are available in the -build environment. -It's desired method to use them for storing passwords, secret keys or whatever -you want. +All variables are set as environment variables in the build environment, and +they are accessible with normal methods that are used to access such variables. +In most cases `bash` or `sh` is used to execute the build script. -**The value of the variable can be shown in build log if explicitly asked to do so.** -If your project is public or internal you can make the builds private. +To access the variables (predefined and user-defined) in a `bash`/`sh` environment, +prefix the variable name with the dollar sign (`$`): -Secure Variables can added by going to `Project > Variables > Add Variable`. - -They will be available for all subsequent builds. - -### Use variables -The variables are set as environment variables in build environment and are accessible with normal methods that are used to access such variables. -In most cases the **bash** is used to execute build script. -To access variables (predefined and user-defined) in bash environment, prefix the variable name with `$`: ``` job_name: script: - echo $CI_BUILD_ID ``` -You can also list all environment variables with `export` command, -but be aware that this will also expose value of all **Secure Variables** in build log: +You can also list all environment variables with the `export` command, +but be aware that this will also expose the values of all the secure variables +you set, in the build log: + ``` job_name: script: - export ``` +[ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784 +[gitlab runner]: https://docs.gitlab.com/runner/ [triggered]: ../triggers/README.md diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 338c9a27789..5f88974d360 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -406,14 +406,20 @@ except master. ### job variables It is possible to define build variables using a `variables` keyword on a job -level. It works basically the same way as its global-level equivalent but -allows you to define job-specific build variables. +level. It works basically the same way as its [global-level equivalent](#variables) +but allows you to define job-specific build variables. When the `variables` keyword is used on a job level, it overrides global YAML -build variables and predefined variables. +build variables and predefined variables. To turn off global defined variables +in your job, define an empty array: -Build variables priority is defined in -[variables documentation](../variables/README.md). +```yaml +job_name: + variables: [] +``` + +Build variables priority is defined in the +[variables documentation][variables]. ### tags -- cgit v1.2.1 From e1285c1d8ad2c7a5ed556bd5296fbe4afcacb16d Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Fri, 25 Nov 2016 15:11:56 +0100 Subject: Restore method that ensures builds being created --- app/services/ci/process_pipeline_service.rb | 15 ++++++++ spec/factories/ci/pipelines.rb | 11 ++++++ spec/services/ci/process_pipeline_service_spec.rb | 47 +++++++++++++---------- 3 files changed, 52 insertions(+), 21 deletions(-) diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index e6bd1d1460c..2e028c44d8b 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -5,6 +5,8 @@ module Ci def execute(pipeline) @pipeline = pipeline + ensure_created_builds! # TODO, remove me in 9.0 + new_builds = stage_indexes_of_created_builds.map do |index| process_stage(index) @@ -67,5 +69,18 @@ module Ci def created_builds pipeline.builds.created end + + # This method is DEPRECATED and should be removed in 9.0. + # + # We need it to maintain backwards compatibility with previous versions + # when builds were not created within one transaction with the pipeline. + # + def ensure_created_builds! + return if created_builds.any? + + Ci::CreatePipelineBuildsService + .new(project, current_user) + .execute(pipeline) + end end end diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb index ac2a1ba5dff..3c35dae8772 100644 --- a/spec/factories/ci/pipelines.rb +++ b/spec/factories/ci/pipelines.rb @@ -29,5 +29,16 @@ FactoryGirl.define do allow(commit).to receive(:ci_yaml_file) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) } end end + + factory(:ci_pipeline_with_yaml) do + transient { yaml nil } + + after(:build) do |pipeline, evaluator| + raise ArgumentError unless evaluator.yaml + + allow(pipeline).to receive(:ci_yaml_file) + .and_return(YAML.dump(evaluator.yaml)) + end + end end end diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index b130f876259..306943a5488 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Ci::ProcessPipelineService, services: true do - let(:pipeline) { create(:ci_pipeline, ref: 'master') } + let(:pipeline) { create(:ci_empty_pipeline, ref: 'master') } let(:user) { create(:user) } describe '#execute' do @@ -293,57 +293,62 @@ describe Ci::ProcessPipelineService, services: true do end end - context 'when there are builds in multiple stages' do + context 'when there are builds that are not created yet' do + let(:pipeline) do + create(:ci_pipeline_with_yaml, yaml: config) + end + + let(:config) do + { rspec: { stage: 'test', script: 'rspec' }, + deploy: { stage: 'deploy', script: 'rsync' } } + end + before do create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage: 'build', stage_idx: 0) create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage: 'build', stage_idx: 0) - create(:ci_build, :created, pipeline: pipeline, name: 'rspec', stage: 'test', stage_idx: 1) - create(:ci_build, :created, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 1) - create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 2) end it 'processes the pipeline' do # Currently we have five builds with state created # expect(builds.count).to eq(0) - expect(all_builds.count).to eq(5) + expect(all_builds.count).to eq(2) - # Process builds will mark the created as pending + # Process builds service will enqueue builds from the first stage. # process_pipeline expect(builds.count).to eq(2) - expect(all_builds.count).to eq(5) + expect(all_builds.count).to eq(2) - # When builds succeed we will enqueue remaining builds - # We will have 2 succeeded, 2 pending (from stage test), - # total 5 (one more build from deploy) + # When builds succeed we will enqueue remaining builds. + # + # We will have 2 succeeded, 1 pending (from stage test), total 4 (two + # additional build from `.gitlab-ci.yml`). # succeed_pending process_pipeline expect(builds.success.count).to eq(2) - expect(builds.pending.count).to eq(2) - expect(all_builds.count).to eq(5) + expect(builds.pending.count).to eq(1) + expect(all_builds.count).to eq(4) - # When we succeed the 2 pending from stage test, - # We will queue a deploy stage. + # When pending build succeeds in stage test, we enqueue deploy stage. # succeed_pending process_pipeline expect(builds.pending.count).to eq(1) - expect(builds.success.count).to eq(4) - expect(all_builds.count).to eq(5) + expect(builds.success.count).to eq(3) + expect(all_builds.count).to eq(4) - # When we succeed last pending build, we will have - # a total of 5 succeeded builds + # When the last one succeeds we have 4 successful builds. # succeed_pending process_pipeline - expect(builds.success.count).to eq(5) - expect(all_builds.count).to eq(5) + expect(builds.success.count).to eq(4) + expect(all_builds.count).to eq(4) end end end -- cgit v1.2.1 From d766ab9f9a72a2919fb20c27fdf7b74e88042340 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Fri, 25 Nov 2016 15:13:15 +0100 Subject: Remove pipeline factory that is not used in tests --- spec/factories/ci/pipelines.rb | 6 ------ 1 file changed, 6 deletions(-) diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb index 3c35dae8772..4fd29806590 100644 --- a/spec/factories/ci/pipelines.rb +++ b/spec/factories/ci/pipelines.rb @@ -18,12 +18,6 @@ FactoryGirl.define do end end - factory :ci_pipeline_with_two_job do - after(:build) do |commit| - allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" }, spinach: { script: "ls" } }) } - end - end - factory :ci_pipeline do after(:build) do |commit| allow(commit).to receive(:ci_yaml_file) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) } -- cgit v1.2.1 From 86e7f22b6d10b9113c52f79685edb332d202f567 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Fri, 25 Nov 2016 15:15:31 +0100 Subject: Improve readability in pipeline test objects factory --- spec/factories/ci/pipelines.rb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb index 4fd29806590..23585db6ebd 100644 --- a/spec/factories/ci/pipelines.rb +++ b/spec/factories/ci/pipelines.rb @@ -7,20 +7,24 @@ FactoryGirl.define do project factory: :empty_project factory :ci_pipeline_without_jobs do - after(:build) do |commit| - allow(commit).to receive(:ci_yaml_file) { YAML.dump({}) } + after(:build) do |pipeline| + allow(pipeline).to receive(:ci_yaml_file) { YAML.dump({}) } end end factory :ci_pipeline_with_one_job do - after(:build) do |commit| - allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" } }) } + after(:build) do |pipeline| + allow(pipeline).to receive(:ci_yaml_file) do + YAML.dump({ rspec: { script: "ls" } }) + end end end factory :ci_pipeline do - after(:build) do |commit| - allow(commit).to receive(:ci_yaml_file) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) } + after(:build) do |pipeline| + allow(pipeline).to receive(:ci_yaml_file) do + File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + end end end -- cgit v1.2.1 From 6a08de7386fd41c9bbe27c32338328c6e6b40027 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Fri, 25 Nov 2016 15:41:28 +0100 Subject: Add issue search slash command One of many requested in: gitlab-org/gitlab-ce#24768 --- .../unreleased/zj-issue-search-slash-command.yml | 4 ++ lib/gitlab/chat_commands/base_command.rb | 4 +- lib/gitlab/chat_commands/command.rb | 1 + lib/gitlab/chat_commands/issue_command.rb | 6 +-- lib/gitlab/chat_commands/issue_search.rb | 17 ++++++++ spec/lib/gitlab/chat_commands/issue_search_spec.rb | 46 ++++++++++++++++++++++ spec/requests/api/services_spec.rb | 2 +- 7 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 changelogs/unreleased/zj-issue-search-slash-command.yml create mode 100644 lib/gitlab/chat_commands/issue_search.rb create mode 100644 spec/lib/gitlab/chat_commands/issue_search_spec.rb diff --git a/changelogs/unreleased/zj-issue-search-slash-command.yml b/changelogs/unreleased/zj-issue-search-slash-command.yml new file mode 100644 index 00000000000..de41c39d545 --- /dev/null +++ b/changelogs/unreleased/zj-issue-search-slash-command.yml @@ -0,0 +1,4 @@ +--- +title: Add issue search slash command +merge_request: +author: diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/chat_commands/base_command.rb index e59d69b72b9..25da8474e95 100644 --- a/lib/gitlab/chat_commands/base_command.rb +++ b/lib/gitlab/chat_commands/base_command.rb @@ -40,9 +40,7 @@ module Gitlab private def find_by_iid(iid) - resource = collection.find_by(iid: iid) - - readable?(resource) ? resource : nil + collection.find_by(iid: iid) end end end diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index 0ec358debc7..b0d3fdbc48a 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -4,6 +4,7 @@ module Gitlab COMMANDS = [ Gitlab::ChatCommands::IssueShow, Gitlab::ChatCommands::IssueCreate, + Gitlab::ChatCommands::IssueSearch, Gitlab::ChatCommands::Deploy, ].freeze diff --git a/lib/gitlab/chat_commands/issue_command.rb b/lib/gitlab/chat_commands/issue_command.rb index f1bc36239d5..84de3e44c70 100644 --- a/lib/gitlab/chat_commands/issue_command.rb +++ b/lib/gitlab/chat_commands/issue_command.rb @@ -6,11 +6,7 @@ module Gitlab end def collection - project.issues - end - - def readable?(issue) - self.class.can?(current_user, :read_issue, issue) + IssuesFinder.new(current_user, project_id: project.id).execute end end end diff --git a/lib/gitlab/chat_commands/issue_search.rb b/lib/gitlab/chat_commands/issue_search.rb new file mode 100644 index 00000000000..51bf80c800b --- /dev/null +++ b/lib/gitlab/chat_commands/issue_search.rb @@ -0,0 +1,17 @@ +module Gitlab + module ChatCommands + class IssueSearch < IssueCommand + def self.match(text) + /\Aissue\s+search\s+(?<query>.*)/.match(text) + end + + def self.help_message + "issue search <your query>" + end + + def execute(match) + collection.search(match[:query]).limit(QUERY_LIMIT) + end + end + end +end diff --git a/spec/lib/gitlab/chat_commands/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/issue_search_spec.rb new file mode 100644 index 00000000000..24c06a967fa --- /dev/null +++ b/spec/lib/gitlab/chat_commands/issue_search_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::IssueSearch, service: true do + describe '#execute' do + let!(:issue) { create(:issue, title: 'find me') } + let!(:confidential) { create(:issue, :confidential, project: project, title: 'mepmep find') } + let(:project) { issue.project } + let(:user) { issue.author } + let(:regex_match) { described_class.match("issue search find") } + + subject do + described_class.new(project, user).execute(regex_match) + end + + context 'when the user has no access' do + it 'only returns the open issues' do + expect(subject).not_to include(confidential) + end + end + + context 'the user has access' do + before do + project.team << [user, :master] + end + + it 'returns all results' do + expect(subject).to include(confidential, issue) + end + end + + context 'without hits on the query' do + it 'returns an empty collection' do + expect(subject).to be_empty + end + end + end + + describe 'self.match' do + let(:query) { "my search keywords" } + it 'matches the query' do + match = described_class.match("issue search #{query}") + + expect(match[:query]).to eq(query) + end + end +end diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index ce9c96ace21..bb0344e5995 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -128,7 +128,7 @@ describe API::API, api: true do ) end - it 'retusn status 200' do + it 'returns status 200' do post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger"), params expect(response).to have_http_status(200) -- cgit v1.2.1 From 40e8185b64a04f719c85a793d0fdd5438a129975 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Tue, 22 Nov 2016 17:28:58 +0100 Subject: Expose coverage on GET pipelines/:id The coverage wasn't exposed yet, now it is but only for detailed requests to save queries on the database. --- changelogs/unreleased/zj-expose-coverage-pipelines.yml | 4 ++++ lib/api/entities.rb | 1 + spec/requests/api/pipelines_spec.rb | 12 ++++++++++++ 3 files changed, 17 insertions(+) create mode 100644 changelogs/unreleased/zj-expose-coverage-pipelines.yml diff --git a/changelogs/unreleased/zj-expose-coverage-pipelines.yml b/changelogs/unreleased/zj-expose-coverage-pipelines.yml new file mode 100644 index 00000000000..34e4926e58a --- /dev/null +++ b/changelogs/unreleased/zj-expose-coverage-pipelines.yml @@ -0,0 +1,4 @@ +--- +title: 'API: expose pipeline coverage' +merge_request: +author: diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 33cb6fd3704..7dfaf2e8eb7 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -606,6 +606,7 @@ module API expose :user, with: Entities::UserBasic expose :created_at, :updated_at, :started_at, :finished_at, :committed_at expose :duration + expose :coverage end class EnvironmentBasic < Grape::Entity diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb index d83f7883c78..a7e511aaeda 100644 --- a/spec/requests/api/pipelines_spec.rb +++ b/spec/requests/api/pipelines_spec.rb @@ -103,6 +103,18 @@ describe API::API, api: true do expect(json_response['message']).to eq '404 Not found' expect(json_response['id']).to be nil end + + context 'with coverage' do + before do + create(:ci_build, coverage: 30, pipeline: pipeline) + end + + it 'exposes the coverage' do + get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) + + expect(json_response["coverage"].to_i).to eq(30) + end + end end context 'unauthorized user' do -- cgit v1.2.1 From 38ed96e9b1a47dca5aa2590fa9b0ade908337435 Mon Sep 17 00:00:00 2001 From: hhoopes <heidih@gmail.com> Date: Wed, 17 Aug 2016 16:27:01 -0600 Subject: Add diff hunks to notification emails on MR Added diff hunks to notification emails of messages on merge requests. This provides code context to the note. Uses existing template for formatting a diff for email (from repository push notifications). --- CHANGELOG.md | 1 + .../mailers/highlighted_diff_email.scss | 143 +++++++++++++++++++++ .../stylesheets/mailers/repository_push_email.scss | 143 --------------------- .../notify/note_merge_request_email.html.haml | 8 ++ app/views/notify/repository_push_email.html.haml | 2 +- spec/mailers/notify_spec.rb | 19 ++- 6 files changed, 170 insertions(+), 146 deletions(-) create mode 100644 app/assets/stylesheets/mailers/highlighted_diff_email.scss delete mode 100644 app/assets/stylesheets/mailers/repository_push_email.scss diff --git a/CHANGELOG.md b/CHANGELOG.md index 549336e4dff..56f749e94ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -797,6 +797,7 @@ entry. ## 8.11.0 (2016-08-22) - Use test coverage value from the latest successful pipeline in badge. !5862 + - Add git diff context to notifications of new notes on merge requests !5855 (hoopes) - Add test coverage report badge. !5708 - Remove the http_parser.rb dependency by removing the tinder gem. !5758 (tbalthazar) - Add Koding (online IDE) integration diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss new file mode 100644 index 00000000000..8d1a6020ca4 --- /dev/null +++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss @@ -0,0 +1,143 @@ +@import "framework/variables"; + +// This file is largely copied from `highlight/white.scss`, but modified to +// avoid all descendant selectors (`table td`). This is because the CSS inlining +// we use performs dramatically worse on descendant selectors than the +// alternatives. +// <https://gitlab.com/gitlab-org/gitlab-ee/issues/490#note_12283632> +// +// DO NOT ADD ANY DESCENDANT SELECTORS TO THIS FILE. Instead, use (in order of +// preference): plain class selectors, type (element name) selectors, or +// explicit child selectors. + +.code { + background-color: #fff; + font-family: monospace; + font-size: $code_font_size; + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + + > tr { + line-height: $code_line_height; + } +} + +.diff-line-num { + padding: 0 5px; + text-align: right; + width: 35px; + background-color: $background-color; + color: $black-transparent; + border-right: 1px solid $table-border-gray; + + &.old { + background-color: $line-number-old; + border-right-color: $line-removed-dark; + } + + &.new { + background-color: $line-number-new; + border-right-color: $line-added-dark; + } +} + +.line_content { + padding-left: 0.5em; + padding-right: 0.5em; + + &.old { + background-color: $line-removed; + + > .line > span.idiff, + > .line > span > span.idiff { + background-color: $line-removed-dark; + } + } + + &.new { + background-color: $line-added; + + > .line > span.idiff, + > .line > span > span.idiff { + background-color: $line-added-dark; + } + } + + &.match { + color: $black-transparent; + background-color: $match-line; + } +} + +pre { + margin: 0; +} + +span.highlight_word { + background-color: #fafe3d !important; +} + +.hll { background-color: #f8f8f8; } +.c { color: #998; font-style: italic; } +.err { color: #a61717; background-color: #e3d2d2; } +.k { font-weight: bold; } +.o { font-weight: bold; } +.cm { color: #998; font-style: italic; } +.cp { color: #999; font-weight: bold; } +.c1 { color: #998; font-style: italic; } +.cs { color: #999; font-weight: bold; font-style: italic; } +.gd { color: #000; background-color: #fdd; } +.gd .x { color: #000; background-color: #faa; } +.ge { font-style: italic; } +.gr { color: #a00; } +.gh { color: #999; } +.gi { color: #000; background-color: #dfd; } +.gi .x { color: #000; background-color: #afa; } +.go { color: #888; } +.gp { color: #555; } +.gs { font-weight: bold; } +.gu { color: #800080; font-weight: bold; } +.gt { color: #a00; } +.kc { font-weight: bold; } +.kd { font-weight: bold; } +.kn { font-weight: bold; } +.kp { font-weight: bold; } +.kr { font-weight: bold; } +.kt { color: #458; font-weight: bold; } +.m { color: #099; } +.s { color: #d14; } +.n { color: #333; } +.na { color: teal; } +.nb { color: #0086b3; } +.nc { color: #458; font-weight: bold; } +.no { color: teal; } +.ni { color: purple; } +.ne { color: #900; font-weight: bold; } +.nf { color: #900; font-weight: bold; } +.nn { color: #555; } +.nt { color: navy; } +.nv { color: teal; } +.ow { font-weight: bold; } +.w { color: #bbb; } +.mf { color: #099; } +.mh { color: #099; } +.mi { color: #099; } +.mo { color: #099; } +.sb { color: #d14; } +.sc { color: #d14; } +.sd { color: #d14; } +.s2 { color: #d14; } +.se { color: #d14; } +.sh { color: #d14; } +.si { color: #d14; } +.sx { color: #d14; } +.sr { color: #009926; } +.s1 { color: #d14; } +.ss { color: #990073; } +.bp { color: #999; } +.vc { color: teal; } +.vg { color: teal; } +.vi { color: teal; } +.il { color: #099; } +.gc { color: #999; background-color: #eaf2f5; } diff --git a/app/assets/stylesheets/mailers/repository_push_email.scss b/app/assets/stylesheets/mailers/repository_push_email.scss deleted file mode 100644 index 8d1a6020ca4..00000000000 --- a/app/assets/stylesheets/mailers/repository_push_email.scss +++ /dev/null @@ -1,143 +0,0 @@ -@import "framework/variables"; - -// This file is largely copied from `highlight/white.scss`, but modified to -// avoid all descendant selectors (`table td`). This is because the CSS inlining -// we use performs dramatically worse on descendant selectors than the -// alternatives. -// <https://gitlab.com/gitlab-org/gitlab-ee/issues/490#note_12283632> -// -// DO NOT ADD ANY DESCENDANT SELECTORS TO THIS FILE. Instead, use (in order of -// preference): plain class selectors, type (element name) selectors, or -// explicit child selectors. - -.code { - background-color: #fff; - font-family: monospace; - font-size: $code_font_size; - -premailer-cellpadding: 0; - -premailer-cellspacing: 0; - -premailer-width: 100%; - - > tr { - line-height: $code_line_height; - } -} - -.diff-line-num { - padding: 0 5px; - text-align: right; - width: 35px; - background-color: $background-color; - color: $black-transparent; - border-right: 1px solid $table-border-gray; - - &.old { - background-color: $line-number-old; - border-right-color: $line-removed-dark; - } - - &.new { - background-color: $line-number-new; - border-right-color: $line-added-dark; - } -} - -.line_content { - padding-left: 0.5em; - padding-right: 0.5em; - - &.old { - background-color: $line-removed; - - > .line > span.idiff, - > .line > span > span.idiff { - background-color: $line-removed-dark; - } - } - - &.new { - background-color: $line-added; - - > .line > span.idiff, - > .line > span > span.idiff { - background-color: $line-added-dark; - } - } - - &.match { - color: $black-transparent; - background-color: $match-line; - } -} - -pre { - margin: 0; -} - -span.highlight_word { - background-color: #fafe3d !important; -} - -.hll { background-color: #f8f8f8; } -.c { color: #998; font-style: italic; } -.err { color: #a61717; background-color: #e3d2d2; } -.k { font-weight: bold; } -.o { font-weight: bold; } -.cm { color: #998; font-style: italic; } -.cp { color: #999; font-weight: bold; } -.c1 { color: #998; font-style: italic; } -.cs { color: #999; font-weight: bold; font-style: italic; } -.gd { color: #000; background-color: #fdd; } -.gd .x { color: #000; background-color: #faa; } -.ge { font-style: italic; } -.gr { color: #a00; } -.gh { color: #999; } -.gi { color: #000; background-color: #dfd; } -.gi .x { color: #000; background-color: #afa; } -.go { color: #888; } -.gp { color: #555; } -.gs { font-weight: bold; } -.gu { color: #800080; font-weight: bold; } -.gt { color: #a00; } -.kc { font-weight: bold; } -.kd { font-weight: bold; } -.kn { font-weight: bold; } -.kp { font-weight: bold; } -.kr { font-weight: bold; } -.kt { color: #458; font-weight: bold; } -.m { color: #099; } -.s { color: #d14; } -.n { color: #333; } -.na { color: teal; } -.nb { color: #0086b3; } -.nc { color: #458; font-weight: bold; } -.no { color: teal; } -.ni { color: purple; } -.ne { color: #900; font-weight: bold; } -.nf { color: #900; font-weight: bold; } -.nn { color: #555; } -.nt { color: navy; } -.nv { color: teal; } -.ow { font-weight: bold; } -.w { color: #bbb; } -.mf { color: #099; } -.mh { color: #099; } -.mi { color: #099; } -.mo { color: #099; } -.sb { color: #d14; } -.sc { color: #d14; } -.sd { color: #d14; } -.s2 { color: #d14; } -.se { color: #d14; } -.sh { color: #d14; } -.si { color: #d14; } -.sx { color: #d14; } -.sr { color: #009926; } -.s1 { color: #d14; } -.ss { color: #990073; } -.bp { color: #999; } -.vc { color: teal; } -.vg { color: teal; } -.vi { color: teal; } -.il { color: #099; } -.gc { color: #999; background-color: #eaf2f5; } diff --git a/app/views/notify/note_merge_request_email.html.haml b/app/views/notify/note_merge_request_email.html.haml index ea7e3d199fd..de3f32e6415 100644 --- a/app/views/notify/note_merge_request_email.html.haml +++ b/app/views/notify/note_merge_request_email.html.haml @@ -1,7 +1,15 @@ += content_for :head do + = stylesheet_link_tag 'mailers/highlighted_diff_email' + - if @note.diff_note? && @note.diff_file %p.details New comment on diff for = link_to @note.diff_file.file_path, @target_url \: + .diff-content.code.js-syntax-highlight + %table + - Discussion.new([@note]).truncated_diff_lines.each do |line| + = render "projects/diffs/line", line: line, diff_file: @note.diff_file, plain: true + = render 'note_message' diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index 307c5a11206..25883de257c 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -1,5 +1,5 @@ = content_for :head do - = stylesheet_link_tag 'mailers/repository_push_email' + = stylesheet_link_tag 'mailers/highlighted_diff_email' %h3 #{@message.author_name} #{@message.action_name} #{@message.ref_type} #{@message.ref_name} diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 932a5dc4862..b40a6b1cbac 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -580,8 +580,10 @@ describe Notify do let(:note_author) { create(:user, name: 'author_name') } let(:note) { create(:note, project: project, author: note_author) } - before :each do - allow(Note).to receive(:find).with(note.id).and_return(note) + before do |example| + unless example.metadata[:skip_before] + allow(Note).to receive(:find).with(note.id).and_return(note) + end end shared_examples 'a note email' do @@ -663,6 +665,19 @@ describe Notify do end end + describe "on a merge request with diffs", :skip_before do + let(:merge_request) { create(:merge_request_with_diffs) } + let(:note_with_diff) {create(:diff_note_on_merge_request)} + + before(:each) { allow(note_with_diff).to receive(:noteable).and_return(merge_request) } + subject { Notify.note_merge_request_email(recipient.id, note_with_diff.id) } + + it 'includes diffs with character-level highlighting' do + expected_line_text = Discussion.new([note_with_diff]).truncated_diff_lines.first.text + is_expected.to have_body_text expected_line_text + end + end + describe 'on an issue' do let(:issue) { create(:issue, project: project) } let(:note_on_issue_path) { namespace_project_issue_path(project.namespace, project, issue, anchor: "note_#{note.id}") } -- cgit v1.2.1 From 24070bac45134c915c13d3e94723a44f59ab4e3a Mon Sep 17 00:00:00 2001 From: hhoopes <heidih@gmail.com> Date: Mon, 22 Aug 2016 23:36:30 -0600 Subject: Add new template to handle both commit & mr notes Currently comments on commits and merge requests do not require merge request- or commit-specific information, but can use the same template. Rather than change the method which calls the template, I opted to keep the templates separate and create a new template to highlight their identicality, while preserving the option to distinguish them from each other in the future. Also removed some of the inconsistencies between text and html email versions. Still needed is a text-only version of git diffs and testing. --- app/mailers/emails/notes.rb | 2 ++ .../notify/_note_mr_or_commit_email.html.haml | 16 ++++++++++++++++ app/views/notify/note_commit_email.html.haml | 9 +++++++-- app/views/notify/note_commit_email.text.erb | 6 +++--- .../notify/note_merge_request_email.html.haml | 22 +++++++--------------- app/views/notify/note_merge_request_email.text.erb | 6 +++--- spec/mailers/notify_spec.rb | 19 ++----------------- 7 files changed, 40 insertions(+), 40 deletions(-) create mode 100644 app/views/notify/_note_mr_or_commit_email.html.haml diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index 96116e916dd..0d20c9092c4 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -4,6 +4,7 @@ module Emails setup_note_mail(note_id, recipient_id) @commit = @note.noteable + @discussion = @note.to_discussion if @note.diff_note? @target_url = namespace_project_commit_url(*note_target_url_options) mail_answer_thread(@commit, @@ -24,6 +25,7 @@ module Emails setup_note_mail(note_id, recipient_id) @merge_request = @note.noteable + @discussion = @note.to_discussion if @note.diff_note? @target_url = namespace_project_merge_request_url(*note_target_url_options) mail_answer_thread(@merge_request, note_thread_options(recipient_id)) end diff --git a/app/views/notify/_note_mr_or_commit_email.html.haml b/app/views/notify/_note_mr_or_commit_email.html.haml new file mode 100644 index 00000000000..3e2046aa9cf --- /dev/null +++ b/app/views/notify/_note_mr_or_commit_email.html.haml @@ -0,0 +1,16 @@ += content_for :head do + = stylesheet_link_tag 'mailers/highlighted_diff_email' + +- if note.diff_note? && note.diff_file + = link_to note.diff_file.file_path, @target_url, class: 'details' + \: + + %table + = render partial: "projects/diffs/line", + collection: discussion.truncated_diff_lines, + as: :line, + locals: { diff_file: note.diff_file, + plain: true, + email: true } + += render 'note_message' diff --git a/app/views/notify/note_commit_email.html.haml b/app/views/notify/note_commit_email.html.haml index 1d961e4424c..f4293a3aa50 100644 --- a/app/views/notify/note_commit_email.html.haml +++ b/app/views/notify/note_commit_email.html.haml @@ -1,2 +1,7 @@ -= render 'note_message' - +%p.details + New comment for Commit + = @commit.short_id + on + = render partial: 'note_mr_or_commit_email', + locals: { note: @note, + discussion: @discussion} diff --git a/app/views/notify/note_commit_email.text.erb b/app/views/notify/note_commit_email.text.erb index aaeaf5fdf73..a1ef9858021 100644 --- a/app/views/notify/note_commit_email.text.erb +++ b/app/views/notify/note_commit_email.text.erb @@ -2,8 +2,8 @@ New comment for Commit <%= @commit.short_id %> <%= url_for(namespace_project_commit_url(@note.project.namespace, @note.project, id: @commit.id, anchor: "note_#{@note.id}")) %> - -Author: <%= @note.author_name %> +<% if current_application_settings.email_author_in_body %> + <%= @note.author_name %> wrote: +<% end %> <%= @note.note %> - diff --git a/app/views/notify/note_merge_request_email.html.haml b/app/views/notify/note_merge_request_email.html.haml index de3f32e6415..75250b0383d 100644 --- a/app/views/notify/note_merge_request_email.html.haml +++ b/app/views/notify/note_merge_request_email.html.haml @@ -1,15 +1,7 @@ -= content_for :head do - = stylesheet_link_tag 'mailers/highlighted_diff_email' - -- if @note.diff_note? && @note.diff_file - %p.details - New comment on diff for - = link_to @note.diff_file.file_path, @target_url - \: - - .diff-content.code.js-syntax-highlight - %table - - Discussion.new([@note]).truncated_diff_lines.each do |line| - = render "projects/diffs/line", line: line, diff_file: @note.diff_file, plain: true - -= render 'note_message' +%p.details + New comment for Merge Request + = @merge_request.to_reference + on + = render partial: 'note_mr_or_commit_email', + locals: { note: @note, + discussion: @discussion} diff --git a/app/views/notify/note_merge_request_email.text.erb b/app/views/notify/note_merge_request_email.text.erb index 8cdab63829e..42d29b34ebc 100644 --- a/app/views/notify/note_merge_request_email.text.erb +++ b/app/views/notify/note_merge_request_email.text.erb @@ -2,8 +2,8 @@ New comment for Merge Request <%= @merge_request.to_reference %> <%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, anchor: "note_#{@note.id}")) %> - -<%= @note.author_name %> +<% if current_application_settings.email_author_in_body %> + <%= @note.author_name %> wrote: +<% end %> <%= @note.note %> - diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index b40a6b1cbac..932a5dc4862 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -580,10 +580,8 @@ describe Notify do let(:note_author) { create(:user, name: 'author_name') } let(:note) { create(:note, project: project, author: note_author) } - before do |example| - unless example.metadata[:skip_before] - allow(Note).to receive(:find).with(note.id).and_return(note) - end + before :each do + allow(Note).to receive(:find).with(note.id).and_return(note) end shared_examples 'a note email' do @@ -665,19 +663,6 @@ describe Notify do end end - describe "on a merge request with diffs", :skip_before do - let(:merge_request) { create(:merge_request_with_diffs) } - let(:note_with_diff) {create(:diff_note_on_merge_request)} - - before(:each) { allow(note_with_diff).to receive(:noteable).and_return(merge_request) } - subject { Notify.note_merge_request_email(recipient.id, note_with_diff.id) } - - it 'includes diffs with character-level highlighting' do - expected_line_text = Discussion.new([note_with_diff]).truncated_diff_lines.first.text - is_expected.to have_body_text expected_line_text - end - end - describe 'on an issue' do let(:issue) { create(:issue, project: project) } let(:note_on_issue_path) { namespace_project_issue_path(project.namespace, project, issue, anchor: "note_#{note.id}") } -- cgit v1.2.1 From f928dba99b0550cefa7534d7fd5bd1ea16609274 Mon Sep 17 00:00:00 2001 From: hhoopes <heidih@gmail.com> Date: Thu, 25 Aug 2016 10:38:07 -0600 Subject: Change diff highlight/truncate for reusability Previously the `truncated_diff_lines` method for outputting a discussion diff took in already highlighted lines, which meant it wasn't reuseable for truncating ANY lines. In the way it was used, it also meant that for any email truncation, the whole diff was being highlighted before being truncated, meaning wasted time highlighting lines that wouldn't even be used (granted, they were being memoized, so perhaps this wasn't that great of an issue). I refactored truncation away from highlighting, in order to truncate formatted diffs for text templates in email, using `>`s to designate each line, but otherwise retaining the parsing already done to create `diff_lines`. Additionally, while notes on merge requests or commits had already been tested, there was no existing test for notes on a diff on an MR or commit. Added mailer tests for such, and a unit test for truncating diff lines. --- app/models/discussion.rb | 12 ++-- app/views/discussions/_diff_with_notes.html.haml | 2 +- .../notify/_note_mr_or_commit_email.html.haml | 9 +-- app/views/notify/_simple_diff.text.erb | 4 ++ app/views/notify/note_commit_email.html.haml | 6 +- app/views/notify/note_commit_email.text.erb | 2 + .../notify/note_merge_request_email.html.haml | 6 +- app/views/notify/note_merge_request_email.text.erb | 2 + lib/gitlab/diff/file.rb | 13 +++- spec/mailers/notify_spec.rb | 80 +++++++++++++++++++++- spec/models/discussion_spec.rb | 25 +++++++ 11 files changed, 139 insertions(+), 22 deletions(-) create mode 100644 app/views/notify/_simple_diff.text.erb diff --git a/app/models/discussion.rb b/app/models/discussion.rb index de06c13481a..486bfd2c766 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -25,7 +25,12 @@ class Discussion to: :last_resolved_note, allow_nil: true - delegate :blob, :highlighted_diff_lines, to: :diff_file, allow_nil: true + delegate :blob, + :highlighted_diff_lines, + :text_parsed_diff_lines, + + to: :diff_file, + allow_nil: true def self.for_notes(notes) notes.group_by(&:discussion_id).values.map { |notes| new(notes) } @@ -162,18 +167,15 @@ class Discussion def truncated_diff_lines prev_lines = [] - highlighted_diff_lines.each do |line| + diff_file.diff_lines.each do |line| if line.meta? prev_lines.clear else prev_lines << line - break if for_line?(line) - prev_lines.shift if prev_lines.length >= NUMBER_OF_TRUNCATED_DIFF_LINES end end - prev_lines end diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml index 3a95a652810..06493ba0105 100644 --- a/app/views/discussions/_diff_with_notes.html.haml +++ b/app/views/discussions/_diff_with_notes.html.haml @@ -9,7 +9,7 @@ %table - discussions = { discussion.original_line_code => discussion } = render partial: "projects/diffs/line", - collection: discussion.truncated_diff_lines, + collection: discussion.highlighted_diff_lines(discussion.truncated_diff_lines), as: :line, locals: { diff_file: diff_file, discussions: discussions, diff --git a/app/views/notify/_note_mr_or_commit_email.html.haml b/app/views/notify/_note_mr_or_commit_email.html.haml index 3e2046aa9cf..7033842b557 100644 --- a/app/views/notify/_note_mr_or_commit_email.html.haml +++ b/app/views/notify/_note_mr_or_commit_email.html.haml @@ -1,15 +1,16 @@ = content_for :head do = stylesheet_link_tag 'mailers/highlighted_diff_email' -- if note.diff_note? && note.diff_file - = link_to note.diff_file.file_path, @target_url, class: 'details' +- if @note.diff_note? && @note.diff_file + on + = link_to @note.diff_file.file_path, @target_url, class: 'details' \: %table = render partial: "projects/diffs/line", - collection: discussion.truncated_diff_lines, + collection: @discussion.highlighted_diff_lines(@discussion.truncated_diff_lines), as: :line, - locals: { diff_file: note.diff_file, + locals: { diff_file: @note.diff_file, plain: true, email: true } diff --git a/app/views/notify/_simple_diff.text.erb b/app/views/notify/_simple_diff.text.erb new file mode 100644 index 00000000000..a5a796bc168 --- /dev/null +++ b/app/views/notify/_simple_diff.text.erb @@ -0,0 +1,4 @@ +<% lines = @discussion.truncated_diff_lines %> +<% @discussion.text_parsed_diff_lines(lines).each do |line| %> + <%= line %> +<% end %> diff --git a/app/views/notify/note_commit_email.html.haml b/app/views/notify/note_commit_email.html.haml index f4293a3aa50..17dcf36689f 100644 --- a/app/views/notify/note_commit_email.html.haml +++ b/app/views/notify/note_commit_email.html.haml @@ -1,7 +1,5 @@ %p.details New comment for Commit = @commit.short_id - on - = render partial: 'note_mr_or_commit_email', - locals: { note: @note, - discussion: @discussion} + + = render 'note_mr_or_commit_email' diff --git a/app/views/notify/note_commit_email.text.erb b/app/views/notify/note_commit_email.text.erb index a1ef9858021..715e58af61c 100644 --- a/app/views/notify/note_commit_email.text.erb +++ b/app/views/notify/note_commit_email.text.erb @@ -2,6 +2,8 @@ New comment for Commit <%= @commit.short_id %> <%= url_for(namespace_project_commit_url(@note.project.namespace, @note.project, id: @commit.id, anchor: "note_#{@note.id}")) %> +<%= render 'simple_diff' if @discussion %> + <% if current_application_settings.email_author_in_body %> <%= @note.author_name %> wrote: <% end %> diff --git a/app/views/notify/note_merge_request_email.html.haml b/app/views/notify/note_merge_request_email.html.haml index 75250b0383d..b7758f191dc 100644 --- a/app/views/notify/note_merge_request_email.html.haml +++ b/app/views/notify/note_merge_request_email.html.haml @@ -1,7 +1,5 @@ %p.details New comment for Merge Request = @merge_request.to_reference - on - = render partial: 'note_mr_or_commit_email', - locals: { note: @note, - discussion: @discussion} + + = render 'note_mr_or_commit_email' diff --git a/app/views/notify/note_merge_request_email.text.erb b/app/views/notify/note_merge_request_email.text.erb index 42d29b34ebc..d24e15af91f 100644 --- a/app/views/notify/note_merge_request_email.text.erb +++ b/app/views/notify/note_merge_request_email.text.erb @@ -2,6 +2,8 @@ New comment for Merge Request <%= @merge_request.to_reference %> <%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, anchor: "note_#{@note.id}")) %> +<%= render 'simple_diff' if @discussion %> + <% if current_application_settings.email_author_in_body %> <%= @note.author_name %> wrote: <% end %> diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index c6bf25b5874..9b60102947a 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -69,15 +69,22 @@ module Gitlab diff_refs.try(:head_sha) end - attr_writer :highlighted_diff_lines + attr_writer :highlighted_diff_lines, :text_parsed_diff_lines # Array of Gitlab::Diff::Line objects def diff_lines @diff_lines ||= Gitlab::Diff::Parser.new.parse(raw_diff.each_line).to_a end - def highlighted_diff_lines - @highlighted_diff_lines ||= Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight + def highlighted_diff_lines(lines = self) + @highlighted_diff_lines ||= Gitlab::Diff::Highlight.new(lines, repository: self.repository).highlight + end + + def text_parsed_diff_lines(lines) + @text_parsed_diff_lines ||= + lines.map do | line | + "> " + line.text + end end # Array[<Hash>] with right/left keys that contains Gitlab::Diff::Line objects which text is hightlighted diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 932a5dc4862..a80eb114c17 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -646,7 +646,6 @@ describe Notify do before(:each) { allow(note).to receive(:noteable).and_return(merge_request) } subject { Notify.note_merge_request_email(recipient.id, note.id) } - it_behaves_like 'a note email' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do let(:model) { merge_request } @@ -686,6 +685,85 @@ describe Notify do end end end + + context 'items that are noteable, emails for a note on a diff' do + let(:note_author) { create(:user, name: 'author_name') } + + before :each do + allow(Note).to receive(:find).with(note.id).and_return(note) + end + + shared_examples 'a note email on a diff' do | model | + let(:note) { create(model, project: project, author: note_author) } + + it "includes diffs with character-level highlighting" do + is_expected.to have_body_text /\<span class='idiff left right'>vars = {<\/span>/ + end + + it 'contains a link to the diff file' do + is_expected.to have_body_text /#{note.diff_file.file_path}/ + end + + it_behaves_like 'it should have Gmail Actions links' + + it 'is sent as the author' do + sender = subject.header[:from].addrs[0] + expect(sender.display_name).to eq(note_author.name) + expect(sender.address).to eq(gitlab_sender) + end + + it 'is sent to the given recipient' do + is_expected.to deliver_to recipient.notification_email + end + + it 'contains the message from the note' do + is_expected.to have_body_text /#{note.note}/ + end + + it 'does not contain note author' do + is_expected.not_to have_body_text /wrote\:/ + end + + context 'when enabled email_author_in_body' do + before do + allow_any_instance_of(ApplicationSetting).to receive(:email_author_in_body).and_return(true) + end + + it 'contains a link to note author' do + is_expected.to have_body_text note.author_name + is_expected.to have_body_text /wrote\:/ + end + end + end + + describe 'on a commit' do + let(:commit) { project.commit } + let(:note) { create(:diff_note_on_commit) } + + subject { Notify.note_commit_email(recipient.id, note.id) } + + it_behaves_like 'a note email on a diff', :diff_note_on_commit + # it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + # let(:model) { commit } + # end + it_behaves_like 'it should show Gmail Actions View Commit link' + it_behaves_like 'a user cannot unsubscribe through footer link' + end + + describe 'on a merge request' do + let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + let(:note) { create(:diff_note_on_merge_request) } + + subject { Notify.note_merge_request_email(recipient.id, note.id) } + + it_behaves_like 'a note email on a diff', :diff_note_on_merge_request + # it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + # let(:model) { merge_request } + # end + it_behaves_like 'it should show Gmail Actions View Merge request link' + it_behaves_like 'an unsubscribeable thread' + end + end end context 'for a group' do diff --git a/spec/models/discussion_spec.rb b/spec/models/discussion_spec.rb index 0142706d140..d4b1f480c56 100644 --- a/spec/models/discussion_spec.rb +++ b/spec/models/discussion_spec.rb @@ -590,4 +590,29 @@ describe Discussion, model: true do end end end + + describe "#truncated_diff_lines" do + let(:truncated_lines) { subject.truncated_diff_lines } + + context "when diff is greater than allowed number of truncated diff lines " do + let(:initial_line_count) { subject.diff_file.diff_lines.count } + let(:truncated_line_count) { truncated_lines.count } + + it "returns fewer lines" do + expect(initial_line_count).to be > described_class::NUMBER_OF_TRUNCATED_DIFF_LINES + + expect(truncated_line_count).to be <= described_class::NUMBER_OF_TRUNCATED_DIFF_LINES + end + end + + context "when some diff lines are meta" do + let(:initial_meta_lines?) { subject.diff_file.diff_lines.any?(&:meta?) } + let(:truncated_meta_lines?) { truncated_lines.any?(&:meta?) } + + it "returns no meta lines" do + expect(initial_meta_lines?).to be true + expect(truncated_meta_lines?).to be false + end + end + end end -- cgit v1.2.1 From a761c59a6bfc4d66649910d01e4c8412bb0b40ec Mon Sep 17 00:00:00 2001 From: hhoopes <heidih@gmail.com> Date: Wed, 31 Aug 2016 10:18:26 -0600 Subject: Add keyword arguments to truncated_diff method * Added keyword arguments to truncated_diff_lines method to allow for using highlighting or not (html templates vs. text) * Tweaked templates for consistency and format appropriateness --- app/models/discussion.rb | 7 ++++--- app/views/discussions/_diff_with_notes.html.haml | 2 +- app/views/notify/_note_message.text.erb | 5 +++++ app/views/notify/_note_mr_or_commit_email.html.haml | 11 +++++++---- app/views/notify/_note_mr_or_commit_email.text.erb | 8 ++++++++ app/views/notify/_simple_diff.text.erb | 5 ++--- app/views/notify/note_commit_email.html.haml | 3 --- app/views/notify/note_commit_email.text.erb | 13 +++---------- app/views/notify/note_merge_request_email.html.haml | 3 --- app/views/notify/note_merge_request_email.text.erb | 13 +++---------- lib/gitlab/diff/file.rb | 11 ++--------- spec/mailers/notify_spec.rb | 8 +------- spec/models/discussion_spec.rb | 4 ++-- 13 files changed, 38 insertions(+), 55 deletions(-) create mode 100644 app/views/notify/_note_message.text.erb create mode 100644 app/views/notify/_note_mr_or_commit_email.text.erb diff --git a/app/models/discussion.rb b/app/models/discussion.rb index 486bfd2c766..9bd37fe6d89 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -27,7 +27,7 @@ class Discussion delegate :blob, :highlighted_diff_lines, - :text_parsed_diff_lines, + :diff_lines, to: :diff_file, allow_nil: true @@ -164,10 +164,11 @@ class Discussion end # Returns an array of at most 16 highlighted lines above a diff note - def truncated_diff_lines + def truncated_diff_lines(highlight: true) + initial_lines = highlight ? highlighted_diff_lines : diff_lines prev_lines = [] - diff_file.diff_lines.each do |line| + initial_lines.each do |line| if line.meta? prev_lines.clear else diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml index 06493ba0105..5c667e4842b 100644 --- a/app/views/discussions/_diff_with_notes.html.haml +++ b/app/views/discussions/_diff_with_notes.html.haml @@ -9,7 +9,7 @@ %table - discussions = { discussion.original_line_code => discussion } = render partial: "projects/diffs/line", - collection: discussion.highlighted_diff_lines(discussion.truncated_diff_lines), + collection: discussion.highlighted_diff_lines, as: :line, locals: { diff_file: diff_file, discussions: discussions, diff --git a/app/views/notify/_note_message.text.erb b/app/views/notify/_note_message.text.erb new file mode 100644 index 00000000000..f82cbc9a3fc --- /dev/null +++ b/app/views/notify/_note_message.text.erb @@ -0,0 +1,5 @@ +<% if current_application_settings.email_author_in_body %> + <%= @note.author_name %> wrote: +<% end -%> + +<%= @note.note %> diff --git a/app/views/notify/_note_mr_or_commit_email.html.haml b/app/views/notify/_note_mr_or_commit_email.html.haml index 7033842b557..15e92c42b14 100644 --- a/app/views/notify/_note_mr_or_commit_email.html.haml +++ b/app/views/notify/_note_mr_or_commit_email.html.haml @@ -1,17 +1,20 @@ = content_for :head do = stylesheet_link_tag 'mailers/highlighted_diff_email' +New comment + - if @note.diff_note? && @note.diff_file on = link_to @note.diff_file.file_path, @target_url, class: 'details' - \: +\: +- if @discussion %table = render partial: "projects/diffs/line", - collection: @discussion.highlighted_diff_lines(@discussion.truncated_diff_lines), + collection: @discussion.truncated_diff_lines, as: :line, locals: { diff_file: @note.diff_file, - plain: true, - email: true } + plain: true, + email: true } = render 'note_message' diff --git a/app/views/notify/_note_mr_or_commit_email.text.erb b/app/views/notify/_note_mr_or_commit_email.text.erb new file mode 100644 index 00000000000..3dd1b4d4c0e --- /dev/null +++ b/app/views/notify/_note_mr_or_commit_email.text.erb @@ -0,0 +1,8 @@ +<% if @note.diff_note? && @note.diff_file -%> + on <%= @note.diff_file.file_path -%> +<% end -%>: + +<%= url %> + +<%= render 'simple_diff' if @discussion -%> +<%= render 'note_message' %> diff --git a/app/views/notify/_simple_diff.text.erb b/app/views/notify/_simple_diff.text.erb index a5a796bc168..58b0018c0ca 100644 --- a/app/views/notify/_simple_diff.text.erb +++ b/app/views/notify/_simple_diff.text.erb @@ -1,4 +1,3 @@ -<% lines = @discussion.truncated_diff_lines %> -<% @discussion.text_parsed_diff_lines(lines).each do |line| %> - <%= line %> +<% @discussion.truncated_diff_lines(highlight: false).each do |line| %> + <%= "> " + line.text %> <% end %> diff --git a/app/views/notify/note_commit_email.html.haml b/app/views/notify/note_commit_email.html.haml index 17dcf36689f..0a650e3b2ca 100644 --- a/app/views/notify/note_commit_email.html.haml +++ b/app/views/notify/note_commit_email.html.haml @@ -1,5 +1,2 @@ %p.details - New comment for Commit - = @commit.short_id - = render 'note_mr_or_commit_email' diff --git a/app/views/notify/note_commit_email.text.erb b/app/views/notify/note_commit_email.text.erb index 715e58af61c..dc764b61659 100644 --- a/app/views/notify/note_commit_email.text.erb +++ b/app/views/notify/note_commit_email.text.erb @@ -1,11 +1,4 @@ -New comment for Commit <%= @commit.short_id %> +<% url = url_for(namespace_project_commit_url(@note.project.namespace, @note.project, id: @commit.id, anchor: "note_#{@note.id}")) %> -<%= url_for(namespace_project_commit_url(@note.project.namespace, @note.project, id: @commit.id, anchor: "note_#{@note.id}")) %> - -<%= render 'simple_diff' if @discussion %> - -<% if current_application_settings.email_author_in_body %> - <%= @note.author_name %> wrote: -<% end %> - -<%= @note.note %> +New comment for Commit <%= @commit.short_id -%> +<%= render partial: 'note_mr_or_commit_email', locals: { url: url } %> diff --git a/app/views/notify/note_merge_request_email.html.haml b/app/views/notify/note_merge_request_email.html.haml index b7758f191dc..0a650e3b2ca 100644 --- a/app/views/notify/note_merge_request_email.html.haml +++ b/app/views/notify/note_merge_request_email.html.haml @@ -1,5 +1,2 @@ %p.details - New comment for Merge Request - = @merge_request.to_reference - = render 'note_mr_or_commit_email' diff --git a/app/views/notify/note_merge_request_email.text.erb b/app/views/notify/note_merge_request_email.text.erb index d24e15af91f..e33d15daded 100644 --- a/app/views/notify/note_merge_request_email.text.erb +++ b/app/views/notify/note_merge_request_email.text.erb @@ -1,11 +1,4 @@ -New comment for Merge Request <%= @merge_request.to_reference %> +<% url = url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, anchor: "note_#{@note.id}")) %> -<%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, anchor: "note_#{@note.id}")) %> - -<%= render 'simple_diff' if @discussion %> - -<% if current_application_settings.email_author_in_body %> - <%= @note.author_name %> wrote: -<% end %> - -<%= @note.note %> +New comment for Merge Request <%= @merge_request.to_reference -%> +<%= render partial: 'note_mr_or_commit_email', locals: { url: url }%> diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index 9b60102947a..84784aaf2fd 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -76,15 +76,8 @@ module Gitlab @diff_lines ||= Gitlab::Diff::Parser.new.parse(raw_diff.each_line).to_a end - def highlighted_diff_lines(lines = self) - @highlighted_diff_lines ||= Gitlab::Diff::Highlight.new(lines, repository: self.repository).highlight - end - - def text_parsed_diff_lines(lines) - @text_parsed_diff_lines ||= - lines.map do | line | - "> " + line.text - end + def highlighted_diff_lines + @highlighted_diff_lines ||= Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight end # Array[<Hash>] with right/left keys that contains Gitlab::Diff::Line objects which text is hightlighted diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index a80eb114c17..824b516f856 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -697,7 +697,7 @@ describe Notify do let(:note) { create(model, project: project, author: note_author) } it "includes diffs with character-level highlighting" do - is_expected.to have_body_text /\<span class='idiff left right'>vars = {<\/span>/ + is_expected.to have_body_text /<span class=\"p\">}<\/span><\/span>/ end it 'contains a link to the diff file' do @@ -743,9 +743,6 @@ describe Notify do subject { Notify.note_commit_email(recipient.id, note.id) } it_behaves_like 'a note email on a diff', :diff_note_on_commit - # it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do - # let(:model) { commit } - # end it_behaves_like 'it should show Gmail Actions View Commit link' it_behaves_like 'a user cannot unsubscribe through footer link' end @@ -757,9 +754,6 @@ describe Notify do subject { Notify.note_merge_request_email(recipient.id, note.id) } it_behaves_like 'a note email on a diff', :diff_note_on_merge_request - # it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do - # let(:model) { merge_request } - # end it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'an unsubscribeable thread' end diff --git a/spec/models/discussion_spec.rb b/spec/models/discussion_spec.rb index d4b1f480c56..187d0bc0150 100644 --- a/spec/models/discussion_spec.rb +++ b/spec/models/discussion_spec.rb @@ -595,7 +595,7 @@ describe Discussion, model: true do let(:truncated_lines) { subject.truncated_diff_lines } context "when diff is greater than allowed number of truncated diff lines " do - let(:initial_line_count) { subject.diff_file.diff_lines.count } + let(:initial_line_count) { subject.diff_lines.count } let(:truncated_line_count) { truncated_lines.count } it "returns fewer lines" do @@ -606,7 +606,7 @@ describe Discussion, model: true do end context "when some diff lines are meta" do - let(:initial_meta_lines?) { subject.diff_file.diff_lines.any?(&:meta?) } + let(:initial_meta_lines?) { subject.diff_lines.any?(&:meta?) } let(:truncated_meta_lines?) { truncated_lines.any?(&:meta?) } it "returns no meta lines" do -- cgit v1.2.1 From c0931722509bba7b26ce46777b6c71f976bc4b7b Mon Sep 17 00:00:00 2001 From: hhoopes <heidih@gmail.com> Date: Fri, 2 Sep 2016 22:25:53 -0600 Subject: Clean up rubocop complaint --- spec/mailers/notify_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 824b516f856..76ea5f6be31 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -693,7 +693,7 @@ describe Notify do allow(Note).to receive(:find).with(note.id).and_return(note) end - shared_examples 'a note email on a diff' do | model | + shared_examples 'a note email on a diff' do |model| let(:note) { create(model, project: project, author: note_author) } it "includes diffs with character-level highlighting" do -- cgit v1.2.1 From 938de31167e262cb3eb952f6be1797b4c891d34c Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Mon, 14 Nov 2016 13:25:25 +0000 Subject: Fix CHANGELOG --- CHANGELOG.md | 1 - .../hoopes-gitlab-ce-21027-add-diff-hunks-to-notification-emails.yml | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/hoopes-gitlab-ce-21027-add-diff-hunks-to-notification-emails.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 56f749e94ac..549336e4dff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -797,7 +797,6 @@ entry. ## 8.11.0 (2016-08-22) - Use test coverage value from the latest successful pipeline in badge. !5862 - - Add git diff context to notifications of new notes on merge requests !5855 (hoopes) - Add test coverage report badge. !5708 - Remove the http_parser.rb dependency by removing the tinder gem. !5758 (tbalthazar) - Add Koding (online IDE) integration diff --git a/changelogs/unreleased/hoopes-gitlab-ce-21027-add-diff-hunks-to-notification-emails.yml b/changelogs/unreleased/hoopes-gitlab-ce-21027-add-diff-hunks-to-notification-emails.yml new file mode 100644 index 00000000000..73d8a52e001 --- /dev/null +++ b/changelogs/unreleased/hoopes-gitlab-ce-21027-add-diff-hunks-to-notification-emails.yml @@ -0,0 +1,4 @@ +--- +title: Add git diff context to notifications of new notes on merge requests +merge_request: +author: Heidi Hoopes -- cgit v1.2.1 From 260749e1648956479cf8a814862fcfa21f8f71fd Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" <lbennett@gitlab.com> Date: Wed, 23 Nov 2016 15:54:35 +0000 Subject: Add `.find` poly --- app/assets/javascripts/extensions/array.js | 8 ----- app/assets/javascripts/extensions/array.js.es6 | 24 ++++++++++++++ spec/javascripts/extensions/array_spec.js | 23 ------------- spec/javascripts/extensions/array_spec.js.es6 | 46 ++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 31 deletions(-) delete mode 100644 app/assets/javascripts/extensions/array.js create mode 100644 app/assets/javascripts/extensions/array.js.es6 delete mode 100644 spec/javascripts/extensions/array_spec.js create mode 100644 spec/javascripts/extensions/array_spec.js.es6 diff --git a/app/assets/javascripts/extensions/array.js b/app/assets/javascripts/extensions/array.js deleted file mode 100644 index fc6c130113d..00000000000 --- a/app/assets/javascripts/extensions/array.js +++ /dev/null @@ -1,8 +0,0 @@ -/* eslint-disable no-extend-native, func-names, space-before-function-paren, semi, space-infix-ops, max-len */ -Array.prototype.first = function() { - return this[0]; -} - -Array.prototype.last = function() { - return this[this.length-1]; -} diff --git a/app/assets/javascripts/extensions/array.js.es6 b/app/assets/javascripts/extensions/array.js.es6 new file mode 100644 index 00000000000..717566a4715 --- /dev/null +++ b/app/assets/javascripts/extensions/array.js.es6 @@ -0,0 +1,24 @@ +/* eslint-disable no-extend-native, func-names, space-before-function-paren, semi, space-infix-ops, max-len */ +Array.prototype.first = function() { + return this[0]; +} + +Array.prototype.last = function() { + return this[this.length-1]; +} + +Array.prototype.find = Array.prototype.find || function(predicate, ...args) { + if (!this) throw new TypeError('Array.prototype.find called on null or undefined'); + if (typeof predicate !== 'function') throw new TypeError('predicate must be a function'); + + const list = Object(this); + const thisArg = args[1]; + let value = {}; + + for (let i = 0; i < list.length; i += 1) { + value = list[i]; + if (predicate.call(thisArg, value, i, list)) return value; + } + + return undefined; +}; diff --git a/spec/javascripts/extensions/array_spec.js b/spec/javascripts/extensions/array_spec.js deleted file mode 100644 index c56e6c7789b..00000000000 --- a/spec/javascripts/extensions/array_spec.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable space-before-function-paren, no-var, padded-blocks */ - -/*= require extensions/array */ - -(function() { - describe('Array extensions', function() { - describe('first', function() { - return it('returns the first item', function() { - var arr; - arr = [0, 1, 2, 3, 4, 5]; - return expect(arr.first()).toBe(0); - }); - }); - return describe('last', function() { - return it('returns the last item', function() { - var arr; - arr = [0, 1, 2, 3, 4, 5]; - return expect(arr.last()).toBe(5); - }); - }); - }); - -}).call(this); diff --git a/spec/javascripts/extensions/array_spec.js.es6 b/spec/javascripts/extensions/array_spec.js.es6 new file mode 100644 index 00000000000..2ec759c8e80 --- /dev/null +++ b/spec/javascripts/extensions/array_spec.js.es6 @@ -0,0 +1,46 @@ +/* eslint-disable space-before-function-paren, no-var, padded-blocks */ + +/*= require extensions/array */ + +(function() { + describe('Array extensions', function() { + describe('first', function() { + return it('returns the first item', function() { + var arr; + arr = [0, 1, 2, 3, 4, 5]; + return expect(arr.first()).toBe(0); + }); + }); + describe('last', function() { + return it('returns the last item', function() { + var arr; + arr = [0, 1, 2, 3, 4, 5]; + return expect(arr.last()).toBe(5); + }); + }); + + describe('find', function () { + beforeEach(() => { + this.arr = [0, 1, 2, 3, 4, 5]; + }); + + it('returns the item that first passes the predicate function', () => { + expect(this.arr.find(item => item === 2)).toBe(2); + }); + + it('returns undefined if no items pass the predicate function', () => { + expect(this.arr.find(item => item === 6)).not.toBeDefined(); + }); + + it('error when called on undefined or null', () => { + expect(Array.prototype.find.bind(undefined, item => item === 1)).toThrow(); + expect(Array.prototype.find.bind(null, item => item === 1)).toThrow(); + }); + + it('error when predicate is not a function', () => { + expect(Array.prototype.find.bind(this.arr, 1)).toThrow(); + }); + }); + }); + +}).call(this); -- cgit v1.2.1 From 5981a0f365b9a312b87bfcebfb0d9365cb2f9c9a Mon Sep 17 00:00:00 2001 From: Nick Thomas <nick@gitlab.com> Date: Fri, 25 Nov 2016 16:22:29 +0000 Subject: Update GitLab Workhorse to v1.0.1 v1.0.0 was mistakenly tagged with a lightweight, rather than annotated, tag, which caused compiled versions of workhorse to wrongly report their version. --- GITLAB_WORKHORSE_VERSION | 2 +- changelogs/unreleased/workhorse-v1-0-1.yml | 4 ++++ doc/install/installation.md | 2 +- doc/update/8.13-to-8.14.md | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/workhorse-v1-0-1.yml diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 3eefcb9dd5b..7dea76edb3d 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -1.0.0 +1.0.1 diff --git a/changelogs/unreleased/workhorse-v1-0-1.yml b/changelogs/unreleased/workhorse-v1-0-1.yml new file mode 100644 index 00000000000..c26c2d45b1d --- /dev/null +++ b/changelogs/unreleased/workhorse-v1-0-1.yml @@ -0,0 +1,4 @@ +--- +title: Update GitLab Workhorse to v1.0.1 +merge_request: 7759 +author: diff --git a/doc/install/installation.md b/doc/install/installation.md index dabd69d7466..ee02d6024d9 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -403,7 +403,7 @@ If you are not using Linux you may have to run `gmake` instead of cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git cd gitlab-workhorse - sudo -u git -H git checkout v1.0.0 + sudo -u git -H git checkout v1.0.1 sudo -u git -H make ### Initialize Database and Activate Advanced Features diff --git a/doc/update/8.13-to-8.14.md b/doc/update/8.13-to-8.14.md index 46ea19d11d0..a0e895773ce 100644 --- a/doc/update/8.13-to-8.14.md +++ b/doc/update/8.13-to-8.14.md @@ -84,7 +84,7 @@ GitLab 8.1. ```bash cd /home/git/gitlab-workhorse sudo -u git -H git fetch --all -sudo -u git -H git checkout v1.0.0 +sudo -u git -H git checkout v1.0.1 sudo -u git -H make ``` -- cgit v1.2.1 From 117a078cbb22ab3890e519655fde2b2c314c6b46 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Fri, 25 Nov 2016 10:35:25 -0600 Subject: use standard gitlab namespace regex for group name validation --- app/views/shared/_group_form.html.haml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index fc1753ca082..0bc851b4256 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -13,8 +13,9 @@ .input-group-addon = root_url = f.text_field :path, placeholder: 'open-source', class: 'form-control', - autofocus: local_assigns[:autofocus] || false, pattern: "[a-zA-Z0-9-_\\.]+", - required: true, title: 'Please choose a group name with no special characters.' + autofocus: local_assigns[:autofocus] || false, required: true, + pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE, + title: 'Please choose a group name with no special characters.' - if @group.persisted? .alert.alert-warning.prepend-top-10 -- cgit v1.2.1 From 752d72f87635c1f43f3334d7e2dd96ab77eeba7d Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Fri, 25 Nov 2016 17:38:51 +0100 Subject: Add docs for pipeline coverage [ci skip] --- doc/api/pipelines.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md index 6455c333faf..82351ae688f 100644 --- a/doc/api/pipelines.md +++ b/doc/api/pipelines.md @@ -41,7 +41,8 @@ Example of response "started_at": null, "finished_at": null, "committed_at": null, - "duration": null + "duration": null, + "coverage": "30.0" }, { "id": 48, @@ -64,7 +65,8 @@ Example of response "started_at": null, "finished_at": null, "committed_at": null, - "duration": null + "duration": null, + "coverage": null } ] ``` @@ -110,7 +112,8 @@ Example of response "started_at": null, "finished_at": "2016-08-11T11:32:35.145Z", "committed_at": null, - "duration": null + "duration": null, + "coverage": "30.0" } ``` @@ -155,7 +158,8 @@ Example of response "started_at": null, "finished_at": null, "committed_at": null, - "duration": null + "duration": null, + "coverage": null } ``` @@ -200,7 +204,8 @@ Response: "started_at": null, "finished_at": "2016-08-11T11:32:35.145Z", "committed_at": null, - "duration": null + "duration": null, + "coverage": null } ``` @@ -245,7 +250,8 @@ Response: "started_at": null, "finished_at": "2016-08-11T11:32:35.145Z", "committed_at": null, - "duration": null + "duration": null, + "coverage": null } ``` -- cgit v1.2.1 From 2ec4d167b68005ece13ed789c03f9816d90e8239 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Tue, 15 Nov 2016 18:28:14 +0100 Subject: Refactor issuable description and metadata form sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- app/views/shared/issuable/_form.html.haml | 48 +-------- .../shared/issuable/form/_description.html.haml | 15 +++ app/views/shared/issuable/form/_metadata.html.haml | 38 +++++++ doc/development/limit_ee_conflicts.md | 111 +++++++++++++++------ 4 files changed, 138 insertions(+), 74 deletions(-) create mode 100644 app/views/shared/issuable/form/_description.html.haml create mode 100644 app/views/shared/issuable/form/_metadata.html.haml diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 9b9ad510444..3d515a05d46 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -16,20 +16,9 @@ = render 'shared/issuable/form/template_selector', issuable: issuable = render 'shared/issuable/form/title', issuable: issuable, form: form -.form-group.detail-page-description - = form.label :description, 'Description', class: 'control-label' - .col-sm-10 += render 'shared/issuable/form/description', issuable: issuable, form: form - = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - = render 'projects/zen', f: form, attr: :description, - classes: 'note-textarea', - placeholder: "Write a comment or drag your files here...", - supports_slash_commands: !issuable.persisted? - = render 'projects/notes/hints', supports_slash_commands: !issuable.persisted? - .clearfix - .error-alert - -- if issuable.is_a?(Issue) +- if issuable.respond_to?(:confidential) .form-group .col-sm-offset-2.col-sm-10 .checkbox @@ -37,38 +26,7 @@ = form.check_box :confidential This issue is confidential and should only be visible to team members with at least Reporter access. -- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) - - has_due_date = issuable.has_attribute?(:due_date) - %hr - .row - %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") } - .form-group.issue-assignee - = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" - .col-sm-10{ class: ("col-lg-8" if has_due_date) } - .issuable-form-select-holder - - if issuable.assignee_id - = form.hidden_field :assignee_id - = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", - placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} }) - .form-group.issue-milestone - = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" - .col-sm-10{ class: ("col-lg-8" if has_due_date) } - .issuable-form-select-holder - = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" - .form-group - - has_labels = @labels && @labels.any? - = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" - = form.hidden_field :label_ids, multiple: true, value: '' - .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } - .issuable-form-select-holder - = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false}, dropdown_title: "Select label" - - if has_due_date - .col-lg-6 - .form-group - = form.label :due_date, "Due date", class: "control-label" - .col-sm-10 - .issuable-form-select-holder - = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date" += render 'shared/issuable/form/metadata', issuable: issuable, form: form - if issuable.can_move?(current_user) %hr diff --git a/app/views/shared/issuable/form/_description.html.haml b/app/views/shared/issuable/form/_description.html.haml new file mode 100644 index 00000000000..dbace9ce401 --- /dev/null +++ b/app/views/shared/issuable/form/_description.html.haml @@ -0,0 +1,15 @@ +- issuable = local_assigns.fetch(:issuable) +- form = local_assigns.fetch(:form) + +.form-group.detail-page-description + = form.label :description, 'Description', class: 'control-label' + .col-sm-10 + + = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do + = render 'projects/zen', f: form, attr: :description, + classes: 'note-textarea', + placeholder: "Write a comment or drag your files here...", + supports_slash_commands: !issuable.persisted? + = render 'projects/notes/hints', supports_slash_commands: !issuable.persisted? + .clearfix + .error-alert diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml new file mode 100644 index 00000000000..a47085230b8 --- /dev/null +++ b/app/views/shared/issuable/form/_metadata.html.haml @@ -0,0 +1,38 @@ +- issuable = local_assigns.fetch(:issuable) + +- return unless can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) + +- has_due_date = issuable.has_attribute?(:due_date) +- has_labels = @labels && @labels.any? +- form = local_assigns.fetch(:form) + +%hr +.row + %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") } + .form-group.issue-assignee + = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" + .col-sm-10{ class: ("col-lg-8" if has_due_date) } + .issuable-form-select-holder + - if issuable.assignee_id + = form.hidden_field :assignee_id + = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", + placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} }) + .form-group.issue-milestone + = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" + .col-sm-10{ class: ("col-lg-8" if has_due_date) } + .issuable-form-select-holder + = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" + .form-group + - has_labels = @labels && @labels.any? + = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" + = form.hidden_field :label_ids, multiple: true, value: '' + .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } + .issuable-form-select-holder + = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false}, dropdown_title: "Select label" + - if has_due_date + .col-lg-6 + .form-group + = form.label :due_date, "Due date", class: "control-label" + .col-sm-10 + .issuable-form-select-holder + = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date" diff --git a/doc/development/limit_ee_conflicts.md b/doc/development/limit_ee_conflicts.md index b7e6387838e..568dedf1669 100644 --- a/doc/development/limit_ee_conflicts.md +++ b/doc/development/limit_ee_conflicts.md @@ -143,109 +143,162 @@ to resolve when you add the indentation to the equation. For instance this kind of thing: ```haml +.form-group.detail-page-description + = form.label :description, 'Description', class: 'control-label' + .col-sm-10 + = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do + = render 'projects/zen', f: form, attr: :description, + classes: 'note-textarea', + placeholder: "Write a comment or drag your files here...", + supports_slash_commands: !issuable.persisted? + = render 'projects/notes/hints', supports_slash_commands: !issuable.persisted? + .clearfix + .error-alert +- if issuable.is_a?(Issue) + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = form.label :confidential do + = form.check_box :confidential + This issue is confidential and should only be visible to team members with at least Reporter access. - if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) - has_due_date = issuable.has_attribute?(:due_date) %hr .row %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") } .form-group.issue-assignee - = f.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" + = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" .col-sm-10{ class: ("col-lg-8" if has_due_date) } .issuable-form-select-holder - if issuable.assignee_id - = f.hidden_field :assignee_id + = form.hidden_field :assignee_id = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} }) .form-group.issue-milestone - = f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" + = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" .col-sm-10{ class: ("col-lg-8" if has_due_date) } .issuable-form-select-holder = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" .form-group - has_labels = @labels && @labels.any? - = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" - = f.hidden_field :label_ids, multiple: true, value: '' + = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" + = form.hidden_field :label_ids, multiple: true, value: '' .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } .issuable-form-select-holder - = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false, show_menu_above: 'true' }, dropdown_title: "Select label" - + = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false }, dropdown_title: "Select label" - if issuable.respond_to?(:weight) + - weight_options = Issue.weight_options + - weight_options.delete(Issue::WEIGHT_ALL) + - weight_options.delete(Issue::WEIGHT_ANY) .form-group - = f.label :label_ids, class: "control-label #{"col-lg-4" if has_due_date}" do + = form.label :label_ids, class: "control-label #{"col-lg-4" if has_due_date}" do Weight .col-sm-10{ class: ("col-lg-8" if has_due_date) } - = f.select :weight, issues_weight_options(issuable.weight, edit: true), { include_blank: true }, - { class: 'select2 js-select2', data: { placeholder: "Select weight" }} - + .issuable-form-select-holder + - if issuable.weight + = form.hidden_field :weight + = dropdown_tag(issuable.weight || "Weight", options: { title: "Select weight", toggle_class: 'js-weight-select js-issuable-form-weight', dropdown_class: "dropdown-menu-selectable dropdown-menu-weight", + placeholder: "Search weight", data: { field_name: "#{issuable.class.model_name.param_key}[weight]" , default_label: "Weight" } }) do + %ul + - weight_options.each do |weight| + %li + %a{href: "#", data: { id: weight, none: weight === Issue::WEIGHT_NONE }, class: ("is-active" if issuable.weight == weight)} + = weight - if has_due_date .col-lg-6 .form-group - = f.label :due_date, "Due date", class: "control-label" + = form.label :due_date, "Due date", class: "control-label" .col-sm-10 .issuable-form-select-holder - = f.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date" + = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date" ``` could be simplified by using partials: ```haml -= render 'metadata_form', issuable: issuable += render 'shared/issuable/form/description', issuable: issuable, form: form + +- if issuable.respond_to?(:confidential) + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = form.label :confidential do + = form.check_box :confidential + This issue is confidential and should only be visible to team members with at least Reporter access. + += render 'shared/issuable/form/metadata', issuable: issuable, form: form ``` -and then the `_metadata_form.html.haml` could be as follows: +and then the `app/views/shared/issuable/form/_metadata.html.haml` could be as follows: ```haml +- issuable = local_assigns.fetch(:issuable) + - return unless can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) - has_due_date = issuable.has_attribute?(:due_date) +- has_labels = @labels && @labels.any? +- form = local_assigns.fetch(:form) + %hr .row %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") } .form-group.issue-assignee - = f.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" + = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" .col-sm-10{ class: ("col-lg-8" if has_due_date) } .issuable-form-select-holder - if issuable.assignee_id - = f.hidden_field :assignee_id + = form.hidden_field :assignee_id = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", - placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} }) + placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} }) .form-group.issue-milestone - = f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" + = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" .col-sm-10{ class: ("col-lg-8" if has_due_date) } .issuable-form-select-holder = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" .form-group - has_labels = @labels && @labels.any? - = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" - = f.hidden_field :label_ids, multiple: true, value: '' + = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" + = form.hidden_field :label_ids, multiple: true, value: '' .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } .issuable-form-select-holder - = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false, show_menu_above: 'true' }, dropdown_title: "Select label" + = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false }, dropdown_title: "Select label" - = render 'weight_form', issuable: issuable, has_due_date: has_due_date + = render "shared/issuable/form/weight", issuable: issuable, form: form - if has_due_date .col-lg-6 .form-group - = f.label :due_date, "Due date", class: "control-label" + = form.label :due_date, "Due date", class: "control-label" .col-sm-10 .issuable-form-select-holder - = f.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date" + = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date" ``` -and then the `_weight_form.html.haml` could be as follows: +and then the `app/views/shared/issuable/form/_weight.html.haml` could be as follows: ```haml +- issuable = local_assigns.fetch(:issuable) + - return unless issuable.respond_to?(:weight) - has_due_date = issuable.has_attribute?(:due_date) +- form = local_assigns.fetch(:form) .form-group - = f.label :label_ids, class: "control-label #{"col-lg-4" if has_due_date}" do + = form.label :label_ids, class: "control-label #{"col-lg-4" if has_due_date}" do Weight .col-sm-10{ class: ("col-lg-8" if has_due_date) } - = f.select :weight, issues_weight_options(issuable.weight, edit: true), { include_blank: true }, - { class: 'select2 js-select2', data: { placeholder: "Select weight" }} + .issuable-form-select-holder + - if issuable.weight + = form.hidden_field :weight + + = weight_dropdown_tag(issuable, toggle_class: 'js-issuable-form-weight') do + %ul + - Issue.weight_options.each do |weight| + %li + %a{ href: '#', data: { id: weight, none: weight === Issue::WEIGHT_NONE }, class: ("is-active" if issuable.weight == weight) } + = weight ``` Note: -- cgit v1.2.1 From d8e21b1bfda544033e4b1830fae7b6f9101279a1 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 25 Nov 2016 17:00:47 +0000 Subject: Adds spinner class --- app/assets/javascripts/environments/components/environment.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index 35e183a9086..84faabf938a 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -181,7 +181,7 @@ <div class="environments-container"> <div class="environments-list-loading text-center" v-if="isLoading"> - <i class="fa fa-spinner spin"></i> + <i class="fa fa-spinner fa-spin"></i> </div> <div class="blank-state blank-state-no-icon" -- cgit v1.2.1 From de0a7378eb0e08ab532a6ba80550abf9fe9db875 Mon Sep 17 00:00:00 2001 From: David Wagner <david@marvid.fr> Date: Fri, 25 Nov 2016 18:26:24 +0100 Subject: Check that both '/help' and '/help/' URLs have the same behaviour The links in the help page may be modified. This new test checks that URLs in this page are absolute and do not depend on the presence of a trailing slash in the URL. Signed-off-by: David Wagner <david@marvid.fr> --- spec/features/help_pages_spec.rb | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb index e2101b333e2..73d03837144 100644 --- a/spec/features/help_pages_spec.rb +++ b/spec/features/help_pages_spec.rb @@ -10,4 +10,28 @@ describe 'Help Pages', feature: true do expect(page).to have_content("ssh-keygen -t rsa -C \"#{@user.email}\"") end end + + describe 'Get the main help page' do + shared_examples_for 'help page' do + it 'prefixes links correctly' do + expect(page).to have_selector('div.documentation-index > ul a[href="/help/api/README.md"]') + end + end + + context 'without a trailing slash' do + before do + visit help_path + end + + it_behaves_like 'help page' + end + + context 'with a trailing slash' do + before do + visit help_path + '/' + end + + it_behaves_like 'help page' + end + end end -- cgit v1.2.1 From a1f53e92caa8a95889c3fee683f8d0b2590228ac Mon Sep 17 00:00:00 2001 From: awhildy <allison@gitlab.com> Date: Fri, 25 Nov 2016 20:19:29 -0800 Subject: [ci skip] UX Guide: add guidance for max height for dropdowns Fix spelling --- doc/development/ux_guide/components.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/development/ux_guide/components.md b/doc/development/ux_guide/components.md index 764c3355714..57e5d03d608 100644 --- a/doc/development/ux_guide/components.md +++ b/doc/development/ux_guide/components.md @@ -91,7 +91,9 @@ TODO: Will update this section when the new filters UI is implemented. ![Dropdown states](img/components-dropdown.png) +### Max size +The max height for dropdowns should target **10-15 items**. If the height of the dropdown is too large, the list becomes very hard to parse and it is easy to visually lose track of the item you are looking for. Usability also suffers as more mouse movement is required, and you have a larger area in which you hijack the scroll away from the page level. While it may initially seem counterintuitive to not show as many items as you can, it is actually quicker and easier to process the information when it is cropped at a reasonable height. --- -- cgit v1.2.1 From b72bd838d76015fb9ef0ab4d8be57a9c8130b744 Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto <gabriel@gitlab.com> Date: Sat, 26 Nov 2016 07:11:05 +0100 Subject: Backporting little groups_helper refactor from gitlab-org/gitlab-ee!904 --- app/helpers/groups_helper.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 75cd9eece5c..19ab059aea6 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -8,11 +8,7 @@ module GroupsHelper group = Group.find_by(path: group) end - if group && group.avatar.present? - group.avatar.url - else - image_path('no_group_avatar.png') - end + group.try(:avatar_url) || image_path('no_group_avatar.png') end def group_title(group, name = nil, url = nil) -- cgit v1.2.1 From 7bd214091dbbbdff46b39e2b4505c64d931fa421 Mon Sep 17 00:00:00 2001 From: Semyon Pupkov <mail@semyonpupkov.com> Date: Sat, 26 Nov 2016 14:36:00 +0500 Subject: Remove unnecessary require_relative calls from finder --- app/finders/issuable_finder.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 6297b2db369..a48f22cee07 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -16,8 +16,6 @@ # label_name: string # sort: string # -require_relative 'projects_finder' - class IssuableFinder NONE = '0' -- cgit v1.2.1 From c47d8ab69e108ef0cb30463fc2f02fbd1d03409b Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" <lbennett@gitlab.com> Date: Sat, 19 Nov 2016 15:40:41 +0000 Subject: Removed leave buttons from settings dropdowns Updated specs --- app/views/layouts/nav/_group_settings.html.haml | 12 ++-------- app/views/layouts/nav/_project.html.haml | 17 ++++---------- .../members/_access_request_buttons.html.haml | 26 +++++++++++++--------- ...e-project-and-leave-group-should-be-buttons.yml | 5 +++++ .../members/last_owner_cannot_leave_group_spec.rb | 4 ++-- .../groups/members/member_leaves_group_spec.rb | 2 +- .../groups/members/user_requests_access_spec.rb | 2 +- ...group_member_cannot_leave_group_project_spec.rb | 2 +- ...uester_cannot_request_access_to_project_spec.rb | 2 +- .../projects/members/member_leaves_project_spec.rb | 2 +- .../members/owner_cannot_leave_project_spec.rb | 4 ++-- 11 files changed, 36 insertions(+), 42 deletions(-) create mode 100644 changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml index c0328fe8842..1579d8f1662 100644 --- a/app/views/layouts/nav/_group_settings.html.haml +++ b/app/views/layouts/nav/_group_settings.html.haml @@ -1,10 +1,8 @@ - if current_user - can_admin_group = can?(current_user, :admin_group, @group) - can_edit = can?(current_user, :admin_group, @group) - - member = @group.members.find_by(user_id: current_user.id) - - can_leave = member && can?(current_user, :destroy_group_member, member) - - if can_admin_group || can_edit || can_leave + - if can_admin_group || can_edit .controls .dropdown.group-settings-dropdown %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'} @@ -14,13 +12,7 @@ - if can_admin_group = nav_link(path: 'groups#projects') do = link_to 'Projects', projects_group_path(@group), title: 'Projects' - - if (can_edit || can_leave) && can_admin_group + - if can_edit && can_admin_group %li.divider - - if can_edit %li = link_to 'Edit Group', edit_group_path(@group) - - if can_leave - %li - = link_to polymorphic_path([:leave, @group, :members]), - data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do - Leave Group diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 99a58bbb676..cc24e51b268 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -6,23 +6,14 @@ = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right - can_edit = can?(current_user, :admin_project, @project) - -# We don't use @project.team.find_member because it searches for group members too... - - member = @project.members.find_by(user_id: current_user.id) - - can_leave = member && can?(current_user, :destroy_project_member, member) = render 'layouts/nav/project_settings', can_edit: can_edit - - if can_edit || can_leave + - if can_edit %li.divider - - if can_edit - %li - = link_to edit_project_path(@project) do - Edit Project - - if can_leave - %li - = link_to polymorphic_path([:leave, @project, :members]), - data: { confirm: leave_confirmation_message(@project) }, method: :delete, title: 'Leave project' do - Leave Project + %li + = link_to edit_project_path(@project) do + Edit Project .scrolling-tabs-container{ class: nav_control_class } .fade-left diff --git a/app/views/shared/members/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml index eff914398bb..e166dfab710 100644 --- a/app/views/shared/members/_access_request_buttons.html.haml +++ b/app/views/shared/members/_access_request_buttons.html.haml @@ -1,10 +1,16 @@ -- if can?(current_user, :request_access, source) - - if requester = source.requesters.find_by(user_id: current_user.id) - = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]), - method: :delete, - data: { confirm: remove_member_message(requester) }, - class: 'btn' - - else - = link_to 'Request Access', polymorphic_path([:request_access, source, :members]), - method: :post, - class: 'btn' +- model_name = source.model_name.to_s.downcase + +- if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id)) + = link_to "Leave #{model_name}", polymorphic_path([:leave, source, :members]), + method: :delete, + data: { confirm: leave_confirmation_message(source) }, + class: 'btn' +- elsif requester = source.requesters.find_by(user_id: current_user.id) + = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]), + method: :delete, + data: { confirm: remove_member_message(requester) }, + class: 'btn' +- elsif source.request_access_enabled && can?(current_user, :request_access, source) + = link_to 'Request Access', polymorphic_path([:request_access, source, :members]), + method: :post, + class: 'btn' diff --git a/changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml b/changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml new file mode 100644 index 00000000000..99dbe4a32a0 --- /dev/null +++ b/changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml @@ -0,0 +1,5 @@ +--- +title: Moved Leave Project and Leave Group buttons to access_request_buttons from + the settings dropdown +merge_request: 7600 +author: diff --git a/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb b/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb index 33bf6d3752f..be60b0489c7 100644 --- a/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb +++ b/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb @@ -10,7 +10,7 @@ feature 'Groups > Members > Last owner cannot leave group', feature: true do visit group_path(group) end - scenario 'user does not see a "Leave Group" link' do - expect(page).not_to have_content 'Leave Group' + scenario 'user does not see a "Leave group" link' do + expect(page).not_to have_content 'Leave group' end end diff --git a/spec/features/groups/members/member_leaves_group_spec.rb b/spec/features/groups/members/member_leaves_group_spec.rb index 3185ff924b9..ac4d94658ae 100644 --- a/spec/features/groups/members/member_leaves_group_spec.rb +++ b/spec/features/groups/members/member_leaves_group_spec.rb @@ -13,7 +13,7 @@ feature 'Groups > Members > Member leaves group', feature: true do end scenario 'user leaves group' do - click_link 'Leave Group' + click_link 'Leave group' expect(current_path).to eq(dashboard_groups_path) expect(group.users.exists?(user.id)).to be_falsey diff --git a/spec/features/groups/members/user_requests_access_spec.rb b/spec/features/groups/members/user_requests_access_spec.rb index d8c9c487996..e4b5ea91bd3 100644 --- a/spec/features/groups/members/user_requests_access_spec.rb +++ b/spec/features/groups/members/user_requests_access_spec.rb @@ -29,7 +29,7 @@ feature 'Groups > Members > User requests access', feature: true do expect(page).to have_content 'Your request for access has been queued for review.' expect(page).to have_content 'Withdraw Access Request' - expect(page).not_to have_content 'Leave Group' + expect(page).not_to have_content 'Leave group' end scenario 'user does not see private projects' do diff --git a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb index 728c0e16361..b483ba4c54c 100644 --- a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb +++ b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb @@ -12,6 +12,6 @@ feature 'Projects > Members > Group member cannot leave group project', feature: end scenario 'user does not see a "Leave project" link' do - expect(page).not_to have_content 'Leave Project' + expect(page).not_to have_content 'Leave project' end end diff --git a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb index 4973e0aee85..bdeeef57273 100644 --- a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb +++ b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Projects > Members > Group requester cannot request access to project', feature: true do +feature 'Projects > Members > Group requester cannot request access to project', feature: true, js: true do let(:user) { create(:user) } let(:owner) { create(:user) } let(:group) { create(:group, :public, :access_requestable) } diff --git a/spec/features/projects/members/member_leaves_project_spec.rb b/spec/features/projects/members/member_leaves_project_spec.rb index 79dec442818..5daa932e4e6 100644 --- a/spec/features/projects/members/member_leaves_project_spec.rb +++ b/spec/features/projects/members/member_leaves_project_spec.rb @@ -11,7 +11,7 @@ feature 'Projects > Members > Member leaves project', feature: true do end scenario 'user leaves project' do - click_link 'Leave Project' + click_link 'Leave project' expect(current_path).to eq(dashboard_projects_path) expect(project.users.exists?(user.id)).to be_falsey diff --git a/spec/features/projects/members/owner_cannot_leave_project_spec.rb b/spec/features/projects/members/owner_cannot_leave_project_spec.rb index 6e948b7a616..b26d55c5d5d 100644 --- a/spec/features/projects/members/owner_cannot_leave_project_spec.rb +++ b/spec/features/projects/members/owner_cannot_leave_project_spec.rb @@ -8,7 +8,7 @@ feature 'Projects > Members > Owner cannot leave project', feature: true do visit namespace_project_path(project.namespace, project) end - scenario 'user does not see a "Leave Project" link' do - expect(page).not_to have_content 'Leave Project' + scenario 'user does not see a "Leave project" link' do + expect(page).not_to have_content 'Leave project' end end -- cgit v1.2.1 From 34c550b054c77699d795cec1daf90b23b2fb7ba5 Mon Sep 17 00:00:00 2001 From: Ryan Harris <harrisryan1@gmail.com> Date: Sat, 26 Nov 2016 16:14:02 -0500 Subject: Fix the width of project avatars in order to adjust alignment within their container element --- app/assets/stylesheets/framework/avatar.scss | 1 + changelogs/unreleased/24999-fix-project-avatar-alignment.yml | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 changelogs/unreleased/24999-fix-project-avatar-alignment.yml diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index ad0d387067f..c0dd1cb3667 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -80,6 +80,7 @@ border-radius: 0; border: none; height: auto; + width: 100%; margin: 0; align-self: center; } diff --git a/changelogs/unreleased/24999-fix-project-avatar-alignment.yml b/changelogs/unreleased/24999-fix-project-avatar-alignment.yml new file mode 100644 index 00000000000..7af812e7359 --- /dev/null +++ b/changelogs/unreleased/24999-fix-project-avatar-alignment.yml @@ -0,0 +1,4 @@ +--- +title: Adjust the width of project avatars to fix alignment within their container +merge_request: +author: Ryan Harris -- cgit v1.2.1 From 084d90acc4a58fbbb97fb524ada947e06d604202 Mon Sep 17 00:00:00 2001 From: Ryan Harris <harrisryan1@gmail.com> Date: Sat, 26 Nov 2016 23:34:19 -0500 Subject: Changes project dashboard tabs to sentence casing --- app/views/dashboard/_projects_head.html.haml | 6 +++--- changelogs/unreleased/25002-sentence-case-dashboard-tabs.yml | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/25002-sentence-case-dashboard-tabs.yml diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index f7abad54286..48b0fd504f4 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -4,13 +4,13 @@ %ul.nav-links = nav_link(page: [dashboard_projects_path, root_path]) do = link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do - Your Projects + Your projects = nav_link(page: starred_dashboard_projects_path) do = link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do - Starred Projects + Starred projects = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do = link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do - Explore Projects + Explore projects .nav-controls = form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| diff --git a/changelogs/unreleased/25002-sentence-case-dashboard-tabs.yml b/changelogs/unreleased/25002-sentence-case-dashboard-tabs.yml new file mode 100644 index 00000000000..cc8b0e28277 --- /dev/null +++ b/changelogs/unreleased/25002-sentence-case-dashboard-tabs.yml @@ -0,0 +1,4 @@ +--- +title: Sentence cased the nav tab headers on the project dashboard page +merge_request: +author: Ryan Harris -- cgit v1.2.1 From 5a5e03b5aa01c837ae4e1fcc02f7f9def960e98b Mon Sep 17 00:00:00 2001 From: awhildy <allison@gitlab.com> Date: Mon, 21 Nov 2016 13:30:55 -0800 Subject: [ci skip] UX Guide: Anchor hover guidance include color change Primary and secondary links should be dark blue on hover Update anchor image to dark blue for secondary Clean up markdown Fix anchorlinks image --- doc/development/ux_guide/components.md | 18 +++++------------- .../ux_guide/img/components-anchorlinks.png | Bin 19948 -> 30089 bytes 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/doc/development/ux_guide/components.md b/doc/development/ux_guide/components.md index 57e5d03d608..8e51edd23ef 100644 --- a/doc/development/ux_guide/components.md +++ b/doc/development/ux_guide/components.md @@ -43,7 +43,7 @@ Primary links are blue in their rest state. Secondary links (such as the time st #### Hover -An underline should always be added on hover. A gray link becomes blue on hover. +On hover, an underline should be added and the color should change. Both the primary and secondary link should become the darker blue color on hover. #### Focus @@ -72,9 +72,7 @@ Secondary buttons are for alternative commands. They should be conveyed by a bu ### Icon and text treatment Text should be in sentence case, where only the first word is capitalized. "Create issue" is correct, not "Create Issue". Buttons should only contain an icon or a text, not both. ->>> -TODO: Rationalize this. Ensure that we still believe this. ->>> +> TODO: Rationalize this. Ensure that we still believe this. ### Colors Follow the color guidance on the [basics](basics.md#color) page. The default color treatment is the white/grey button. @@ -85,9 +83,7 @@ Follow the color guidance on the [basics](basics.md#color) page. The default col Dropdowns are used to allow users to choose one (or many) options from a list of options. If this list of options is more 20, there should generally be a way to search through and filter the options (see the complex filter dropdowns below.) ->>> -TODO: Will update this section when the new filters UI is implemented. ->>> +> TODO: Will update this section when the new filters UI is implemented. ![Dropdown states](img/components-dropdown.png) @@ -166,9 +162,7 @@ Cover blocks are generally used to create a heading element for a page, such as ## Panels ->>> -TODO: Catalog how we are currently using panels and rationalize how they relate to alerts ->>> +> TODO: Catalog how we are currently using panels and rationalize how they relate to alerts ![Panels](img/components-panels.png) @@ -176,9 +170,7 @@ TODO: Catalog how we are currently using panels and rationalize how they relate ## Alerts ->>> -TODO: Catalog how we are currently using alerts ->>> +> TODO: Catalog how we are currently using alerts ![Alerts](img/components-alerts.png) diff --git a/doc/development/ux_guide/img/components-anchorlinks.png b/doc/development/ux_guide/img/components-anchorlinks.png index 7dd6a8a3876..4a9c730566c 100644 Binary files a/doc/development/ux_guide/img/components-anchorlinks.png and b/doc/development/ux_guide/img/components-anchorlinks.png differ -- cgit v1.2.1 From 3761a0c50ea13b86152417a5e659b30879cb16b1 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Sun, 27 Nov 2016 16:24:43 +0100 Subject: Extend pipelines factory with transient config attribute --- spec/factories/ci/pipelines.rb | 21 ++++++++------------- spec/services/ci/process_pipeline_service_spec.rb | 2 +- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb index 23585db6ebd..1735791f644 100644 --- a/spec/factories/ci/pipelines.rb +++ b/spec/factories/ci/pipelines.rb @@ -21,21 +21,16 @@ FactoryGirl.define do end factory :ci_pipeline do - after(:build) do |pipeline| - allow(pipeline).to receive(:ci_yaml_file) do - File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) - end - end - end - - factory(:ci_pipeline_with_yaml) do - transient { yaml nil } + transient { config nil } after(:build) do |pipeline, evaluator| - raise ArgumentError unless evaluator.yaml - - allow(pipeline).to receive(:ci_yaml_file) - .and_return(YAML.dump(evaluator.yaml)) + allow(pipeline).to receive(:ci_yaml_file) do + if evaluator.config + YAML.dump(evaluator.config) + else + File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + end + end end end end diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index 306943a5488..ebb11166964 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -295,7 +295,7 @@ describe Ci::ProcessPipelineService, services: true do context 'when there are builds that are not created yet' do let(:pipeline) do - create(:ci_pipeline_with_yaml, yaml: config) + create(:ci_pipeline, config: config) end let(:config) do -- cgit v1.2.1 From 1e66f35c560b5a0067851cabb792fe23281cfd51 Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Mon, 28 Nov 2016 16:50:08 +0800 Subject: Pass `--load-images=no` to PhantomJS via Capybara/Poltergeist We were unintentionally hitting `gravatar.com` whenever a test that used Poltergeist was run. This was certainly wasting their resources and slowing down our tests even further, for no reason. --- features/support/capybara.rb | 10 +++++++++- spec/support/capybara.rb | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/features/support/capybara.rb b/features/support/capybara.rb index dae0d0f918c..47372df152d 100644 --- a/features/support/capybara.rb +++ b/features/support/capybara.rb @@ -6,7 +6,15 @@ timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 15 Capybara.javascript_driver = :poltergeist Capybara.register_driver :poltergeist do |app| - Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout, window_size: [1366, 768]) + Capybara::Poltergeist::Driver.new( + app, + js_errors: true, + timeout: timeout, + window_size: [1366, 768], + phantomjs_options: [ + '--load-images=no' + ] + ) end Capybara.default_max_wait_time = timeout diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index e1f90e17cce..16d5f2bf0b8 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -7,7 +7,15 @@ timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10 Capybara.javascript_driver = :poltergeist Capybara.register_driver :poltergeist do |app| - Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout, window_size: [1366, 768]) + Capybara::Poltergeist::Driver.new( + app, + js_errors: true, + timeout: timeout, + window_size: [1366, 768], + phantomjs_options: [ + '--load-images=no' + ] + ) end Capybara.default_max_wait_time = timeout -- cgit v1.2.1 From 13ad9a745a392e0bf0cedd0e1f318c1acee9b969 Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Mon, 28 Nov 2016 13:08:14 +0800 Subject: Speed up Project security access specs Prior, every single test was creating four `ProjectMember` objects, each of which created one `User` record, even though each test only used _one_ of those Users, if any. Now each test only creates the single user record it needs, if it needs one. This shaves minutes off of each spec file changed here. --- .../security/project/internal_access_spec.rb | 517 ++++++++++----------- .../security/project/private_access_spec.rb | 445 +++++++++--------- .../security/project/public_access_spec.rb | 515 ++++++++++---------- spec/support/matchers/access_matchers.rb | 35 +- 4 files changed, 747 insertions(+), 765 deletions(-) diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index b6acc509342..1897c8119d2 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -5,19 +5,6 @@ describe "Internal Project Access", feature: true do let(:project) { create(:project, :internal) } - let(:owner) { project.owner } - let(:master) { create(:user) } - let(:developer) { create(:user) } - let(:reporter) { create(:user) } - let(:guest) { create(:user) } - - before do - project.team << [master, :master] - project.team << [developer, :developer] - project.team << [reporter, :reporter] - project.team << [guest, :guest] - end - describe "Project should be internal" do describe '#internal?' do subject { project.internal? } @@ -28,213 +15,213 @@ describe "Internal Project Access", feature: true do describe "GET /:project_path" do subject { namespace_project_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/tree/master" do subject { namespace_project_tree_path(project.namespace, project, project.repository.root_ref) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/commits/master" do subject { namespace_project_commits_path(project.namespace, project, project.repository.root_ref, limit: 1) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/commit/:sha" do subject { namespace_project_commit_path(project.namespace, project, project.repository.commit) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/compare" do subject { namespace_project_compare_index_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/project_members" do subject { namespace_project_project_members_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :visitor } - it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:visitor) } + it { is_expected.to be_denied_for(:external) } end describe "GET /:project_path/blob" do let(:commit) { project.repository.commit } subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore')) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/edit" do subject { edit_namespace_project_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_denied_for developer } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_denied_for(:developer).of(project) } + it { is_expected.to be_denied_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/deploy_keys" do subject { namespace_project_deploy_keys_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_denied_for developer } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_denied_for(:developer).of(project) } + it { is_expected.to be_denied_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/issues" do subject { namespace_project_issues_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/issues/:id/edit" do let(:issue) { create(:issue, project: project) } subject { edit_namespace_project_issue_path(project.namespace, project, issue) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/snippets" do subject { namespace_project_snippets_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/snippets/new" do subject { new_namespace_project_snippet_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/merge_requests" do subject { namespace_project_merge_requests_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/merge_requests/new" do subject { new_namespace_project_merge_request_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_denied_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/branches" do @@ -245,15 +232,15 @@ describe "Internal Project Access", feature: true do allow_any_instance_of(Project).to receive(:branches).and_return([]) end - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/tags" do @@ -264,58 +251,58 @@ describe "Internal Project Access", feature: true do allow_any_instance_of(Project).to receive(:tags).and_return([]) end - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/hooks" do subject { namespace_project_hooks_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_denied_for developer } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_denied_for(:developer).of(project) } + it { is_expected.to be_denied_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/pipelines" do subject { namespace_project_pipelines_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/pipelines/:id" do let(:pipeline) { create(:ci_pipeline, project: project) } subject { namespace_project_pipeline_path(project.namespace, project, pipeline) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/builds" do @@ -324,29 +311,29 @@ describe "Internal Project Access", feature: true do context "when allowed for public and internal" do before { project.update(public_builds: true) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end context "when disallowed for public and internal" do before { project.update(public_builds: false) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end end @@ -358,73 +345,73 @@ describe "Internal Project Access", feature: true do context "when allowed for public and internal" do before { project.update(public_builds: true) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end context "when disallowed for public and internal" do before { project.update(public_builds: false) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end end describe "GET /:project_path/environments" do subject { namespace_project_environments_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/environments/:id" do let(:environment) { create(:environment, project: project) } subject { namespace_project_environment_path(project.namespace, project, environment) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/environments/new" do subject { new_namespace_project_environment_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_denied_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/container_registry" do @@ -435,14 +422,14 @@ describe "Internal Project Access", feature: true do subject { namespace_project_container_registry_index_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end end diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index 79417c769a8..290ddb4c6dd 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -5,19 +5,6 @@ describe "Private Project Access", feature: true do let(:project) { create(:project, :private) } - let(:owner) { project.owner } - let(:master) { create(:user) } - let(:developer) { create(:user) } - let(:reporter) { create(:user) } - let(:guest) { create(:user) } - - before do - project.team << [master, :master] - project.team << [developer, :developer] - project.team << [reporter, :reporter] - project.team << [guest, :guest] - end - describe "Project should be private" do describe '#private?' do subject { project.private? } @@ -28,185 +15,185 @@ describe "Private Project Access", feature: true do describe "GET /:project_path" do subject { namespace_project_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/tree/master" do subject { namespace_project_tree_path(project.namespace, project, project.repository.root_ref) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/commits/master" do subject { namespace_project_commits_path(project.namespace, project, project.repository.root_ref, limit: 1) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/commit/:sha" do subject { namespace_project_commit_path(project.namespace, project, project.repository.commit) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/compare" do subject { namespace_project_compare_index_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/project_members" do subject { namespace_project_project_members_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/blob" do let(:commit) { project.repository.commit } subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore'))} - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/edit" do subject { edit_namespace_project_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_denied_for developer } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_denied_for(:developer).of(project) } + it { is_expected.to be_denied_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/deploy_keys" do subject { namespace_project_deploy_keys_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_denied_for developer } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_denied_for(:developer).of(project) } + it { is_expected.to be_denied_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/issues" do subject { namespace_project_issues_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/issues/:id/edit" do let(:issue) { create(:issue, project: project) } subject { edit_namespace_project_issue_path(project.namespace, project, issue) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/snippets" do subject { namespace_project_snippets_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/merge_requests" do subject { namespace_project_merge_requests_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/branches" do @@ -217,15 +204,15 @@ describe "Private Project Access", feature: true do allow_any_instance_of(Project).to receive(:branches).and_return([]) end - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/tags" do @@ -236,72 +223,72 @@ describe "Private Project Access", feature: true do allow_any_instance_of(Project).to receive(:tags).and_return([]) end - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/hooks" do subject { namespace_project_hooks_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_denied_for developer } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_denied_for(:developer).of(project) } + it { is_expected.to be_denied_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/pipelines" do subject { namespace_project_pipelines_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/pipelines/:id" do let(:pipeline) { create(:ci_pipeline, project: project) } subject { namespace_project_pipeline_path(project.namespace, project, pipeline) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/builds" do subject { namespace_project_builds_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/builds/:id" do @@ -309,58 +296,58 @@ describe "Private Project Access", feature: true do let(:build) { create(:ci_build, pipeline: pipeline) } subject { namespace_project_build_path(project.namespace, project, build.id) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/environments" do subject { namespace_project_environments_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/environments/:id" do let(:environment) { create(:environment, project: project) } subject { namespace_project_environment_path(project.namespace, project, environment) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/environments/new" do subject { new_namespace_project_environment_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_denied_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/container_registry" do @@ -371,14 +358,14 @@ describe "Private Project Access", feature: true do subject { namespace_project_container_registry_index_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end end diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index 985663e7c98..bed9e92fcb6 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -5,19 +5,6 @@ describe "Public Project Access", feature: true do let(:project) { create(:project, :public) } - let(:owner) { project.owner } - let(:master) { create(:user) } - let(:developer) { create(:user) } - let(:reporter) { create(:user) } - let(:guest) { create(:user) } - - before do - project.team << [master, :master] - project.team << [developer, :developer] - project.team << [reporter, :reporter] - project.team << [guest, :guest] - end - describe "Project should be public" do describe '#public?' do subject { project.public? } @@ -28,114 +15,114 @@ describe "Public Project Access", feature: true do describe "GET /:project_path" do subject { namespace_project_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :external } - it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:external) } + it { is_expected.to be_allowed_for(:visitor) } end describe "GET /:project_path/tree/master" do subject { namespace_project_tree_path(project.namespace, project, project.repository.root_ref) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :external } - it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:external) } + it { is_expected.to be_allowed_for(:visitor) } end describe "GET /:project_path/commits/master" do subject { namespace_project_commits_path(project.namespace, project, project.repository.root_ref, limit: 1) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :external } - it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:external) } + it { is_expected.to be_allowed_for(:visitor) } end describe "GET /:project_path/commit/:sha" do subject { namespace_project_commit_path(project.namespace, project, project.repository.commit) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :external } - it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:external) } + it { is_expected.to be_allowed_for(:visitor) } end describe "GET /:project_path/compare" do subject { namespace_project_compare_index_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :external } - it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:external) } + it { is_expected.to be_allowed_for(:visitor) } end describe "GET /:project_path/project_members" do subject { namespace_project_project_members_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :visitor } - it { is_expected.to be_allowed_for :external } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:visitor) } + it { is_expected.to be_allowed_for(:external) } end describe "GET /:project_path/pipelines" do subject { namespace_project_pipelines_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :external } - it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:external) } + it { is_expected.to be_allowed_for(:visitor) } end describe "GET /:project_path/pipelines/:id" do let(:pipeline) { create(:ci_pipeline, project: project) } subject { namespace_project_pipeline_path(project.namespace, project, pipeline) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :external } - it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:external) } + it { is_expected.to be_allowed_for(:visitor) } end describe "GET /:project_path/builds" do @@ -144,29 +131,29 @@ describe "Public Project Access", feature: true do context "when allowed for public" do before { project.update(public_builds: true) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :external } - it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:external) } + it { is_expected.to be_allowed_for(:visitor) } end context "when disallowed for public" do before { project.update(public_builds: false) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end end @@ -178,73 +165,73 @@ describe "Public Project Access", feature: true do context "when allowed for public" do before { project.update(public_builds: true) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :external } - it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:external) } + it { is_expected.to be_allowed_for(:visitor) } end context "when disallowed for public" do before { project.update(public_builds: false) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end end describe "GET /:project_path/environments" do subject { namespace_project_environments_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/environments/:id" do let(:environment) { create(:environment, project: project) } subject { namespace_project_environment_path(project.namespace, project, environment) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/environments/new" do subject { new_namespace_project_environment_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_denied_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/blob" do @@ -252,127 +239,127 @@ describe "Public Project Access", feature: true do subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore')) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:visitor) } end describe "GET /:project_path/edit" do subject { edit_namespace_project_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_denied_for developer } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_denied_for(:developer).of(project) } + it { is_expected.to be_denied_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/deploy_keys" do subject { namespace_project_deploy_keys_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_denied_for developer } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_denied_for(:developer).of(project) } + it { is_expected.to be_denied_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/issues" do subject { namespace_project_issues_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :external } - it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:external) } + it { is_expected.to be_allowed_for(:visitor) } end describe "GET /:project_path/issues/:id/edit" do let(:issue) { create(:issue, project: project) } subject { edit_namespace_project_issue_path(project.namespace, project, issue) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/snippets" do subject { namespace_project_snippets_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :external } - it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:external) } + it { is_expected.to be_allowed_for(:visitor) } end describe "GET /:project_path/snippets/new" do subject { new_namespace_project_snippet_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/merge_requests" do subject { namespace_project_merge_requests_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :external } - it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:external) } + it { is_expected.to be_allowed_for(:visitor) } end describe "GET /:project_path/merge_requests/new" do subject { new_namespace_project_merge_request_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_denied_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/branches" do @@ -383,15 +370,15 @@ describe "Public Project Access", feature: true do allow_any_instance_of(Project).to receive(:branches).and_return([]) end - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :external } - it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:external) } + it { is_expected.to be_allowed_for(:visitor) } end describe "GET /:project_path/tags" do @@ -402,29 +389,29 @@ describe "Public Project Access", feature: true do allow_any_instance_of(Project).to receive(:tags).and_return([]) end - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :external } - it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:external) } + it { is_expected.to be_allowed_for(:visitor) } end describe "GET /:project_path/hooks" do subject { namespace_project_hooks_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_denied_for developer } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_denied_for(:developer).of(project) } + it { is_expected.to be_denied_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/container_registry" do @@ -435,14 +422,14 @@ describe "Public Project Access", feature: true do subject { namespace_project_container_registry_index_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :external } - it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:external) } + it { is_expected.to be_allowed_for(:visitor) } end end diff --git a/spec/support/matchers/access_matchers.rb b/spec/support/matchers/access_matchers.rb index 0497e391860..691d7e05f57 100644 --- a/spec/support/matchers/access_matchers.rb +++ b/spec/support/matchers/access_matchers.rb @@ -7,7 +7,7 @@ module AccessMatchers extend RSpec::Matchers::DSL include Warden::Test::Helpers - def emulate_user(user) + def emulate_user(user, project = nil) case user when :user login_as(create(:user)) @@ -18,6 +18,18 @@ module AccessMatchers when :external login_as(create(:user, external: true)) when User + login_as(user) + when :owner + raise ArgumentError, "cannot emulate owner without project" unless project + + login_as(project.owner) + when *Gitlab::Access.sym_options.keys + raise ArgumentError, "cannot emulate user #{user} without project" unless project + + role = user + user = create(:user) + project.public_send(:"add_#{role}", user) + login_as(user) else raise ArgumentError, "cannot emulate user #{user}" @@ -26,8 +38,7 @@ module AccessMatchers def description_for(user, type) if user.kind_of?(User) - # User#inspect displays too much information for RSpec's description - # messages + # User#inspect displays too much information for RSpec's descriptions "be #{type} for the specified user" else "be #{type} for #{user}" @@ -36,21 +47,31 @@ module AccessMatchers matcher :be_allowed_for do |user| match do |url| - emulate_user(user) - visit url + emulate_user(user, @project) + visit(url) + status_code != 404 && current_path != new_user_session_path end + chain :of do |project| + @project = project + end + description { description_for(user, 'allowed') } end matcher :be_denied_for do |user| match do |url| - emulate_user(user) - visit url + emulate_user(user, @project) + visit(url) + status_code == 404 || current_path == new_user_session_path end + chain :of do |project| + @project = project + end + description { description_for(user, 'denied') } end end -- cgit v1.2.1 From a4e41107005c538e0d6be342a2ba626f25816d37 Mon Sep 17 00:00:00 2001 From: Semyon Pupkov <mail@semyonpupkov.com> Date: Sun, 27 Nov 2016 22:35:44 +0500 Subject: Refactor issuable_filters_present to reduce duplications https://gitlab.com/gitlab-org/gitlab-ce/issues/23546 --- app/helpers/issuables_helper.rb | 20 +++++++++++++++----- app/views/shared/issuable/_filter.html.haml | 4 ++-- .../issuable_filters_present-refactor.yml | 4 ++++ spec/helpers/issuables_helper_spec.rb | 21 +++++++++++++++++++++ 4 files changed, 42 insertions(+), 7 deletions(-) create mode 100644 changelogs/unreleased/issuable_filters_present-refactor.yml diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 8bebda07787..6584aa3edd5 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -143,6 +143,20 @@ module IssuablesHelper end end + def issuable_filter_params + [ + :search, + :author_id, + :assignee_id, + :milestone_title, + :label_name + ] + end + + def issuable_filter_present? + issuable_filter_params.any? { |k| params.key?(k) } + end + private def assigned_issuables_count(assignee, issuable_type, state) @@ -165,13 +179,9 @@ module IssuablesHelper end end - def issuable_filters_present - params[:search] || params[:author_id] || params[:assignee_id] || params[:milestone_title] || params[:label_name] - end - def issuables_count_for_state(issuable_type, state) issuables_finder = public_send("#{issuable_type}_finder") - + params = issuables_finder.params.merge(state: state) finder = issuables_finder.class.new(issuables_finder.current_user, params) diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index b7e5e928993..e3503981afe 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -29,9 +29,9 @@ .filter-item.inline.labels-filter = render "shared/issuable/label_dropdown", selected: finder.labels.select(:title).uniq, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" } - - if issuable_filters_present + - if issuable_filter_present? .filter-item.inline.reset-filters - %a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search])} Reset filters + %a{href: page_filter_path(without: issuable_filter_params)} Reset filters .pull-right - if boards_page diff --git a/changelogs/unreleased/issuable_filters_present-refactor.yml b/changelogs/unreleased/issuable_filters_present-refactor.yml new file mode 100644 index 00000000000..c131f9cb68e --- /dev/null +++ b/changelogs/unreleased/issuable_filters_present-refactor.yml @@ -0,0 +1,4 @@ +--- +title: Refactor issuable_filters_present to reduce duplications +merge_request: 7776 +author: Semyon Pupkov diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index 62cc10f579a..a4f08dc4af0 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -114,4 +114,25 @@ describe IssuablesHelper do end end end + + describe '#issuable_filter_present?' do + it 'returns true when any key is present' do + allow(helper).to receive(:params).and_return( + ActionController::Parameters.new(milestone_title: 'Velit consectetur asperiores natus delectus.', + project_id: 'gitlabhq', + scope: 'all') + ) + + expect(helper.issuable_filter_present?).to be_truthy + end + + it 'returns false when no key is present' do + allow(helper).to receive(:params).and_return( + ActionController::Parameters.new(project_id: 'gitlabhq', + scope: 'all') + ) + + expect(helper.issuable_filter_present?).to be_falsey + end + end end -- cgit v1.2.1 From d9a2093e7e6b0d532131b18dc57c240f5b4a7c55 Mon Sep 17 00:00:00 2001 From: Adam Niedzielski <adamsunday@gmail.com> Date: Mon, 28 Nov 2016 12:03:53 +0100 Subject: Prevent error when submitting a merge request and pipeline is not defined --- .../projects/merge_requests_controller.rb | 2 +- app/models/merge_request.rb | 2 +- .../projects/merge_requests/_new_submit.html.haml | 8 +++--- app/views/projects/merge_requests/_show.html.haml | 5 ++-- ...rror-undefined-method-size-for-nil-nilclass.yml | 4 +++ .../merge_requests/_new_submit.html.haml_spec.rb | 31 ++++++++++++++++++++++ 6 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 changelogs/unreleased/24860-actionview-template-error-undefined-method-size-for-nil-nilclass.yml create mode 100644 spec/views/projects/merge_requests/_new_submit.html.haml_spec.rb diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index e24a670631f..a2225cc8343 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -564,7 +564,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def define_pipelines_vars @pipelines = @merge_request.all_pipelines @pipeline = @merge_request.pipeline - @statuses = @pipeline.statuses.relevant if @pipeline.present? + @statuses_count = @pipeline.present? ? @pipeline.statuses.relevant.count : 0 end def define_new_vars diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 69c6aa700d6..38d8c15e6b0 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -781,7 +781,7 @@ class MergeRequest < ActiveRecord::Base end def all_pipelines - return unless source_project + return Ci::Pipeline.none unless source_project @all_pipelines ||= source_project.pipelines .where(sha: all_commits_sha, ref: source_branch) diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 9c6f562f7db..4a08ed045f4 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -34,10 +34,11 @@ = link_to url_for(params), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do Pipelines %span.badge= @pipelines.size + - if @pipeline.present? %li.builds-tab = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do Builds - %span.badge= @statuses.size + %span.badge= @statuses_count %li.diffs-tab = link_to url_for(params.merge(action: 'new_diffs')), data: {target: 'div#diffs', action: 'new/diffs', toggle: 'tab'} do Changes @@ -48,9 +49,10 @@ = render "projects/merge_requests/show/commits" #diffs.diffs.tab-pane - # This tab is always loaded via AJAX - - if @pipelines.any? + - if @pipeline.present? #builds.builds.tab-pane = render "projects/merge_requests/show/builds" + - if @pipelines.any? #pipelines.pipelines.tab-pane = render "projects/merge_requests/show/pipelines" @@ -65,5 +67,5 @@ :javascript var merge_request = new MergeRequest({ action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}", - buildsLoaded: "#{@pipelines.any? ? 'true' : 'false'}" + buildsLoaded: "#{@pipeline.present? ? 'true' : 'false'}" }); diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index a497f418c7c..0e2975bd551 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -59,15 +59,16 @@ = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do Commits %span.badge= @commits_count - - if @pipeline + - if @pipelines.any? %li.pipelines-tab = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do Pipelines %span.badge= @pipelines.size + - if @pipeline.present? %li.builds-tab = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#builds', action: 'builds', toggle: 'tab' } do Builds - %span.badge= @statuses.size + %span.badge= @statuses_count %li.diffs-tab = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do Changes diff --git a/changelogs/unreleased/24860-actionview-template-error-undefined-method-size-for-nil-nilclass.yml b/changelogs/unreleased/24860-actionview-template-error-undefined-method-size-for-nil-nilclass.yml new file mode 100644 index 00000000000..4b4aea79380 --- /dev/null +++ b/changelogs/unreleased/24860-actionview-template-error-undefined-method-size-for-nil-nilclass.yml @@ -0,0 +1,4 @@ +--- +title: Prevent error when submitting a merge request and pipeline is not defined +merge_request: 7707 +author: diff --git a/spec/views/projects/merge_requests/_new_submit.html.haml_spec.rb b/spec/views/projects/merge_requests/_new_submit.html.haml_spec.rb new file mode 100644 index 00000000000..4f698a34ab5 --- /dev/null +++ b/spec/views/projects/merge_requests/_new_submit.html.haml_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe 'projects/merge_requests/_new_submit.html.haml', :view do + let(:merge_request) { create(:merge_request) } + let!(:pipeline) { create(:ci_empty_pipeline) } + + before do + controller.prepend_view_path('app/views/projects') + + assign(:merge_request, merge_request) + assign(:commits, merge_request.commits) + assign(:project, merge_request.target_project) + + allow(view).to receive(:can?).and_return(true) + allow(view).to receive(:url_for).and_return('#') + allow(view).to receive(:current_user).and_return(merge_request.author) + end + + context 'when there are pipelines for merge request but no pipeline for last commit' do + before do + assign(:pipelines, Ci::Pipeline.all) + assign(:pipeline, nil) + end + + it 'shows <<Pipelines>> tab and hides <<Builds>> tab' do + render + expect(rendered).to have_text('Pipelines 1') + expect(rendered).not_to have_text('Builds') + end + end +end -- cgit v1.2.1 From 9e6cdc64741583ed0db74485892c1970ff960eab Mon Sep 17 00:00:00 2001 From: Adam Niedzielski <adamsunday@gmail.com> Date: Mon, 28 Nov 2016 13:00:42 +0100 Subject: Revert "Pass correct tag target to post-receive hook when creating tag via UI" This reverts commit ae51774bc45d2e15ccc61b01a30d1b588f179f85. --- app/models/repository.rb | 13 +++---------- spec/models/repository_spec.rb | 22 ---------------------- 2 files changed, 3 insertions(+), 32 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index bf136ccdb6c..5e831f84879 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -196,18 +196,11 @@ class Repository options = { message: message, tagger: user_to_committer(user) } if message - rugged.tags.create(tag_name, target, options) - tag = find_tag(tag_name) - - GitHooksService.new.execute(user, path_to_repo, oldrev, tag.target, ref) do - # we already created a tag, because we need tag SHA to pass correct - # values to hooks + GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do + rugged.tags.create(tag_name, target, options) end - tag - rescue GitHooksService::PreReceiveError - rugged.tags.delete(tag_name) - raise + find_tag(tag_name) end def rm_branch(user, branch_name) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 04afb8ebc98..214bf478d19 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1303,28 +1303,6 @@ describe Repository, models: true do repository.add_tag(user, '8.5', 'master', 'foo') end - it 'does not create a tag when a pre-hook fails' do - allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) - - expect do - repository.add_tag(user, '8.5', 'master', 'foo') - end.to raise_error(GitHooksService::PreReceiveError) - - repository.expire_tags_cache - expect(repository.find_tag('8.5')).to be_nil - end - - it 'passes tag SHA to hooks' do - spy = GitHooksService.new - allow(GitHooksService).to receive(:new).and_return(spy) - allow(spy).to receive(:execute).and_call_original - - tag = repository.add_tag(user, '8.5', 'master', 'foo') - - expect(spy).to have_received(:execute). - with(anything, anything, anything, tag.target, anything) - end - it 'returns a Gitlab::Git::Tag object' do tag = repository.add_tag(user, '8.5', 'master', 'foo') -- cgit v1.2.1 From cf58271e11f6704523be5211ecfb2d02ae1091fe Mon Sep 17 00:00:00 2001 From: Adam Niedzielski <adamsunday@gmail.com> Date: Mon, 28 Nov 2016 15:04:51 +0100 Subject: Pass tag SHA to post-receive hook when tag is created via UI We only know the tag SHA after we create the tag. This means that we pass a different value to the hooks that happen before creating the tag, and a different value to the hooks that happen after creating the tag. This is not an ideal situation, but it is a trade-off we decided to make. For discussion of the alternatives please refer to https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7700#note_18982873 "pre-receive" and "update" hooks always get the SHA of the commit that the tag points to. "post-receive" gets the tag SHA if it is an annotated tag or the commit SHA if it is an lightweight tag. Currently we always create annotated tags if UI is used. --- app/models/repository.rb | 5 +++-- app/services/git_hooks_service.rb | 6 +++-- ...-developer-access-can-no-longer-create-tags.yml | 4 ++++ spec/models/repository_spec.rb | 26 ++++++++++++++++++++++ 4 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/24813-project-members-with-developer-access-can-no-longer-create-tags.yml diff --git a/app/models/repository.rb b/app/models/repository.rb index 5e831f84879..e2e7d08abac 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -196,8 +196,9 @@ class Repository options = { message: message, tagger: user_to_committer(user) } if message - GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do - rugged.tags.create(tag_name, target, options) + GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do |service| + raw_tag = rugged.tags.create(tag_name, target, options) + service.newrev = raw_tag.target_id end find_tag(tag_name) diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb index 172bd85dade..6cd3908d43a 100644 --- a/app/services/git_hooks_service.rb +++ b/app/services/git_hooks_service.rb @@ -1,6 +1,8 @@ class GitHooksService PreReceiveError = Class.new(StandardError) + attr_accessor :oldrev, :newrev, :ref + def execute(user, repo_path, oldrev, newrev, ref) @repo_path = repo_path @user = Gitlab::GlId.gl_id(user) @@ -16,7 +18,7 @@ class GitHooksService end end - yield + yield self run_hook('post-receive') end @@ -25,6 +27,6 @@ class GitHooksService def run_hook(name) hook = Gitlab::Git::Hook.new(name, @repo_path) - hook.trigger(@user, @oldrev, @newrev, @ref) + hook.trigger(@user, oldrev, newrev, ref) end end diff --git a/changelogs/unreleased/24813-project-members-with-developer-access-can-no-longer-create-tags.yml b/changelogs/unreleased/24813-project-members-with-developer-access-can-no-longer-create-tags.yml new file mode 100644 index 00000000000..9254db40742 --- /dev/null +++ b/changelogs/unreleased/24813-project-members-with-developer-access-can-no-longer-create-tags.yml @@ -0,0 +1,4 @@ +--- +title: Pass tag SHA to post-receive hook when tag is created via UI +merge_request: 7700 +author: diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 214bf478d19..b797d19161d 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1308,6 +1308,32 @@ describe Repository, models: true do expect(tag).to be_a(Gitlab::Git::Tag) end + + it 'passes commit SHA to pre-receive and update hooks,\ + and tag SHA to post-receive hook' do + pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', repository.path_to_repo) + update_hook = Gitlab::Git::Hook.new('update', repository.path_to_repo) + post_receive_hook = Gitlab::Git::Hook.new('post-receive', repository.path_to_repo) + + allow(Gitlab::Git::Hook).to receive(:new). + and_return(pre_receive_hook, update_hook, post_receive_hook) + + allow(pre_receive_hook).to receive(:trigger).and_call_original + allow(update_hook).to receive(:trigger).and_call_original + allow(post_receive_hook).to receive(:trigger).and_call_original + + tag = repository.add_tag(user, '8.5', 'master', 'foo') + + commit_sha = repository.commit('master').id + tag_sha = tag.target + + expect(pre_receive_hook).to have_received(:trigger). + with(anything, anything, commit_sha, anything) + expect(update_hook).to have_received(:trigger). + with(anything, anything, commit_sha, anything) + expect(post_receive_hook).to have_received(:trigger). + with(anything, anything, tag_sha, anything) + end end context 'with an invalid target' do -- cgit v1.2.1 From 61abf53a622ad31f266f0eec1695fcb2e2bd27e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= <alejorro70@gmail.com> Date: Mon, 28 Nov 2016 12:26:28 -0300 Subject: Update CHANGELOG.md for 8.14.1 [ci skip] --- CHANGELOG.md | 20 ++++++++++++++++++++ ...rces-in-administrator-settings-enable-disable.yml | 4 ---- ...ess-the-Orange-button-on-Merge-request-screen.yml | 4 ---- .../24739-collapsed-build-list-sorting.yml | 4 ---- ...9-last-deployment-call-on-nil-environment-fix.yml | 4 ---- ...n-projects-pipelinessettingscontroller-update.yml | 4 ---- .../24863-mrs-without-discussions-are-mergeable.yml | 4 ---- .../Last-minute-CI-Style-tweaks-for-8-14.yml | 4 ---- .../unreleased/disable-calendar-deselection.yml | 4 ---- .../fix-build-without-trace-exceptions.yml | 4 ---- .../unreleased/fix-cycle-analytics-plan-issue.yml | 4 ---- .../unreleased/fix_sidekiq_stats_in_admin_area.yml | 4 ---- changelogs/unreleased/issue-boards-dragging-fix.yml | 4 ---- changelogs/unreleased/zj-upgrade-grape.yml | 4 ---- 14 files changed, 20 insertions(+), 52 deletions(-) delete mode 100644 changelogs/unreleased/24161-non-intuitive-buttons-for-import-sources-in-administrator-settings-enable-disable.yml delete mode 100644 changelogs/unreleased/24266-Afraid-to-press-the-Orange-button-on-Merge-request-screen.yml delete mode 100644 changelogs/unreleased/24739-collapsed-build-list-sorting.yml delete mode 100644 changelogs/unreleased/24779-last-deployment-call-on-nil-environment-fix.yml delete mode 100644 changelogs/unreleased/24804-wrong-render-index-should-be-render-show-in-projects-pipelinessettingscontroller-update.yml delete mode 100644 changelogs/unreleased/24863-mrs-without-discussions-are-mergeable.yml delete mode 100644 changelogs/unreleased/Last-minute-CI-Style-tweaks-for-8-14.yml delete mode 100644 changelogs/unreleased/disable-calendar-deselection.yml delete mode 100644 changelogs/unreleased/fix-build-without-trace-exceptions.yml delete mode 100644 changelogs/unreleased/fix-cycle-analytics-plan-issue.yml delete mode 100644 changelogs/unreleased/fix_sidekiq_stats_in_admin_area.yml delete mode 100644 changelogs/unreleased/issue-boards-dragging-fix.yml delete mode 100644 changelogs/unreleased/zj-upgrade-grape.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 549336e4dff..18b3755d365 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 8.14.1 (2016-11-28) + +- Fix deselecting calendar days on contribution graph. !6453 (ClemMakesApps) +- Update grape entity to 0.6.0. !7491 +- If Build running change accept merge request when build succeeds button from orange to blue. !7577 +- Changed import sources buttons to checkboxes. !7598 (Luke "Jared" Bennett) +- Last minute CI Style tweaks for 8.14. !7643 +- Fix exceptions when loading build trace. !7658 +- Fix wrong template rendered when CI/CD settings aren't update successfully. !7665 +- fixes last_deployment call environment is nil. !7671 +- Sort builds by name within pipeline graph. !7681 +- Correctly determine mergeability of MR with no discussions. +- Sidekiq stats in the admin area will now show correctly on different platforms. (blackst0ne) +- Fixed issue boards dragging card removing random issues. +- Fix information disclosure in `Projects::BlobController#update`. +- Fix missing access checks on issue lookup using IssuableFinder. +- Replace issue access checks with use of IssuableFinder. +- Non members cannot create labels through the API. +- Fix cycle analytics plan stage when commits are missing. + ## 8.14.0 (2016-11-22) - Use separate email-token for incoming email and revert back the inactive feature. !5914 diff --git a/changelogs/unreleased/24161-non-intuitive-buttons-for-import-sources-in-administrator-settings-enable-disable.yml b/changelogs/unreleased/24161-non-intuitive-buttons-for-import-sources-in-administrator-settings-enable-disable.yml deleted file mode 100644 index 1404748e83e..00000000000 --- a/changelogs/unreleased/24161-non-intuitive-buttons-for-import-sources-in-administrator-settings-enable-disable.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Changed import sources buttons to checkboxes -merge_request: 7598 -author: Luke "Jared" Bennett diff --git a/changelogs/unreleased/24266-Afraid-to-press-the-Orange-button-on-Merge-request-screen.yml b/changelogs/unreleased/24266-Afraid-to-press-the-Orange-button-on-Merge-request-screen.yml deleted file mode 100644 index 28ca20c7dcc..00000000000 --- a/changelogs/unreleased/24266-Afraid-to-press-the-Orange-button-on-Merge-request-screen.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: If Build running change accept merge request when build succeeds button from orange to blue -merge_request: 7577 -author: diff --git a/changelogs/unreleased/24739-collapsed-build-list-sorting.yml b/changelogs/unreleased/24739-collapsed-build-list-sorting.yml deleted file mode 100644 index 036e606318f..00000000000 --- a/changelogs/unreleased/24739-collapsed-build-list-sorting.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Sort builds by name within pipeline graph -merge_request: 7681 -author: diff --git a/changelogs/unreleased/24779-last-deployment-call-on-nil-environment-fix.yml b/changelogs/unreleased/24779-last-deployment-call-on-nil-environment-fix.yml deleted file mode 100644 index 5e7580fb8f2..00000000000 --- a/changelogs/unreleased/24779-last-deployment-call-on-nil-environment-fix.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: fixes last_deployment call environment is nil -merge_request: 7671 -author: diff --git a/changelogs/unreleased/24804-wrong-render-index-should-be-render-show-in-projects-pipelinessettingscontroller-update.yml b/changelogs/unreleased/24804-wrong-render-index-should-be-render-show-in-projects-pipelinessettingscontroller-update.yml deleted file mode 100644 index 92dbbe3d164..00000000000 --- a/changelogs/unreleased/24804-wrong-render-index-should-be-render-show-in-projects-pipelinessettingscontroller-update.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix wrong template rendered when CI/CD settings aren't update successfully -merge_request: 7665 -author: diff --git a/changelogs/unreleased/24863-mrs-without-discussions-are-mergeable.yml b/changelogs/unreleased/24863-mrs-without-discussions-are-mergeable.yml deleted file mode 100644 index 9bdb9411135..00000000000 --- a/changelogs/unreleased/24863-mrs-without-discussions-are-mergeable.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Correctly determine mergeability of MR with no discussions -merge_request: -author: diff --git a/changelogs/unreleased/Last-minute-CI-Style-tweaks-for-8-14.yml b/changelogs/unreleased/Last-minute-CI-Style-tweaks-for-8-14.yml deleted file mode 100644 index 7d49c639a43..00000000000 --- a/changelogs/unreleased/Last-minute-CI-Style-tweaks-for-8-14.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Last minute CI Style tweaks for 8.14 -merge_request: 7643 -author: diff --git a/changelogs/unreleased/disable-calendar-deselection.yml b/changelogs/unreleased/disable-calendar-deselection.yml deleted file mode 100644 index 060797bba34..00000000000 --- a/changelogs/unreleased/disable-calendar-deselection.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix deselecting calendar days on contribution graph -merge_request: 6453 -author: ClemMakesApps diff --git a/changelogs/unreleased/fix-build-without-trace-exceptions.yml b/changelogs/unreleased/fix-build-without-trace-exceptions.yml deleted file mode 100644 index 3b95e96e212..00000000000 --- a/changelogs/unreleased/fix-build-without-trace-exceptions.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix exceptions when loading build trace -merge_request: 7658 -author: diff --git a/changelogs/unreleased/fix-cycle-analytics-plan-issue.yml b/changelogs/unreleased/fix-cycle-analytics-plan-issue.yml deleted file mode 100644 index 6ed16c6d722..00000000000 --- a/changelogs/unreleased/fix-cycle-analytics-plan-issue.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix cycle analytics plan stage when commits are missing -merge_request: -author: diff --git a/changelogs/unreleased/fix_sidekiq_stats_in_admin_area.yml b/changelogs/unreleased/fix_sidekiq_stats_in_admin_area.yml deleted file mode 100644 index 4f007be8624..00000000000 --- a/changelogs/unreleased/fix_sidekiq_stats_in_admin_area.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Sidekiq stats in the admin area will now show correctly on different platforms -merge_request: -author: blackst0ne diff --git a/changelogs/unreleased/issue-boards-dragging-fix.yml b/changelogs/unreleased/issue-boards-dragging-fix.yml deleted file mode 100644 index 565e09b930b..00000000000 --- a/changelogs/unreleased/issue-boards-dragging-fix.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed issue boards dragging card removing random issues -merge_request: -author: diff --git a/changelogs/unreleased/zj-upgrade-grape.yml b/changelogs/unreleased/zj-upgrade-grape.yml deleted file mode 100644 index 1df42d98733..00000000000 --- a/changelogs/unreleased/zj-upgrade-grape.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Update grape entity to 0.6.0 -merge_request: 7491 -author: -- cgit v1.2.1 From d5b662c2a7512f6c419cb22faab1e08009b7c050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Mon, 28 Nov 2016 16:32:01 +0100 Subject: Update CHANGELOG.md for 8.13.7 [ci skip] --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18b3755d365..12a3e63ed2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -222,6 +222,15 @@ entry. - Fix "Without projects" filter. !6611 (Ben Bodenmiller) - Fix 404 when visit /projects page +## 8.13.7 (2016-11-28) + +- fixes 500 error on project show when user is not logged in and project is still empty. !7376 +- Update grape entity to 0.6.0. !7491 +- Fix information disclosure in `Projects::BlobController#update`. +- Fix missing access checks on issue lookup using IssuableFinder. +- Replace issue access checks with use of IssuableFinder. +- Non members cannot create labels through the API. + ## 8.13.6 (2016-11-17) - Omniauth auto link LDAP user falls back to find by DN when user cannot be found by UID. !7002 -- cgit v1.2.1 From c33b489853eb025c4d2d9c4a79630109ddf55e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Mon, 28 Nov 2016 16:45:08 +0100 Subject: Add `null: true` to timestamps in migrations that does not define it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is to ensure that migrations will still be consitent when we will upgrade to Rails 5 which default to `null: false` for timestamps columns. Fixes #23666. Signed-off-by: Rémy Coutable <remy@rymai.me> --- db/migrate/20130319214458_create_forked_project_links.rb | 2 +- db/migrate/20130506090604_create_deploy_keys_projects.rb | 2 +- db/migrate/20130617095603_create_users_groups.rb | 2 +- db/migrate/20130711063759_create_project_group_links.rb | 2 +- db/migrate/20131112114325_create_broadcast_messages.rb | 2 +- db/migrate/20140122112253_create_merge_request_diffs.rb | 2 +- db/migrate/20140209025651_create_emails.rb | 2 +- db/migrate/20140625115202_create_users_star_projects.rb | 2 +- db/migrate/20140729134820_create_labels.rb | 2 +- db/migrate/20140729140420_create_label_links.rb | 2 +- db/migrate/20140914113604_add_members_table.rb | 2 +- db/migrate/20140914173417_remove_old_member_tables.rb | 4 ++-- db/migrate/20141118150935_add_audit_event.rb | 2 +- db/migrate/20141216155758_create_doorkeeper_tables.rb | 2 +- db/migrate/20150108073740_create_application_settings.rb | 2 +- db/migrate/20150313012111_create_subscriptions_table.rb | 2 +- db/migrate/20150806104937_create_abuse_reports.rb | 2 +- db/migrate/20151103134857_create_lfs_objects.rb | 2 +- db/migrate/20151103134958_create_lfs_objects_projects.rb | 2 +- db/migrate/20151105094515_create_releases.rb | 2 +- db/migrate/20160212123307_create_tasks.rb | 2 +- db/migrate/20160416180807_add_award_emoji.rb | 2 +- db/migrate/20160831214002_create_project_features.rb | 2 +- 23 files changed, 24 insertions(+), 24 deletions(-) diff --git a/db/migrate/20130319214458_create_forked_project_links.rb b/db/migrate/20130319214458_create_forked_project_links.rb index 66eb11a4b2b..41b0b700a6f 100644 --- a/db/migrate/20130319214458_create_forked_project_links.rb +++ b/db/migrate/20130319214458_create_forked_project_links.rb @@ -5,7 +5,7 @@ class CreateForkedProjectLinks < ActiveRecord::Migration t.integer :forked_to_project_id, null: false t.integer :forked_from_project_id, null: false - t.timestamps + t.timestamps null: true end add_index :forked_project_links, :forked_to_project_id, unique: true end diff --git a/db/migrate/20130506090604_create_deploy_keys_projects.rb b/db/migrate/20130506090604_create_deploy_keys_projects.rb index 7d6662d358a..f2e416d3b6f 100644 --- a/db/migrate/20130506090604_create_deploy_keys_projects.rb +++ b/db/migrate/20130506090604_create_deploy_keys_projects.rb @@ -5,7 +5,7 @@ class CreateDeployKeysProjects < ActiveRecord::Migration t.integer :deploy_key_id, null: false t.integer :project_id, null: false - t.timestamps + t.timestamps null: true end end end diff --git a/db/migrate/20130617095603_create_users_groups.rb b/db/migrate/20130617095603_create_users_groups.rb index 45cff93fe4a..cb098aa9bf9 100644 --- a/db/migrate/20130617095603_create_users_groups.rb +++ b/db/migrate/20130617095603_create_users_groups.rb @@ -6,7 +6,7 @@ class CreateUsersGroups < ActiveRecord::Migration t.integer :group_id, null: false t.integer :user_id, null: false - t.timestamps + t.timestamps null: true end end end diff --git a/db/migrate/20130711063759_create_project_group_links.rb b/db/migrate/20130711063759_create_project_group_links.rb index bd9d40a50db..8da7ff6f4cd 100644 --- a/db/migrate/20130711063759_create_project_group_links.rb +++ b/db/migrate/20130711063759_create_project_group_links.rb @@ -5,7 +5,7 @@ class CreateProjectGroupLinks < ActiveRecord::Migration t.integer :project_id, null: false t.integer :group_id, null: false - t.timestamps + t.timestamps null: true end end end diff --git a/db/migrate/20131112114325_create_broadcast_messages.rb b/db/migrate/20131112114325_create_broadcast_messages.rb index ce37a8e2708..4ada40f1b66 100644 --- a/db/migrate/20131112114325_create_broadcast_messages.rb +++ b/db/migrate/20131112114325_create_broadcast_messages.rb @@ -7,7 +7,7 @@ class CreateBroadcastMessages < ActiveRecord::Migration t.datetime :ends_at t.integer :alert_type - t.timestamps + t.timestamps null: true end end end diff --git a/db/migrate/20140122112253_create_merge_request_diffs.rb b/db/migrate/20140122112253_create_merge_request_diffs.rb index 395c3edfc79..68448c91529 100644 --- a/db/migrate/20140122112253_create_merge_request_diffs.rb +++ b/db/migrate/20140122112253_create_merge_request_diffs.rb @@ -7,7 +7,7 @@ class CreateMergeRequestDiffs < ActiveRecord::Migration t.text :st_diffs, null: true t.integer :merge_request_id, null: false - t.timestamps + t.timestamps null: true end if ActiveRecord::Base.configurations[Rails.env]['adapter'] =~ /^mysql/ diff --git a/db/migrate/20140209025651_create_emails.rb b/db/migrate/20140209025651_create_emails.rb index 571beb19cdd..48d14682628 100644 --- a/db/migrate/20140209025651_create_emails.rb +++ b/db/migrate/20140209025651_create_emails.rb @@ -5,7 +5,7 @@ class CreateEmails < ActiveRecord::Migration t.integer :user_id, null: false t.string :email, null: false - t.timestamps + t.timestamps null: true end add_index :emails, :user_id diff --git a/db/migrate/20140625115202_create_users_star_projects.rb b/db/migrate/20140625115202_create_users_star_projects.rb index 32dd99e83be..c50bc4bd614 100644 --- a/db/migrate/20140625115202_create_users_star_projects.rb +++ b/db/migrate/20140625115202_create_users_star_projects.rb @@ -4,7 +4,7 @@ class CreateUsersStarProjects < ActiveRecord::Migration create_table :users_star_projects do |t| t.integer :project_id, null: false t.integer :user_id, null: false - t.timestamps + t.timestamps null: true end add_index :users_star_projects, :user_id add_index :users_star_projects, :project_id diff --git a/db/migrate/20140729134820_create_labels.rb b/db/migrate/20140729134820_create_labels.rb index df0f8cb9f03..589aced0d76 100644 --- a/db/migrate/20140729134820_create_labels.rb +++ b/db/migrate/20140729134820_create_labels.rb @@ -6,7 +6,7 @@ class CreateLabels < ActiveRecord::Migration t.string :color t.integer :project_id - t.timestamps + t.timestamps null: true end end end diff --git a/db/migrate/20140729140420_create_label_links.rb b/db/migrate/20140729140420_create_label_links.rb index fa5992605f8..abdbaa1bd1a 100644 --- a/db/migrate/20140729140420_create_label_links.rb +++ b/db/migrate/20140729140420_create_label_links.rb @@ -6,7 +6,7 @@ class CreateLabelLinks < ActiveRecord::Migration t.integer :target_id t.string :target_type - t.timestamps + t.timestamps null: true end end end diff --git a/db/migrate/20140914113604_add_members_table.rb b/db/migrate/20140914113604_add_members_table.rb index bc3c1bb61e4..fb0876dd520 100644 --- a/db/migrate/20140914113604_add_members_table.rb +++ b/db/migrate/20140914113604_add_members_table.rb @@ -9,7 +9,7 @@ class AddMembersTable < ActiveRecord::Migration t.integer :notification_level, null: false t.string :type - t.timestamps + t.timestamps null: true end add_index :members, :type diff --git a/db/migrate/20140914173417_remove_old_member_tables.rb b/db/migrate/20140914173417_remove_old_member_tables.rb index aff8e94e5be..067dc21ccf1 100644 --- a/db/migrate/20140914173417_remove_old_member_tables.rb +++ b/db/migrate/20140914173417_remove_old_member_tables.rb @@ -12,7 +12,7 @@ class RemoveOldMemberTables < ActiveRecord::Migration t.integer :user_id, null: false t.integer :notification_level, null: false, default: 3 - t.timestamps + t.timestamps null: true end create_table :users_projects do |t| @@ -21,7 +21,7 @@ class RemoveOldMemberTables < ActiveRecord::Migration t.integer :user_id, null: false t.integer :notification_level, null: false, default: 3 - t.timestamps + t.timestamps null: true end end end diff --git a/db/migrate/20141118150935_add_audit_event.rb b/db/migrate/20141118150935_add_audit_event.rb index 3884228456f..f0452f46459 100644 --- a/db/migrate/20141118150935_add_audit_event.rb +++ b/db/migrate/20141118150935_add_audit_event.rb @@ -13,7 +13,7 @@ class AddAuditEvent < ActiveRecord::Migration # Details for the event t.text :details - t.timestamps + t.timestamps null: true end add_index :audit_events, :author_id diff --git a/db/migrate/20141216155758_create_doorkeeper_tables.rb b/db/migrate/20141216155758_create_doorkeeper_tables.rb index b323ffe96f5..1c4d32e133c 100644 --- a/db/migrate/20141216155758_create_doorkeeper_tables.rb +++ b/db/migrate/20141216155758_create_doorkeeper_tables.rb @@ -7,7 +7,7 @@ class CreateDoorkeeperTables < ActiveRecord::Migration t.string :secret, null: false t.text :redirect_uri, null: false t.string :scopes, null: false, default: '' - t.timestamps + t.timestamps null: true end add_index :oauth_applications, :uid, unique: true diff --git a/db/migrate/20150108073740_create_application_settings.rb b/db/migrate/20150108073740_create_application_settings.rb index dfa2f765357..c26a7c39574 100644 --- a/db/migrate/20150108073740_create_application_settings.rb +++ b/db/migrate/20150108073740_create_application_settings.rb @@ -8,7 +8,7 @@ class CreateApplicationSettings < ActiveRecord::Migration t.boolean :gravatar_enabled t.text :sign_in_text - t.timestamps + t.timestamps null: true end end end diff --git a/db/migrate/20150313012111_create_subscriptions_table.rb b/db/migrate/20150313012111_create_subscriptions_table.rb index 8adb193b27f..0977c9adfec 100644 --- a/db/migrate/20150313012111_create_subscriptions_table.rb +++ b/db/migrate/20150313012111_create_subscriptions_table.rb @@ -6,7 +6,7 @@ class CreateSubscriptionsTable < ActiveRecord::Migration t.references :subscribable, polymorphic: true t.boolean :subscribed - t.timestamps + t.timestamps null: true end add_index :subscriptions, diff --git a/db/migrate/20150806104937_create_abuse_reports.rb b/db/migrate/20150806104937_create_abuse_reports.rb index 3c749b5d9a9..9f1512db862 100644 --- a/db/migrate/20150806104937_create_abuse_reports.rb +++ b/db/migrate/20150806104937_create_abuse_reports.rb @@ -6,7 +6,7 @@ class CreateAbuseReports < ActiveRecord::Migration t.integer :user_id t.text :message - t.timestamps + t.timestamps null: true end end end diff --git a/db/migrate/20151103134857_create_lfs_objects.rb b/db/migrate/20151103134857_create_lfs_objects.rb index 745b52e2b24..fadaf637cec 100644 --- a/db/migrate/20151103134857_create_lfs_objects.rb +++ b/db/migrate/20151103134857_create_lfs_objects.rb @@ -5,7 +5,7 @@ class CreateLfsObjects < ActiveRecord::Migration t.string :oid, null: false, unique: true t.integer :size, null: false - t.timestamps + t.timestamps null: true end end end diff --git a/db/migrate/20151103134958_create_lfs_objects_projects.rb b/db/migrate/20151103134958_create_lfs_objects_projects.rb index 3178e85b899..e830cbe4656 100644 --- a/db/migrate/20151103134958_create_lfs_objects_projects.rb +++ b/db/migrate/20151103134958_create_lfs_objects_projects.rb @@ -5,7 +5,7 @@ class CreateLfsObjectsProjects < ActiveRecord::Migration t.integer :lfs_object_id, null: false t.integer :project_id, null: false - t.timestamps + t.timestamps null: true end add_index :lfs_objects_projects, :project_id diff --git a/db/migrate/20151105094515_create_releases.rb b/db/migrate/20151105094515_create_releases.rb index 145b8db1486..87f692c64d0 100644 --- a/db/migrate/20151105094515_create_releases.rb +++ b/db/migrate/20151105094515_create_releases.rb @@ -6,7 +6,7 @@ class CreateReleases < ActiveRecord::Migration t.text :description t.integer :project_id - t.timestamps + t.timestamps null: true end add_index :releases, :project_id diff --git a/db/migrate/20160212123307_create_tasks.rb b/db/migrate/20160212123307_create_tasks.rb index 20573b01351..8b5b1dd694d 100644 --- a/db/migrate/20160212123307_create_tasks.rb +++ b/db/migrate/20160212123307_create_tasks.rb @@ -9,7 +9,7 @@ class CreateTasks < ActiveRecord::Migration t.integer :action, null: false t.string :state, null: false, index: true - t.timestamps + t.timestamps null: true end end end diff --git a/db/migrate/20160416180807_add_award_emoji.rb b/db/migrate/20160416180807_add_award_emoji.rb index a3bee9b1bc6..c0957f028a8 100644 --- a/db/migrate/20160416180807_add_award_emoji.rb +++ b/db/migrate/20160416180807_add_award_emoji.rb @@ -6,7 +6,7 @@ class AddAwardEmoji < ActiveRecord::Migration t.references :user t.references :awardable, polymorphic: true - t.timestamps + t.timestamps null: true end add_index :award_emoji, :user_id diff --git a/db/migrate/20160831214002_create_project_features.rb b/db/migrate/20160831214002_create_project_features.rb index 2d76a015a08..343953826f0 100644 --- a/db/migrate/20160831214002_create_project_features.rb +++ b/db/migrate/20160831214002_create_project_features.rb @@ -10,7 +10,7 @@ class CreateProjectFeatures < ActiveRecord::Migration t.integer :snippets_access_level t.integer :builds_access_level - t.timestamps + t.timestamps null: true end end end -- cgit v1.2.1 From beedd40ef7744151d87f4d3ba0b47b2878a83195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Mon, 28 Nov 2016 13:03:31 +0100 Subject: Ensure user is authenticated to create a new snippet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- app/controllers/concerns/toggle_award_emoji.rb | 5 +---- .../25026-authenticate-user-for-new-snippet.yml | 4 ++++ spec/controllers/snippets_controller_spec.rb | 22 ++++++++++++++++++++++ spec/features/snippets/create_snippet_spec.rb | 20 ++++++++++++++++++++ 4 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/25026-authenticate-user-for-new-snippet.yml create mode 100644 spec/features/snippets/create_snippet_spec.rb diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb index 3717c49f272..fbf9a026b10 100644 --- a/app/controllers/concerns/toggle_award_emoji.rb +++ b/app/controllers/concerns/toggle_award_emoji.rb @@ -1,11 +1,8 @@ module ToggleAwardEmoji extend ActiveSupport::Concern - included do - before_action :authenticate_user!, only: [:toggle_award_emoji] - end - def toggle_award_emoji + authenticate_user! name = params.require(:name) if awardable.user_can_award?(current_user, name) diff --git a/changelogs/unreleased/25026-authenticate-user-for-new-snippet.yml b/changelogs/unreleased/25026-authenticate-user-for-new-snippet.yml new file mode 100644 index 00000000000..a7b5810f1bf --- /dev/null +++ b/changelogs/unreleased/25026-authenticate-user-for-new-snippet.yml @@ -0,0 +1,4 @@ +--- +title: Redirect to sign-in page when unauthenticated user tries to create a snippet +merge_request: 7786 +author: diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index 2d762fdaa04..d76fe9f580f 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -3,6 +3,28 @@ require 'spec_helper' describe SnippetsController do let(:user) { create(:user) } + describe 'GET #new' do + context 'when signed in' do + before do + sign_in(user) + end + + it 'responds with status 200' do + get :new + + expect(response).to have_http_status(200) + end + end + + context 'when not signed in' do + it 'redirects to the sign in page' do + get :new + + expect(response).to redirect_to(new_user_session_path) + end + end + end + describe 'GET #show' do context 'when the personal snippet is private' do let(:personal_snippet) { create(:personal_snippet, :private, author: user) } diff --git a/spec/features/snippets/create_snippet_spec.rb b/spec/features/snippets/create_snippet_spec.rb new file mode 100644 index 00000000000..cb95e7828db --- /dev/null +++ b/spec/features/snippets/create_snippet_spec.rb @@ -0,0 +1,20 @@ +require 'rails_helper' + +feature 'Create Snippet', feature: true do + before do + login_as :user + visit new_snippet_path + end + + scenario 'Authenticated user creates a snippet' do + fill_in 'personal_snippet_title', with: 'My Snippet Title' + page.within('.file-editor') do + find(:xpath, "//input[@id='personal_snippet_content']").set 'Hello World!' + end + + click_button 'Create snippet' + + expect(page).to have_content('My Snippet Title') + expect(page).to have_content('Hello World!') + end +end -- cgit v1.2.1 From 8b265aa3cadf9a9c4727495ff4fa9f2aa3a39375 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran <alfredo@gitlab.com> Date: Mon, 28 Nov 2016 11:13:17 -0500 Subject: Show dashes when date is empty --- .../cycle_analytics/components/total_time_component.js.es6 | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 index b9675f50e31..0d85e1a4678 100644 --- a/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 @@ -10,10 +10,15 @@ }, template: ` <span class="total-time"> - <template v-if="time.days">{{ time.days }} <span>{{ time.days === 1 ? 'day' : 'days' }}</span></template> - <template v-if="time.hours">{{ time.hours }} <span>hr</span></template> - <template v-if="time.mins && !time.days">{{ time.mins }} <span>mins</span></template> - <template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>s</span></template> + <template v-if="Object.keys(time).length"> + <template v-if="time.days">{{ time.days }} <span>{{ time.days === 1 ? 'day' : 'days' }}</span></template> + <template v-if="time.hours">{{ time.hours }} <span>hr</span></template> + <template v-if="time.mins && !time.days">{{ time.mins }} <span>mins</span></template> + <template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>s</span></template> + </template> + <template v-else> + -- + </template> </span> `, }); -- cgit v1.2.1 From 6743d5aad0b6fc410919170c393780ca17d2437b Mon Sep 17 00:00:00 2001 From: Chris Peressini <cperessini@gitlab.com> Date: Mon, 28 Nov 2016 16:34:47 +0000 Subject: Create secondary colors with darken function --- app/assets/stylesheets/framework/variables.scss | 72 +++++++++++++------------ 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 750d99ebabe..2539c841111 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -12,67 +12,71 @@ $sidebar-breakpoint: 1024px; /* * Color schema */ + $darken-normal-factor: 7%; +$darken-dark-factor: 10%; +$darken-border-factor: 5%; + $white-light: #fff; -$white-normal: #ededed; -$white-dark: #ececec; +$white-normal: darken($white-light, $darken-normal-factor); +$white-dark: darken($white-light, $darken-dark-factor); $gray-lightest: #fdfdfd; $gray-light: #fafafa; $gray-lighter: #f9f9f9; -$gray-normal: #f5f5f5; -$gray-dark: #ededed; +$gray-normal: darken($gray-light, $darken-normal-factor); +$gray-dark: darken($gray-light, $darken-dark-factor); $gray-darker: #eee; $gray-darkest: #c9c9c9; -$green-light: #38ae67; -$green-normal: #2faa60; -$green-dark: #2ca05b; +$green-light: #3cbd70; +$green-normal: darken($green-light, $darken-normal-factor); +$green-dark: darken($green-light, $darken-dark-factor); $blue-light: #2ea8e5; -$blue-normal: #2d9fd8; -$blue-dark: #2897ce; +$blue-normal: darken($blue-light, $darken-normal-factor); +$blue-dark: darken($blue-light, $darken-dark-factor); $blue-medium-light: #3498cb; -$blue-medium: #2f8ebf; -$blue-medium-dark: #2d86b4; +$blue-medium: darken($blue-medium-light, $darken-normal-factor); +$blue-medium-dark: darken($blue-medium-light, $darken-dark-factor); $blue-light-transparent: rgba(44, 159, 216, 0.05); $orange-light: #fc8a51; -$orange-normal: #e75e40; -$orange-dark: #ce5237; +$orange-normal: darken($orange-light, $darken-normal-factor); +$orange-dark: darken($orange-light, $darken-dark-factor); $red-light: #e52c5a; -$red-normal: #d22852; -$red-dark: darken($red-normal, 5%); +$red-normal: darken($red-light, $darken-normal-factor); +$red-dark: darken($red-light, $darken-dark-factor); $black: #000; $black-transparent: rgba(0, 0, 0, 0.3); -$border-white-light: #f1f2f4; -$border-white-normal: #d6dae2; -$border-white-dark: #c6cacf; +$border-white-light: darken($white-light, $darken-border-factor); +$border-white-normal: darken($white-normal, $darken-border-factor); +$border-white-dark: darken($white-dark, $darken-border-factor); -$border-gray-light: #dcdcdc; -$border-gray-normal: #d7d7d7; -$border-gray-dark: #c6cacf; +$border-gray-light: darken($gray-light, $darken-border-factor); +$border-gray-normal: darken($gray-normal, $darken-border-factor); +$border-gray-dark: darken($gray-dark, $darken-border-factor); $border-green-extra-light: #9adb84; -$border-green-light: #2faa60; -$border-green-normal: #2ca05b; -$border-green-dark: #279654; +$border-green-light: darken($green-light, $darken-border-factor); +$border-green-normal: darken($green-normal, $darken-border-factor); +$border-green-dark: darken($green-dark, $darken-border-factor); -$border-blue-light: #2d9fd8; -$border-blue-normal: #2897ce; -$border-blue-dark: #258dc1; +$border-blue-light: darken($blue-light, $darken-border-factor); +$border-blue-normal: darken($blue-normal, $darken-border-factor); +$border-blue-dark: darken($blue-dark, $darken-border-factor); -$border-orange-light: #fc6d26; -$border-orange-normal: #ce5237; -$border-orange-dark: #c14e35; +$border-orange-light: darken($orange-light, $darken-border-factor); +$border-orange-normal: darken($orange-normal, $darken-border-factor); +$border-orange-dark: darken($orange-dark, $darken-border-factor); -$border-red-light: #d22852; -$border-red-normal: #ca264f; -$border-red-dark: darken($border-red-normal, 5%); +$border-red-light: darken($red-light, $darken-border-factor); +$border-red-normal: darken($red-normal, $darken-border-factor); +$border-red-dark: darken($red-dark, $darken-border-factor); $help-well-bg: $gray-light; $help-well-border: #e5e5e5; @@ -255,7 +259,7 @@ $search-input-border-color: rgba(#4688f1, .8); $search-input-focus-shadow-color: $dropdown-input-focus-shadow; $search-input-width: 220px; $location-badge-color: #aaa; -$location-badge-bg: $gray-normal; +$location-badge-bg: $dark-background-color; $location-badge-active-bg: #4f91f8; $location-icon-color: #e7e9ed; $location-icon-active-color: #807e7e; -- cgit v1.2.1 From 297c8683982c4ee83fc6b866f121b6aa18f5488a Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 28 Nov 2016 18:27:29 +0100 Subject: Add guidelines in doc linking with HAML [ci skip] --- doc/development/doc_styleguide.md | 71 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index b137e6ae82e..fc948a7a116 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -113,6 +113,77 @@ merge request. add an alternative text: `[identifier]: https://example.com "Alternative text"` that appears when hovering your mouse on a link +### Linking to inline docs + +Sometimes it's needed to link to the built-in documentation that GitLab provides +under `/help`. This is normally done in files inside the `app/views/` directory +with the help of the `help_page_path` helper method. + +In its simplest form, the HAML code to generate a link to the `/help` page is: + +```haml += link_to 'Help page', help_page_path('user/permissions') +``` + +The `help_page_path` contains the path to the document you want to link to with +the following conventions: + +- it is relative to the `doc/` directory in the GitLab repository +- the `.md` extension must be omitted +- it must not end with a slash (`/`) + +Below are some special cases where should be used depending on the context. +You can combine one or more of the following: + +1. **Linking to an anchor link.** Use `anchor` as part of the `help_page_path` + method: + + ```haml + = link_to 'Help page', help_page_path('user/permissions', anchor: 'anchor-link') + ``` + +1. **Opening links in a new tab.** This should be the default behavior: + + ```haml + = link_to 'Help page', help_page_path('user/permissions'), target: '_blank' + ``` + +1. **Linking to a circle icon.** Usually used in settings where a long + description cannot be used, like near checkboxes. You can basically use + any font awesome icon, but prefer the `question-circle`: + + ```haml + = link_to icon('question-circle'), help_page_path('user/permissions') + ``` + +1. **Using a button link.** Useful in places where text would be out of context + with the rest of the page layout: + + ```haml + = link_to 'Help page', help_page_path('user/permissions'), class: 'btn btn-info' + ``` + +1. **Underlining a link.** + + ```haml + = link_to 'Help page', help_page_path('user/permissions'), class: 'underlined-link' + ``` + +1. **Using links inline of some text.** + + ```haml + Description to #{link_to 'Help page', help_page_path('user/permissions')}. + ``` + +1. **Adding a period at the end of the sentence.** Useful when you don't want + the period to be part of the link: + + ```haml + = succeed '.' do + Learn more in the + = link_to 'Help page', help_page_path('user/permissions') + ``` + ## Images - Place images in a separate directory named `img/` in the same directory where -- cgit v1.2.1 From eb4f15571d02634920b975e7ce2325572d88356e Mon Sep 17 00:00:00 2001 From: Livier <lhirales@nearsoft.com> Date: Wed, 23 Nov 2016 13:14:08 -0700 Subject: Changed API spec files to describe the correct class Restore changes for api spec files Fix error in rspec Users Delete extra space Repositories-spec --- changelogs/unreleased/update-api-spec-files.yml | 4 ++++ spec/requests/api/award_emoji_spec.rb | 2 +- spec/requests/api/boards_spec.rb | 2 +- spec/requests/api/branches_spec.rb | 2 +- spec/requests/api/builds_spec.rb | 2 +- spec/requests/api/commits_spec.rb | 2 +- spec/requests/api/deploy_keys_spec.rb | 2 +- spec/requests/api/deployments_spec.rb | 2 +- spec/requests/api/environments_spec.rb | 2 +- spec/requests/api/files_spec.rb | 2 +- spec/requests/api/groups_spec.rb | 2 +- spec/requests/api/internal_spec.rb | 2 +- spec/requests/api/issues_spec.rb | 2 +- spec/requests/api/keys_spec.rb | 2 +- spec/requests/api/labels_spec.rb | 2 +- spec/requests/api/merge_request_diffs_spec.rb | 2 +- spec/requests/api/merge_requests_spec.rb | 2 +- spec/requests/api/milestones_spec.rb | 2 +- spec/requests/api/namespaces_spec.rb | 2 +- spec/requests/api/notes_spec.rb | 2 +- spec/requests/api/notification_settings_spec.rb | 2 +- spec/requests/api/pipelines_spec.rb | 2 +- spec/requests/api/project_hooks_spec.rb | 2 +- spec/requests/api/project_snippets_spec.rb | 2 +- spec/requests/api/projects_spec.rb | 2 +- spec/requests/api/repositories_spec.rb | 5 ++--- spec/requests/api/services_spec.rb | 2 +- spec/requests/api/session_spec.rb | 2 +- spec/requests/api/settings_spec.rb | 2 +- spec/requests/api/system_hooks_spec.rb | 2 +- spec/requests/api/tags_spec.rb | 2 +- spec/requests/api/triggers_spec.rb | 2 +- spec/requests/api/users_spec.rb | 2 +- spec/requests/api/variables_spec.rb | 2 +- spec/requests/api/version_spec.rb | 2 +- spec/requests/ci/api/builds_spec.rb | 2 +- spec/requests/ci/api/runners_spec.rb | 2 +- spec/requests/ci/api/triggers_spec.rb | 2 +- 38 files changed, 42 insertions(+), 39 deletions(-) create mode 100644 changelogs/unreleased/update-api-spec-files.yml diff --git a/changelogs/unreleased/update-api-spec-files.yml b/changelogs/unreleased/update-api-spec-files.yml new file mode 100644 index 00000000000..349d866cf22 --- /dev/null +++ b/changelogs/unreleased/update-api-spec-files.yml @@ -0,0 +1,4 @@ +--- +title: Update API spec files to describe the correct class +merge_request: +author: Livier diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb index 5ad4fc4865a..c8e8f31cc1f 100644 --- a/spec/requests/api/award_emoji_spec.rb +++ b/spec/requests/api/award_emoji_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, api: true do +describe API::AwardEmoji, api: true do include ApiHelpers let(:user) { create(:user) } let!(:project) { create(:empty_project) } diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb index 4f5c09a3029..3019724f52e 100644 --- a/spec/requests/api/boards_spec.rb +++ b/spec/requests/api/boards_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, api: true do +describe API::Boards, api: true do include ApiHelpers let(:user) { create(:user) } diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index fe6b875b997..ce1f2c60537 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require 'mime/types' -describe API::API, api: true do +describe API::Branches, api: true do include ApiHelpers let(:user) { create(:user) } diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index fc72a44d663..0ea991b18b8 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, api: true do +describe API::Builds, api: true do include ApiHelpers let(:user) { create(:user) } diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index a6e8550fac3..52491e1b9ed 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require 'mime/types' -describe API::API, api: true do +describe API::Commits, api: true do include ApiHelpers let(:user) { create(:user) } let(:user2) { create(:user) } diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 65897edba7f..5fa7299044e 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, api: true do +describe API::DeployKeys, api: true do include ApiHelpers let(:user) { create(:user) } diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb index 8fa8c66db6c..31e3cfa1b2f 100644 --- a/spec/requests/api/deployments_spec.rb +++ b/spec/requests/api/deployments_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, api: true do +describe API::Deployments, api: true do include ApiHelpers let(:user) { create(:user) } diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 1898b07835d..126496c43a5 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, api: true do +describe API::Environments, api: true do include ApiHelpers let(:user) { create(:user) } diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index 050d0dd082d..2081f80ccc1 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, api: true do +describe API::Files, api: true do include ApiHelpers let(:user) { create(:user) } let!(:project) { create(:project, namespace: user.namespace ) } diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index d9fdafde05e..548ed8e1892 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, api: true do +describe API::Groups, api: true do include ApiHelpers let(:user1) { create(:user, can_create_group: false) } diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index e88a7e27d45..35644bd8cc9 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, api: true do +describe API::Internal, api: true do include ApiHelpers let(:user) { create(:user) } let(:key) { create(:key, user: user) } diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 7bae055b241..e385456dfbe 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, api: true do +describe API::Issues, api: true do include ApiHelpers let(:user) { create(:user) } diff --git a/spec/requests/api/keys_spec.rb b/spec/requests/api/keys_spec.rb index 893ed5c2b10..4c80987d680 100644 --- a/spec/requests/api/keys_spec.rb +++ b/spec/requests/api/keys_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, api: true do +describe API::Keys, api: true do include ApiHelpers let(:user) { create(:user) } diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index aaf41639277..b29ce1ea25e 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, api: true do +describe API::Labels, api: true do include ApiHelpers let(:user) { create(:user) } diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb index 131c2d406ea..e1887138aab 100644 --- a/spec/requests/api/merge_request_diffs_spec.rb +++ b/spec/requests/api/merge_request_diffs_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe API::API, 'MergeRequestDiffs', api: true do +describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do include ApiHelpers let!(:user) { create(:user) } diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 37fcb2bc3a9..40534671522 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe API::API, api: true do +describe API::MergeRequests, api: true do include ApiHelpers let(:base_time) { Time.now } let(:user) { create(:user) } diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index b0946a838a1..8beef821d6c 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, api: true do +describe API::Milestones, api: true do include ApiHelpers let(:user) { create(:user) } let!(:project) { create(:empty_project, namespace: user.namespace ) } diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb index 5347cf4f7bc..c1edf384d5c 100644 --- a/spec/requests/api/namespaces_spec.rb +++ b/spec/requests/api/namespaces_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, api: true do +describe API::Namespaces, api: true do include ApiHelpers let(:admin) { create(:admin) } let(:user) { create(:user) } diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 0124b7271b3..e44ef8e765d 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, api: true do +describe API::Notes, api: true do include ApiHelpers let(:user) { create(:user) } let!(:project) { create(:project, :public, namespace: user.namespace) } diff --git a/spec/requests/api/notification_settings_spec.rb b/spec/requests/api/notification_settings_spec.rb index e6d8a5ee954..8691a81420f 100644 --- a/spec/requests/api/notification_settings_spec.rb +++ b/spec/requests/api/notification_settings_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, api: true do +describe API::NotificationSettings, api: true do include ApiHelpers let(:user) { create(:user) } diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb index d83f7883c78..511007486f3 100644 --- a/spec/requests/api/pipelines_spec.rb +++ b/spec/requests/api/pipelines_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, api: true do +describe API::Pipelines, api: true do include ApiHelpers let(:user) { create(:user) } diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index 5f39329a1b8..a42cedae614 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, 'ProjectHooks', api: true do +describe API::ProjectHooks, 'ProjectHooks', api: true do include ApiHelpers let(:user) { create(:user) } let(:user3) { create(:user) } diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 1c25fd04339..01032c0929b 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe API::API, api: true do +describe API::ProjectSnippets, api: true do include ApiHelpers let(:project) { create(:empty_project, :public) } diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index e53ee2a4e76..cde774d9cfa 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- require 'spec_helper' -describe API::API, api: true do +describe API::Projects, api: true do include ApiHelpers include Gitlab::CurrentSettings let(:user) { create(:user) } diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 38c8ad34f9d..c90b69e8ebb 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require 'mime/types' -describe API::API, api: true do +describe API::Repositories, api: true do include ApiHelpers include RepoHelpers include WorkhorseHelpers @@ -44,7 +44,6 @@ describe API::API, api: true do end end - describe 'GET /projects/:id/repository/tree?recursive=1' do context 'authorized user' do before { project.team << [user2, :reporter] } @@ -67,7 +66,7 @@ describe API::API, api: true do expect(json_response).to be_an Object json_response['message'] == '404 Tree Not Found' - end + end end context "unauthorized user" do diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index ce9c96ace21..bf95eaff96a 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe API::API, api: true do +describe API::Services, api: true do include ApiHelpers let(:user) { create(:user) } let(:admin) { create(:admin) } diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb index e3f22b4c578..794e2b5c04d 100644 --- a/spec/requests/api/session_spec.rb +++ b/spec/requests/api/session_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, api: true do +describe API::Session, api: true do include ApiHelpers let(:user) { create(:user) } diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 096a8ebab70..9a8d633d657 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, 'Settings', api: true do +describe API::Settings, 'Settings', api: true do include ApiHelpers let(:user) { create(:user) } diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb index 6c9df21f598..b3e5afdadb1 100644 --- a/spec/requests/api/system_hooks_spec.rb +++ b/spec/requests/api/system_hooks_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, api: true do +describe API::SystemHooks, api: true do include ApiHelpers let(:user) { create(:user) } diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index d563883cd47..06fa94fae87 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require 'mime/types' -describe API::API, api: true do +describe API::Tags, api: true do include ApiHelpers include RepoHelpers diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index c890a51ae42..67ec3168679 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API do +describe API::Triggers do include ApiHelpers let(:user) { create(:user) } diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 1a6e7716b2f..f82f52e7399 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, api: true do +describe API::Users, api: true do include ApiHelpers let(:user) { create(:user) } diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb index 05fbdb909dc..7435f320607 100644 --- a/spec/requests/api/variables_spec.rb +++ b/spec/requests/api/variables_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, api: true do +describe API::Variables, api: true do include ApiHelpers let(:user) { create(:user) } diff --git a/spec/requests/api/version_spec.rb b/spec/requests/api/version_spec.rb index 54b69a0cae7..da1b2fda70e 100644 --- a/spec/requests/api/version_spec.rb +++ b/spec/requests/api/version_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, api: true do +describe API::Version, api: true do include ApiHelpers describe 'GET /version' do diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index a09d8689ff2..80652129928 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::API::API do +describe Ci::API::Builds do include ApiHelpers let(:runner) { FactoryGirl.create(:ci_runner, tag_list: ["mysql", "ruby"]) } diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb index d6c26fd8a94..bd55934d0c8 100644 --- a/spec/requests/ci/api/runners_spec.rb +++ b/spec/requests/ci/api/runners_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::API::API do +describe Ci::API::Runners do include ApiHelpers include StubGitlabCalls diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb index 0a0f979f57d..2d434ab5dd8 100644 --- a/spec/requests/ci/api/triggers_spec.rb +++ b/spec/requests/ci/api/triggers_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::API::API do +describe Ci::API::Triggers do include ApiHelpers describe 'POST /projects/:project_id/refs/:ref/trigger' do -- cgit v1.2.1 From ebfaaffef1e750b214979ec94d0b98b5d4beb200 Mon Sep 17 00:00:00 2001 From: tauriedavis <taurie@gitlab.com> Date: Fri, 18 Nov 2016 11:24:08 -0800 Subject: Add hover state to navigation rows --- app/assets/stylesheets/framework/nav.scss | 51 +++++++++++-------------------- 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index ce864c2de5e..a2787ede53c 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -51,19 +51,26 @@ margin-bottom: -1px; font-size: 15px; line-height: 28px; - color: #959494; + color: $note-toolbar-color; border-bottom: 2px solid transparent; &:hover, &:active, &:focus { text-decoration: none; + border-bottom: 2px solid $gray-darkest; + color: $black; + + .badge { + color: $black; + } } } &.active a { border-bottom: 2px solid $link-underline-blue; color: $black; + font-weight: 600; } .badge { @@ -85,14 +92,20 @@ li { + &.active a { + border-bottom: none; + } + a { margin: 0; padding: 11px 10px 9px; - } - &.active a { - border-bottom: none; - color: $link-underline-blue; + &:hover, + &:active, + &:focus { + color: $black; + border-bottom: none; + } } } } @@ -310,37 +323,9 @@ height: 51px; li { - a { padding-top: 10px; } - - a, - i { - color: $layout-link-gray; - } - - &.active { - - a, - i { - color: $black; - } - - svg { - path, - polygon { - fill: $black; - } - } - } - - &:hover { - a, - i { - color: $black; - } - } } } } -- cgit v1.2.1 From bc1b305dcfff3c4818e6e3d1315234db9a555d61 Mon Sep 17 00:00:00 2001 From: winniehell <git@winniehell.de> Date: Wed, 23 Nov 2016 01:48:14 +0100 Subject: Replace static fixture for right_sidebar_spec (!7687) --- changelogs/unreleased/right-sidebar-fixture.yml | 4 ++++ spec/javascripts/fixtures/right_sidebar.html.haml | 17 ----------------- spec/javascripts/right_sidebar_spec.js | 14 ++++++-------- 3 files changed, 10 insertions(+), 25 deletions(-) create mode 100644 changelogs/unreleased/right-sidebar-fixture.yml delete mode 100644 spec/javascripts/fixtures/right_sidebar.html.haml diff --git a/changelogs/unreleased/right-sidebar-fixture.yml b/changelogs/unreleased/right-sidebar-fixture.yml new file mode 100644 index 00000000000..46a3e459fef --- /dev/null +++ b/changelogs/unreleased/right-sidebar-fixture.yml @@ -0,0 +1,4 @@ +--- +title: Replace static fixture for right_sidebar_spec +merge_request: 7687 +author: winniehell diff --git a/spec/javascripts/fixtures/right_sidebar.html.haml b/spec/javascripts/fixtures/right_sidebar.html.haml deleted file mode 100644 index d259b58f235..00000000000 --- a/spec/javascripts/fixtures/right_sidebar.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -%div - %div.page-gutter.page-with-sidebar - - %aside.right-sidebar - %div.block.issuable-sidebar-header - %a.gutter-toggle.pull-right.js-sidebar-toggle - %i.fa.fa-angle-double-left - %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", data: { todo_text: "Add todo", mark_text: "Mark done", issuable_id: "1", issuable_type: "issue", url: "/todos" }} - %span.js-issuable-todo-text - Add todo - %i.fa.fa-spin.fa-spinner.js-issuable-todo-loading.hidden - - %form.issuable-context-form - %div.block.labels - %div.sidebar-collapsed-icon - %i.fa.fa-tags - %span 1 diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 83ebbd63f3a..0a9bc546144 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -34,9 +34,10 @@ }; describe('RightSidebar', function() { - fixture.preload('right_sidebar.html'); + var fixtureName = 'issues/open-issue.html.raw'; + fixture.preload(fixtureName); beforeEach(function() { - fixture.load('right_sidebar.html'); + fixture.load(fixtureName); this.sidebar = new Sidebar; $aside = $('.right-sidebar'); $page = $('.page-with-sidebar'); @@ -44,15 +45,12 @@ $toggle = $aside.find('.js-sidebar-toggle'); return $labelsIcon = $aside.find('.sidebar-collapsed-icon'); }); - it('should expand the sidebar when arrow is clicked', function() { + it('should expand/collapse the sidebar when arrow is clicked', function() { + assertSidebarState('expanded'); $toggle.click(); - return assertSidebarState('expanded'); - }); - it('should collapse the sidebar when arrow is clicked', function() { + assertSidebarState('collapsed'); $toggle.click(); assertSidebarState('expanded'); - $toggle.click(); - return assertSidebarState('collapsed'); }); it('should float over the page and when sidebar icons clicked', function() { $labelsIcon.click(); -- cgit v1.2.1 From 854fbbfb07b84394cc952d0ae20b7f29957e6555 Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Thu, 17 Nov 2016 22:22:39 +0000 Subject: Tidy up text emails --- app/models/discussion.rb | 7 +++++-- app/views/notify/_note_mr_or_commit_email.text.erb | 2 +- app/views/notify/_simple_diff.text.erb | 2 +- app/views/notify/note_commit_email.text.erb | 4 +--- app/views/notify/note_merge_request_email.text.erb | 4 +--- spec/mailers/notify_spec.rb | 8 ++++---- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/app/models/discussion.rb b/app/models/discussion.rb index 9bd37fe6d89..75a85563235 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -165,18 +165,21 @@ class Discussion # Returns an array of at most 16 highlighted lines above a diff note def truncated_diff_lines(highlight: true) - initial_lines = highlight ? highlighted_diff_lines : diff_lines + lines = highlight ? highlighted_diff_lines : diff_lines prev_lines = [] - initial_lines.each do |line| + lines.each do |line| if line.meta? prev_lines.clear else prev_lines << line + break if for_line?(line) + prev_lines.shift if prev_lines.length >= NUMBER_OF_TRUNCATED_DIFF_LINES end end + prev_lines end diff --git a/app/views/notify/_note_mr_or_commit_email.text.erb b/app/views/notify/_note_mr_or_commit_email.text.erb index 3dd1b4d4c0e..b4fcdf6b1e9 100644 --- a/app/views/notify/_note_mr_or_commit_email.text.erb +++ b/app/views/notify/_note_mr_or_commit_email.text.erb @@ -1,4 +1,4 @@ -<% if @note.diff_note? && @note.diff_file -%> +<% if @discussion && @discussion.diff_file -%> on <%= @note.diff_file.file_path -%> <% end -%>: diff --git a/app/views/notify/_simple_diff.text.erb b/app/views/notify/_simple_diff.text.erb index 58b0018c0ca..c28d1cc34d3 100644 --- a/app/views/notify/_simple_diff.text.erb +++ b/app/views/notify/_simple_diff.text.erb @@ -1,3 +1,3 @@ <% @discussion.truncated_diff_lines(highlight: false).each do |line| %> - <%= "> " + line.text %> +> <%= line.text %> <% end %> diff --git a/app/views/notify/note_commit_email.text.erb b/app/views/notify/note_commit_email.text.erb index dc764b61659..6aa085a172e 100644 --- a/app/views/notify/note_commit_email.text.erb +++ b/app/views/notify/note_commit_email.text.erb @@ -1,4 +1,2 @@ -<% url = url_for(namespace_project_commit_url(@note.project.namespace, @note.project, id: @commit.id, anchor: "note_#{@note.id}")) %> - New comment for Commit <%= @commit.short_id -%> -<%= render partial: 'note_mr_or_commit_email', locals: { url: url } %> +<%= render partial: 'note_mr_or_commit_email', locals: { url: @target_url } %> diff --git a/app/views/notify/note_merge_request_email.text.erb b/app/views/notify/note_merge_request_email.text.erb index e33d15daded..2ce64c494cf 100644 --- a/app/views/notify/note_merge_request_email.text.erb +++ b/app/views/notify/note_merge_request_email.text.erb @@ -1,4 +1,2 @@ -<% url = url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, anchor: "note_#{@note.id}")) %> - New comment for Merge Request <%= @merge_request.to_reference -%> -<%= render partial: 'note_mr_or_commit_email', locals: { url: url }%> +<%= render partial: 'note_mr_or_commit_email', locals: { url: @target_url }%> diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 76ea5f6be31..1c4ecf83141 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -50,7 +50,7 @@ describe Notify do context 'when enabled email_author_in_body' do before do - allow_any_instance_of(ApplicationSetting).to receive(:email_author_in_body).and_return(true) + stub_application_setting(email_author_in_body: true) end it 'contains a link to note author' do @@ -229,7 +229,7 @@ describe Notify do context 'when enabled email_author_in_body' do before do - allow_any_instance_of(ApplicationSetting).to receive(:email_author_in_body).and_return(true) + stub_application_setting(email_author_in_body: true) end it 'contains a link to note author' do @@ -607,7 +607,7 @@ describe Notify do context 'when enabled email_author_in_body' do before do - allow_any_instance_of(ApplicationSetting).to receive(:email_author_in_body).and_return(true) + stub_application_setting(email_author_in_body: true) end it 'contains a link to note author' do @@ -726,7 +726,7 @@ describe Notify do context 'when enabled email_author_in_body' do before do - allow_any_instance_of(ApplicationSetting).to receive(:email_author_in_body).and_return(true) + stub_application_setting(email_author_in_body: true) end it 'contains a link to note author' do -- cgit v1.2.1 From 93f6fcc91e5f241aeef13e96a16bbb6f4027e2fe Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Tue, 22 Nov 2016 12:46:00 +0000 Subject: Don't remove + / - signs from diff emails In the browser, we remove the + and - signs from the front of a diff line because we add them in with CSS, so they aren't copied. We can't do that in an email, because the CSS isn't supported, so we should keep them in that case. --- app/helpers/diff_helper.rb | 4 +++- app/views/projects/diffs/_line.html.haml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index f489f9aa0d6..ce16d971dc6 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -51,9 +51,11 @@ module DiffHelper html.html_safe end - def diff_line_content(line) + def diff_line_content(line, email: false) if line.blank? " ".html_safe + elsif email + line.html_safe else line.sub(/^[\-+ ]/, '').html_safe end diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index a3e4b5b777e..bf2519d4f1c 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -25,7 +25,7 @@ %a{href: "##{line_code}", data: { linenumber: link_text }} %td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }< - if email - %pre= diff_line_content(line.text) + %pre= diff_line_content(line.text, email: true) - else = diff_line_content(line.text) -- cgit v1.2.1 From 87a2762b8b001aa8b8d715c964f5ce99d2585ead Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Wed, 23 Nov 2016 16:21:45 +0000 Subject: Don't use diff_line_content for emails --- app/helpers/diff_helper.rb | 4 +--- app/views/projects/diffs/_line.html.haml | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index ce16d971dc6..f489f9aa0d6 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -51,11 +51,9 @@ module DiffHelper html.html_safe end - def diff_line_content(line, email: false) + def diff_line_content(line) if line.blank? " ".html_safe - elsif email - line.html_safe else line.sub(/^[\-+ ]/, '').html_safe end diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index bf2519d4f1c..16c96b66714 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -25,7 +25,7 @@ %a{href: "##{line_code}", data: { linenumber: link_text }} %td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }< - if email - %pre= diff_line_content(line.text, email: true) + %pre= line.text - else = diff_line_content(line.text) -- cgit v1.2.1 From b8917eb75e94cb13b02534c920ee926c9e97174e Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Wed, 23 Nov 2016 16:25:31 +0000 Subject: Fix spec style --- spec/mailers/notify_spec.rb | 1 + spec/models/discussion_spec.rb | 14 ++++---------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 1c4ecf83141..39ba48f61cb 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -646,6 +646,7 @@ describe Notify do before(:each) { allow(note).to receive(:noteable).and_return(merge_request) } subject { Notify.note_merge_request_email(recipient.id, note.id) } + it_behaves_like 'a note email' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do let(:model) { merge_request } diff --git a/spec/models/discussion_spec.rb b/spec/models/discussion_spec.rb index 187d0bc0150..2a67c60b978 100644 --- a/spec/models/discussion_spec.rb +++ b/spec/models/discussion_spec.rb @@ -595,23 +595,17 @@ describe Discussion, model: true do let(:truncated_lines) { subject.truncated_diff_lines } context "when diff is greater than allowed number of truncated diff lines " do - let(:initial_line_count) { subject.diff_lines.count } - let(:truncated_line_count) { truncated_lines.count } - it "returns fewer lines" do - expect(initial_line_count).to be > described_class::NUMBER_OF_TRUNCATED_DIFF_LINES + expect(subject.diff_lines.count).to be > described_class::NUMBER_OF_TRUNCATED_DIFF_LINES - expect(truncated_line_count).to be <= described_class::NUMBER_OF_TRUNCATED_DIFF_LINES + expect(truncated_lines.count).to be <= described_class::NUMBER_OF_TRUNCATED_DIFF_LINES end end context "when some diff lines are meta" do - let(:initial_meta_lines?) { subject.diff_lines.any?(&:meta?) } - let(:truncated_meta_lines?) { truncated_lines.any?(&:meta?) } - it "returns no meta lines" do - expect(initial_meta_lines?).to be true - expect(truncated_meta_lines?).to be false + expect(subject.diff_lines).to include(be_meta) + expect(truncated_lines).not_to include(be_meta) end end end -- cgit v1.2.1 From 87665170eccfc423fc3f7fe2cadd208d808a6847 Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Wed, 23 Nov 2016 16:25:37 +0000 Subject: Use assigned variables better --- app/views/discussions/_diff_with_notes.html.haml | 2 +- app/views/notify/_note_mr_or_commit_email.html.haml | 6 ++---- lib/gitlab/diff/file.rb | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml index 5c667e4842b..3a95a652810 100644 --- a/app/views/discussions/_diff_with_notes.html.haml +++ b/app/views/discussions/_diff_with_notes.html.haml @@ -9,7 +9,7 @@ %table - discussions = { discussion.original_line_code => discussion } = render partial: "projects/diffs/line", - collection: discussion.highlighted_diff_lines, + collection: discussion.truncated_diff_lines, as: :line, locals: { diff_file: diff_file, discussions: discussions, diff --git a/app/views/notify/_note_mr_or_commit_email.html.haml b/app/views/notify/_note_mr_or_commit_email.html.haml index 15e92c42b14..edf8dfe7e9e 100644 --- a/app/views/notify/_note_mr_or_commit_email.html.haml +++ b/app/views/notify/_note_mr_or_commit_email.html.haml @@ -3,12 +3,10 @@ New comment -- if @note.diff_note? && @note.diff_file +- if @discussion && @discussion.diff_file on = link_to @note.diff_file.file_path, @target_url, class: 'details' -\: - -- if @discussion + \: %table = render partial: "projects/diffs/line", collection: @discussion.truncated_diff_lines, diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index 84784aaf2fd..c6bf25b5874 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -69,7 +69,7 @@ module Gitlab diff_refs.try(:head_sha) end - attr_writer :highlighted_diff_lines, :text_parsed_diff_lines + attr_writer :highlighted_diff_lines # Array of Gitlab::Diff::Line objects def diff_lines -- cgit v1.2.1 From 7c607a55ab339293b0e67eeb33439d5407e22aad Mon Sep 17 00:00:00 2001 From: Robert Schilling <rschilling@student.tugraz.at> Date: Wed, 9 Nov 2016 15:51:27 +0100 Subject: Grapify the projects API --- doc/api/projects.md | 1 + lib/api/helpers.rb | 26 +- lib/api/projects.rb | 578 +++++++++++++++++-------------------- spec/requests/api/projects_spec.rb | 41 ++- 4 files changed, 288 insertions(+), 358 deletions(-) diff --git a/doc/api/projects.md b/doc/api/projects.md index de57f91bb8e..132be644b59 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -626,6 +626,7 @@ Parameters: | `path` | string | no | Custom repository name for new project. By default generated based on name | | `default_branch` | string | no | `master` by default | | `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) | +| `default_branch` | string | no | `master` by default | | `description` | string | no | Short project description | | `issues_enabled` | boolean | no | Enable issues for this project | | `merge_requests_enabled` | boolean | no | Enable merge requests for this project | diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 0d3ddb89dc3..2fd50143e91 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -324,11 +324,6 @@ module API # Projects helpers def filter_projects(projects) - # If the archived parameter is passed, limit results accordingly - if params[:archived].present? - projects = projects.where(archived: to_boolean(params[:archived])) - end - if params[:search].present? projects = projects.search(params[:search]) end @@ -337,25 +332,8 @@ module API projects = projects.search_by_visibility(params[:visibility]) end - projects.reorder(project_order_by => project_sort) - end - - def project_order_by - order_fields = %w(id name path created_at updated_at last_activity_at) - - if order_fields.include?(params['order_by']) - params['order_by'] - else - 'created_at' - end - end - - def project_sort - if params["sort"] == 'asc' - :asc - else - :desc - end + projects = projects.where(archived: params[:archived]) + projects.reorder(params[:order_by] => params[:sort]) end # file helpers diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 2ea3c433ae2..8975b1a751c 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -1,293 +1,287 @@ module API # Projects API class Projects < Grape::API + include PaginationParams + before { authenticate! } - resource :projects, requirements: { id: /[^\/]+/ } do + helpers do + params :optional_params do + optional :description, type: String, desc: 'The description of the project' + optional :issues_enabled, type: Boolean, desc: 'Flag indication if the issue tracker is enabled' + optional :merge_requests_enabled, type: Boolean, desc: 'Flag indication if merge requests are enabled' + optional :wiki_enabled, type: Boolean, desc: 'Flag indication if the wiki is enabled' + optional :builds_enabled, type: Boolean, desc: 'Flag indication if builds are enabled' + optional :snippets_enabled, type: Boolean, desc: 'Flag indication if snippets are enabled' + optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project' + optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project' + optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project' + optional :public, type: Boolean, desc: 'Create a public project. The same as visibility_level = 20.' + optional :visibility_level, type: Integer, values: [ + Gitlab::VisibilityLevel::PRIVATE, + Gitlab::VisibilityLevel::INTERNAL, + Gitlab::VisibilityLevel::PUBLIC ], desc: 'Create a public project. The same as visibility_level = 20.' + optional :public_builds, type: Boolean, desc: 'Perform public builds' + optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access' + optional :only_allow_merge_if_build_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed' + optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved' + end + + def map_public_to_visibility_level(attrs) + publik = attrs.delete(:public) + if !publik.nil? && !attrs[:visibility_level].present? + # Since setting the public attribute to private could mean either + # private or internal, use the more conservative option, private. + attrs[:visibility_level] = (publik == true) ? Gitlab::VisibilityLevel::PUBLIC : Gitlab::VisibilityLevel::PRIVATE + end + attrs + end + end + + resource :projects do helpers do - def map_public_to_visibility_level(attrs) - publik = attrs.delete(:public) - if publik.present? && !attrs[:visibility_level].present? - publik = to_boolean(publik) - # Since setting the public attribute to private could mean either - # private or internal, use the more conservative option, private. - attrs[:visibility_level] = (publik == true) ? Gitlab::VisibilityLevel::PUBLIC : Gitlab::VisibilityLevel::PRIVATE - end - attrs + params :sort_params do + optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at], + default: 'created_at', desc: 'Return projects ordered by field' + optional :sort, type: String, values: %w[asc desc], default: 'desc', + desc: 'Return projects sorted in ascending and descending order' + end + + params :filter_params do + optional :archived, type: Boolean, default: false, desc: 'Limit by archived status' + optional :visibility, type: String, values: %w[public internal private], + desc: 'Limit by visibility' + optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria' + use :sort_params + end + + params :create_params do + optional :namespace_id, type: Integer, desc: 'Namespace ID for the new project. Default to the user namespace.' + optional :import_url, type: String, desc: 'URL from which the project is imported' end end - # Get a projects list for authenticated user - # - # Example Request: - # GET /projects + desc 'Get a projects list for authenticated user' do + success Entities::BasicProjectDetails + end + params do + optional :simple, type: Boolean, default: false, + desc: 'Return only the ID, URL, name, and path of each project' + use :filter_params + use :pagination + end get do projects = current_user.authorized_projects projects = filter_projects(projects) - projects = paginate projects entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess - present projects, with: entity, user: current_user + present paginate(projects), with: entity, user: current_user end - # Get a list of visible projects for authenticated user - # - # Example Request: - # GET /projects/visible + desc 'Get a list of visible projects for authenticated user' do + success Entities::BasicProjectDetails + end + params do + optional :simple, type: Boolean, default: false, + desc: 'Return only the ID, URL, name, and path of each project' + use :filter_params + use :pagination + end get '/visible' do projects = ProjectsFinder.new.execute(current_user) projects = filter_projects(projects) - projects = paginate projects entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess - present projects, with: entity, user: current_user + present paginate(projects), with: entity, user: current_user end - # Get an owned projects list for authenticated user - # - # Example Request: - # GET /projects/owned + desc 'Get an owned projects list for authenticated user' do + success Entities::BasicProjectDetails + end + params do + use :filter_params + use :pagination + end get '/owned' do projects = current_user.owned_projects projects = filter_projects(projects) - projects = paginate projects - present projects, with: Entities::ProjectWithAccess, user: current_user + + present paginate(projects), with: Entities::ProjectWithAccess, user: current_user end - # Gets starred project for the authenticated user - # - # Example Request: - # GET /projects/starred + desc 'Gets starred project for the authenticated user' do + success Entities::BasicProjectDetails + end + params do + use :filter_params + use :pagination + end get '/starred' do projects = current_user.viewable_starred_projects projects = filter_projects(projects) - projects = paginate projects - present projects, with: Entities::Project, user: current_user + + present paginate(projects), with: Entities::Project, user: current_user end - # Get all projects for admin user - # - # Example Request: - # GET /projects/all + desc 'Get all projects for admin user' do + success Entities::BasicProjectDetails + end + params do + use :filter_params + use :pagination + end get '/all' do authenticated_as_admin! projects = Project.all projects = filter_projects(projects) - projects = paginate projects - present projects, with: Entities::ProjectWithAccess, user: current_user + + present paginate(projects), with: Entities::ProjectWithAccess, user: current_user end - # Get a single project - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # GET /projects/:id - get ":id" do - present user_project, with: Entities::ProjectWithAccess, user: current_user, - user_can_admin_project: can?(current_user, :admin_project, user_project) + desc 'Search for projects the current user has access to' do + success Entities::Project + end + params do + requires :query, type: String, desc: 'The project name to be searched' + use :sort_params + use :pagination end + get "/search/:query" do + search_service = Search::GlobalService.new(current_user, search: params[:query]).execute + projects = search_service.objects('projects', params[:page]) + projects = projects.reorder(params[:order_by] => params[:sort]) - # Get events for a single project - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # GET /projects/:id/events - get ":id/events" do - events = paginate user_project.events.recent - present events, with: Entities::Event - end - - # Create new project - # - # Parameters: - # name (required) - name for new project - # description (optional) - short project description - # issues_enabled (optional) - # merge_requests_enabled (optional) - # builds_enabled (optional) - # wiki_enabled (optional) - # snippets_enabled (optional) - # container_registry_enabled (optional) - # shared_runners_enabled (optional) - # namespace_id (optional) - defaults to user namespace - # public (optional) - if true same as setting visibility_level = 20 - # visibility_level (optional) - 0 by default - # import_url (optional) - # public_builds (optional) - # lfs_enabled (optional) - # request_access_enabled (optional) - Allow users to request member access - # Example Request - # POST /projects + present paginate(projects), with: Entities::Project + end + + desc 'Create new project' do + success Entities::Project + end + params do + requires :name, type: String, desc: 'The name of the project' + optional :path, type: String, desc: 'The path of the repository' + use :optional_params + use :create_params + end post do - required_attributes! [:name] - attrs = attributes_for_keys [:builds_enabled, - :container_registry_enabled, - :description, - :import_url, - :issues_enabled, - :lfs_enabled, - :merge_requests_enabled, - :name, - :namespace_id, - :only_allow_merge_if_build_succeeds, - :path, - :public, - :public_builds, - :request_access_enabled, - :shared_runners_enabled, - :snippets_enabled, - :visibility_level, - :wiki_enabled, - :only_allow_merge_if_all_discussions_are_resolved] - attrs = map_public_to_visibility_level(attrs) - @project = ::Projects::CreateService.new(current_user, attrs).execute - if @project.saved? - present @project, with: Entities::Project, - user_can_admin_project: can?(current_user, :admin_project, @project) + attrs = map_public_to_visibility_level(declared_params(include_missing: false)) + project = ::Projects::CreateService.new(current_user, attrs).execute + + if project.saved? + present project, with: Entities::Project, + user_can_admin_project: can?(current_user, :admin_project, project) else - if @project.errors[:limit_reached].present? - error!(@project.errors[:limit_reached], 403) + if project.errors[:limit_reached].present? + error!(project.errors[:limit_reached], 403) end - render_validation_error!(@project) + render_validation_error!(project) end end - # Create new project for a specified user. Only available to admin users. - # - # Parameters: - # user_id (required) - The ID of a user - # name (required) - name for new project - # description (optional) - short project description - # default_branch (optional) - 'master' by default - # issues_enabled (optional) - # merge_requests_enabled (optional) - # builds_enabled (optional) - # wiki_enabled (optional) - # snippets_enabled (optional) - # container_registry_enabled (optional) - # shared_runners_enabled (optional) - # public (optional) - if true same as setting visibility_level = 20 - # visibility_level (optional) - # import_url (optional) - # public_builds (optional) - # lfs_enabled (optional) - # request_access_enabled (optional) - Allow users to request member access - # Example Request - # POST /projects/user/:user_id + desc 'Create new project for a specified user. Only available to admin users.' do + success Entities::Project + end + params do + requires :name, type: String, desc: 'The name of the project' + requires :user_id, type: Integer, desc: 'The ID of a user' + optional :default_branch, type: String, desc: 'The default branch of the project' + use :optional_params + use :create_params + end post "user/:user_id" do authenticated_as_admin! - user = User.find(params[:user_id]) - attrs = attributes_for_keys [:builds_enabled, - :default_branch, - :description, - :import_url, - :issues_enabled, - :lfs_enabled, - :merge_requests_enabled, - :name, - :only_allow_merge_if_build_succeeds, - :public, - :public_builds, - :request_access_enabled, - :shared_runners_enabled, - :snippets_enabled, - :visibility_level, - :wiki_enabled, - :only_allow_merge_if_all_discussions_are_resolved] - attrs = map_public_to_visibility_level(attrs) - @project = ::Projects::CreateService.new(user, attrs).execute - if @project.saved? - present @project, with: Entities::Project, - user_can_admin_project: can?(current_user, :admin_project, @project) + user = User.find_by(id: params.delete(:user_id)) + not_found!('User') unless user + + attrs = map_public_to_visibility_level(declared_params(include_missing: false)) + project = ::Projects::CreateService.new(user, attrs).execute + + if project.saved? + present project, with: Entities::Project, + user_can_admin_project: can?(current_user, :admin_project, project) else - render_validation_error!(@project) + render_validation_error!(project) end end + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: { id: /[^\/]+/ } do + desc 'Get a single project' do + success Entities::ProjectWithAccess + end + get ":id" do + present user_project, with: Entities::ProjectWithAccess, user: current_user, + user_can_admin_project: can?(current_user, :admin_project, user_project) + end + + desc 'Get events for a single project' do + success Entities::Event + end + params do + use :pagination + end + get ":id/events" do + present paginate(user_project.events.recent), with: Entities::Event + end - # Fork new project for the current user or provided namespace. - # - # Parameters: - # id (required) - The ID of a project - # namespace (optional) - The ID or name of the namespace that the project will be forked into. - # Example Request - # POST /projects/fork/:id + desc 'Fork new project for the current user or provided namespace.' do + success Entities::Project + end + params do + optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be forked into' + end post 'fork/:id' do - attrs = {} - namespace_id = params[:namespace] + fork_params = declared_params(include_missing: false) + namespace_id = fork_params[:namespace] if namespace_id.present? - namespace = Namespace.find_by(id: namespace_id) || Namespace.find_by_path_or_name(namespace_id) + fork_params[:namespace] = if namespace_id =~ /^\d+$/ + Namespace.find_by(id: namespace_id) + else + Namespace.find_by_path_or_name(namespace_id) + end - unless namespace && can?(current_user, :create_projects, namespace) + unless fork_params[:namespace] && can?(current_user, :create_projects, fork_params[:namespace]) not_found!('Target Namespace') end - - attrs[:namespace] = namespace end - @forked_project = - ::Projects::ForkService.new(user_project, - current_user, - attrs).execute + forked_project = ::Projects::ForkService.new(user_project, current_user, fork_params).execute - if @forked_project.errors.any? - conflict!(@forked_project.errors.messages) + if forked_project.errors.any? + conflict!(forked_project.errors.messages) else - present @forked_project, with: Entities::Project, - user_can_admin_project: can?(current_user, :admin_project, @forked_project) + present forked_project, with: Entities::Project, + user_can_admin_project: can?(current_user, :admin_project, forked_project) end end - # Update an existing project - # - # Parameters: - # id (required) - the id of a project - # name (optional) - name of a project - # path (optional) - path of a project - # description (optional) - short project description - # issues_enabled (optional) - # merge_requests_enabled (optional) - # builds_enabled (optional) - # wiki_enabled (optional) - # snippets_enabled (optional) - # container_registry_enabled (optional) - # shared_runners_enabled (optional) - # public (optional) - if true same as setting visibility_level = 20 - # visibility_level (optional) - visibility level of a project - # public_builds (optional) - # lfs_enabled (optional) - # Example Request - # PUT /projects/:id + desc 'Update an existing project' do + success Entities::Project + end + params do + optional :name, type: String, desc: 'The name of the project' + optional :default_branch, type: String, desc: 'The default branch of the project' + optional :path, type: String, desc: 'The path of the repository' + use :optional_params + at_least_one_of :name, :description, :issues_enabled, :merge_requests_enabled, + :wiki_enabled, :builds_enabled, :snippets_enabled, + :shared_runners_enabled, :container_registry_enabled, + :lfs_enabled, :public, :visibility_level, :public_builds, + :request_access_enabled, :only_allow_merge_if_build_succeeds, + :only_allow_merge_if_all_discussions_are_resolved, :path, + :default_branch + end put ':id' do - attrs = attributes_for_keys [:builds_enabled, - :container_registry_enabled, - :default_branch, - :description, - :issues_enabled, - :lfs_enabled, - :merge_requests_enabled, - :name, - :only_allow_merge_if_build_succeeds, - :path, - :public, - :public_builds, - :request_access_enabled, - :shared_runners_enabled, - :snippets_enabled, - :visibility_level, - :wiki_enabled, - :only_allow_merge_if_all_discussions_are_resolved] - attrs = map_public_to_visibility_level(attrs) authorize_admin_project + attrs = map_public_to_visibility_level(declared_params(include_missing: false)) authorize! :rename_project, user_project if attrs[:name].present? - if attrs[:visibility_level].present? - authorize! :change_visibility_level, user_project - end + authorize! :change_visibility_level, user_project if attrs[:visibility_level].present? - ::Projects::UpdateService.new(user_project, - current_user, attrs).execute + ::Projects::UpdateService.new(user_project, current_user, attrs).execute if user_project.errors.any? render_validation_error!(user_project) @@ -297,12 +291,9 @@ module API end end - # Archive project - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # PUT /projects/:id/archive + desc 'Archive a project' do + success Entities::Project + end post ':id/archive' do authorize!(:archive_project, user_project) @@ -311,12 +302,9 @@ module API present user_project, with: Entities::Project end - # Unarchive project - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # PUT /projects/:id/unarchive + desc 'Unarchive a project' do + success Entities::Project + end post ':id/unarchive' do authorize!(:archive_project, user_project) @@ -325,12 +313,9 @@ module API present user_project, with: Entities::Project end - # Star project - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # POST /projects/:id/star + desc 'Star a project' do + success Entities::Project + end post ':id/star' do if current_user.starred?(user_project) not_modified! @@ -342,12 +327,9 @@ module API end end - # Unstar project - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # DELETE /projects/:id/star + desc 'Unstar a project' do + success Entities::Project + end delete ':id/star' do if current_user.starred?(user_project) current_user.toggle_star(user_project) @@ -359,67 +341,51 @@ module API end end - # Remove project - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # DELETE /projects/:id + desc 'Remove a project' delete ":id" do authorize! :remove_project, user_project ::Projects::DestroyService.new(user_project, current_user, {}).async_execute end - # Mark this project as forked from another - # - # Parameters: - # id: (required) - The ID of the project being marked as a fork - # forked_from_id: (required) - The ID of the project it was forked from - # Example Request: - # POST /projects/:id/fork/:forked_from_id + desc 'Mark this project as forked from another' + params do + requires :forked_from_id, type: String, desc: 'The ID of the project it was forked from' + end post ":id/fork/:forked_from_id" do authenticated_as_admin! + forked_from_project = find_project!(params[:forked_from_id]) - unless forked_from_project.nil? - if user_project.forked_from_project.nil? - user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id) - else - render_api_error!("Project already forked", 409) - end + not_found!("Source Project") unless forked_from_project + + if user_project.forked_from_project.nil? + user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id) else - not_found!("Source Project") + render_api_error!("Project already forked", 409) end end - # Remove a forked_from relationship - # - # Parameters: - # id: (required) - The ID of the project being marked as a fork - # Example Request: - # DELETE /projects/:id/fork + desc 'Remove a forked_from relationship' delete ":id/fork" do authorize! :remove_fork_project, user_project + if user_project.forked? user_project.forked_project_link.destroy + else + not_modified! end end - # Share project with group - # - # Parameters: - # id (required) - The ID of a project - # group_id (required) - The ID of a group - # group_access (required) - Level of permissions for sharing - # expires_at (optional) - Share expiration date - # - # Example Request: - # POST /projects/:id/share + desc 'Share the project with a group' do + success Entities::ProjectGroupLink + end + params do + requires :group_id, type: Integer, desc: 'The ID of a group' + requires :group_access, type: Integer, values: Gitlab::Access.values, desc: 'The group access level' + optional :expires_at, type: Date, desc: 'Share expiration date' + end post ":id/share" do authorize! :admin_project, user_project - required_attributes! [:group_id, :group_access] - attrs = attributes_for_keys [:group_id, :group_access, :expires_at] - - group = Group.find_by_id(attrs[:group_id]) + group = Group.find_by_id(params[:group_id]) unless group && can?(current_user, :read_group, group) not_found!('Group') @@ -429,7 +395,7 @@ module API return render_api_error!("The project sharing with group is disabled", 400) end - link = user_project.project_group_links.new(attrs) + link = user_project.project_group_links.new(declared_params(include_missing: false)) if link.save present link, with: Entities::ProjectGroupLink @@ -451,40 +417,26 @@ module API no_content! end - # Upload a file - # - # Parameters: - # id: (required) - The ID of the project - # file: (required) - The file to be uploaded + desc 'Upload a file' + params do + requires :file, type: File, desc: 'The file to be uploaded' + end post ":id/uploads" do ::Projects::UploadService.new(user_project, params[:file]).execute end - # search for projects current_user has access to - # - # Parameters: - # query (required) - A string contained in the project name - # per_page (optional) - number of projects to return per page - # page (optional) - the page to retrieve - # Example Request: - # GET /projects/search/:query - get "/search/:query" do - search_service = Search::GlobalService.new(current_user, search: params[:query]).execute - projects = search_service.objects('projects', params[:page]) - projects = projects.reorder(project_order_by => project_sort) - - present paginate(projects), with: Entities::Project + desc 'Get the users list of a project' do + success Entities::UserBasic + end + params do + optional :search, type: String, desc: 'Return list of users matching the search criteria' + use :pagination end - - # Get a users list - # - # Example Request: - # GET /users get ':id/users' do - @users = User.where(id: user_project.team.users.map(&:id)) - @users = @users.search(params[:search]) if params[:search].present? - @users = paginate @users - present @users, with: Entities::UserBasic + users = User.where(id: user_project.team.users.map(&:id)) + users = users.search(params[:search]) if params[:search].present? + + present paginate(users), with: Entities::UserBasic end end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index e53ee2a4e76..482e81b29a6 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -415,16 +415,7 @@ describe API::API, api: true do not_to change { Project.count } expect(response).to have_http_status(400) - expect(json_response['message']['name']).to eq([ - 'can\'t be blank', - 'is too short (minimum is 0 characters)', - Gitlab::Regex.project_name_regex_message - ]) - expect(json_response['message']['path']).to eq([ - 'can\'t be blank', - 'is too short (minimum is 0 characters)', - Gitlab::Regex.send(:project_path_regex_message) - ]) + expect(json_response['error']).to eq('name is missing') end it 'assigns attributes to project' do @@ -438,6 +429,7 @@ describe API::API, api: true do post api("/projects/user/#{user.id}", admin), project + expect(response).to have_http_status(201) project.each_pair do |k, v| next if %i[has_external_issue_tracker path].include?(k) expect(json_response[k.to_s]).to eq(v) @@ -447,6 +439,8 @@ describe API::API, api: true do it 'sets a project as public' do project = attributes_for(:project, :public) post api("/projects/user/#{user.id}", admin), project + + expect(response).to have_http_status(201) expect(json_response['public']).to be_truthy expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) end @@ -454,6 +448,8 @@ describe API::API, api: true do it 'sets a project as public using :public' do project = attributes_for(:project, { public: true }) post api("/projects/user/#{user.id}", admin), project + + expect(response).to have_http_status(201) expect(json_response['public']).to be_truthy expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) end @@ -461,6 +457,8 @@ describe API::API, api: true do it 'sets a project as internal' do project = attributes_for(:project, :internal) post api("/projects/user/#{user.id}", admin), project + + expect(response).to have_http_status(201) expect(json_response['public']).to be_falsey expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) end @@ -468,6 +466,7 @@ describe API::API, api: true do it 'sets a project as internal overriding :public' do project = attributes_for(:project, :internal, { public: true }) post api("/projects/user/#{user.id}", admin), project + expect(response).to have_http_status(201) expect(json_response['public']).to be_falsey expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) end @@ -848,7 +847,7 @@ describe API::API, api: true do it 'is 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).to have_http_status(200) + expect(response).to have_http_status(304) expect(project_fork_target.reload.forked_from_project).to be_nil end end @@ -865,7 +864,7 @@ describe API::API, api: true do post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at end.to change { ProjectGroupLink.count }.by(1) - expect(response.status).to eq 201 + expect(response).to have_http_status(201) expect(json_response['group_id']).to eq(group.id) expect(json_response['group_access']).to eq(Gitlab::Access::DEVELOPER) expect(json_response['expires_at']).to eq(expires_at.to_s) @@ -873,18 +872,18 @@ describe API::API, api: true do it "returns a 400 error when group id is not given" do post api("/projects/#{project.id}/share", user), group_access: Gitlab::Access::DEVELOPER - expect(response.status).to eq 400 + expect(response).to have_http_status(400) end it "returns a 400 error when access level is not given" do post api("/projects/#{project.id}/share", user), group_id: group.id - expect(response.status).to eq 400 + expect(response).to have_http_status(400) end it "returns a 400 error when sharing is disabled" do project.namespace.update(share_with_group_lock: true) post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER - expect(response.status).to eq 400 + expect(response).to have_http_status(400) end it 'returns a 404 error when user cannot read group' do @@ -892,19 +891,20 @@ describe API::API, api: true do post api("/projects/#{project.id}/share", user), group_id: private_group.id, group_access: Gitlab::Access::DEVELOPER - expect(response.status).to eq 404 + expect(response).to have_http_status(404) end it 'returns a 404 error when group does not exist' do post api("/projects/#{project.id}/share", user), group_id: 1234, group_access: Gitlab::Access::DEVELOPER - expect(response.status).to eq 404 + expect(response).to have_http_status(404) end - it "returns a 409 error when wrong params passed" do + it "returns a 400 error when wrong params passed" do post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234 - expect(response.status).to eq 409 - expect(json_response['message']).to eq 'Group access is not included in the list' + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq 'group_access does not have a valid value' end end @@ -1017,7 +1017,6 @@ describe API::API, api: true do it 'updates visibility_level from public to private' do project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC }) - project_param = { public: false } put api("/projects/#{project3.id}", user), project_param expect(response).to have_http_status(200) -- cgit v1.2.1 From ac761e450919b7a2d2dd76e08afe2a877e878e61 Mon Sep 17 00:00:00 2001 From: Dan Dedrick <ddedrick@lexmark.com> Date: Mon, 28 Nov 2016 15:49:07 -0500 Subject: Fix broken README.md UX guide link. Replace broken link to UX guide with new working link in the README.md file. --- README.md | 2 +- changelogs/unreleased/readme-link-fix.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/readme-link-fix.yml diff --git a/README.md b/README.md index f63543ca39d..c9bb5af6da2 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ For more information please see the [architecture documentation](https://docs.gi ## UX design -Please adhere to the [UX Guide](doc/development/ux_guide/readme.md) when creating designs and implementing code. +Please adhere to the [UX Guide](doc/development/ux_guide/index.md) when creating designs and implementing code. ## Third-party applications diff --git a/changelogs/unreleased/readme-link-fix.yml b/changelogs/unreleased/readme-link-fix.yml new file mode 100644 index 00000000000..211d3b80c3a --- /dev/null +++ b/changelogs/unreleased/readme-link-fix.yml @@ -0,0 +1,4 @@ +--- +title: Fix broken README.md UX guide link. +merge_request: +author: -- cgit v1.2.1 From b834ecd2f6c3ce28e343f6c82b333a504ee125eb Mon Sep 17 00:00:00 2001 From: Chris Peressini <cperessini@gitlab.com> Date: Mon, 28 Nov 2016 16:38:05 +0000 Subject: Add darker active state for outline buttons and new border colors. --- app/assets/stylesheets/framework/buttons.scss | 24 +++++++++++++++--------- app/assets/stylesheets/framework/variables.scss | 2 +- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 4a9aa0f8717..2f52588dc18 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -15,7 +15,7 @@ @include btn-default; } -@mixin btn-outline($background, $text, $border, $hover-background, $hover-text, $hover-border) { +@mixin btn-outline($background, $text, $border, $hover-background, $hover-text, $hover-border, $active-background, $active-border) { background-color: $background; color: $text; border-color: $border; @@ -23,8 +23,14 @@ &:hover, &:focus { background-color: $hover-background; - color: $hover-text; border-color: $hover-border; + color: $hover-text; + } + + &:active { + background-color: $active-background; + border-color: $active-border; + color: $hover-text; } } @@ -82,11 +88,11 @@ } @mixin btn-gray { - @include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-light, $gray-dark, $border-gray-dark, $gl-gray-dark); + @include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-gray-dark); } @mixin btn-white { - @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-dark, $btn-white-active); + @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-dark, $gl-text-color); } @mixin btn-with-margin { @@ -139,11 +145,11 @@ &.btn-new, &.btn-create, &.btn-save { - @include btn-outline($white-light, $green-normal, $green-normal, $green-light, $white-light, $green-light); + @include btn-outline($white-light, $border-green-light, $border-green-light, $green-light, $white-light, $border-green-light, $green-normal, $border-green-normal); } &.btn-remove { - @include btn-outline($white-light, $red-normal, $red-normal, $red-light, $white-light, $red-light); + @include btn-outline($white-light, $border-red-light, $border-red-light, $red-light, $white-light, $border-red-light, $red-normal, $border-red-normal); } } @@ -165,11 +171,11 @@ } &.btn-close { - @include btn-outline($white-light, $orange-normal, $orange-normal, $orange-light, $white-light, $orange-light); + @include btn-outline($white-light, $border-orange-light, $border-orange-light, $orange-light, $white-light, $border-orange-light, $orange-normal, $border-orange-normal); } &.btn-spam { - @include btn-outline($white-light, $red-normal, $red-normal, $red-light, $white-light, $red-light); + @include btn-outline($white-light, $border-red-light, $border-red-light, $red-light, $white-light, $border-red-light, $red-normal, $border-red-normal); } &.btn-danger, @@ -351,7 +357,7 @@ .btn-inverted { &-secondary { - @include btn-outline($white-light, $blue-normal, $blue-normal, $blue-light, $white-light, $blue-light); + @include btn-outline($white-light, $border-blue-light, $border-blue-light, $blue-light, $white-light, $border-blue-light, $blue-normal, $border-blue-normal); } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 2539c841111..b259e7eae3e 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -12,7 +12,7 @@ $sidebar-breakpoint: 1024px; /* * Color schema */ - $darken-normal-factor: 7%; +$darken-normal-factor: 7%; $darken-dark-factor: 10%; $darken-border-factor: 5%; -- cgit v1.2.1 From b62e2bedbfa49aacfc4847049aa589f045af15ce Mon Sep 17 00:00:00 2001 From: Ruben Davila <rdavila84@gmail.com> Date: Mon, 28 Nov 2016 17:00:03 -0500 Subject: Add new configuration setting to enable/disable HTML emails. This new global setting will allow admins to specify if HTML emails should be sent or not, this is basically useful when system administrators want to save some disk space by avoiding emails in HTML format and using only the Plain Text version. --- .../admin/application_settings_controller.rb | 1 + .../admin/application_settings/_form.html.haml | 11 ++++++- .../7749-add-setting-to-disable-html-emails.yml | 3 ++ config/initializers/email_template_interceptor.rb | 2 ++ ..._html_emails_enabled_to_application_settings.rb | 29 ++++++++++++++++++ db/schema.rb | 3 +- lib/email_template_interceptor.rb | 13 +++++++++ spec/mailers/notify_spec.rb | 34 ++++++++++++++++++++++ 8 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/7749-add-setting-to-disable-html-emails.yml create mode 100644 config/initializers/email_template_interceptor.rb create mode 100644 db/migrate/20161128161412_add_html_emails_enabled_to_application_settings.rb create mode 100644 lib/email_template_interceptor.rb diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index b81842e319b..c2bb8464824 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -112,6 +112,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :koding_enabled, :koding_url, :email_author_in_body, + :html_emails_enabled, :repository_checks_enabled, :metrics_packet_size, :send_user_confirmation_email, diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index ce803f329f9..7accd2529af 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -443,7 +443,16 @@ Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead. - + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :html_emails_enabled do + = f.check_box :html_emails_enabled + Enable HTML emails + .help-block + By default GitLab sends emails in HTML and plain text formats so mail + clients can choose what format to use. Disable this option if you only + want to send emails in plain text format. %fieldset %legend Automatic Git repository housekeeping .form-group diff --git a/changelogs/unreleased/7749-add-setting-to-disable-html-emails.yml b/changelogs/unreleased/7749-add-setting-to-disable-html-emails.yml new file mode 100644 index 00000000000..9dd04d3f089 --- /dev/null +++ b/changelogs/unreleased/7749-add-setting-to-disable-html-emails.yml @@ -0,0 +1,3 @@ +title: Add setting to enable/disable HTML emails +merge_request: 7749 +author: diff --git a/config/initializers/email_template_interceptor.rb b/config/initializers/email_template_interceptor.rb new file mode 100644 index 00000000000..f195ca9bcd6 --- /dev/null +++ b/config/initializers/email_template_interceptor.rb @@ -0,0 +1,2 @@ +# Interceptor in lib/email_template_interceptor.rb +ActionMailer::Base.register_interceptor(EmailTemplateInterceptor) diff --git a/db/migrate/20161128161412_add_html_emails_enabled_to_application_settings.rb b/db/migrate/20161128161412_add_html_emails_enabled_to_application_settings.rb new file mode 100644 index 00000000000..1c59241d0fe --- /dev/null +++ b/db/migrate/20161128161412_add_html_emails_enabled_to_application_settings.rb @@ -0,0 +1,29 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddHtmlEmailsEnabledToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + add_column :application_settings, :html_emails_enabled, :boolean, default: true + end +end diff --git a/db/schema.rb b/db/schema.rb index b3c49b52597..3d630a148f0 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: 20161118183841) do +ActiveRecord::Schema.define(version: 20161128161412) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -106,6 +106,7 @@ ActiveRecord::Schema.define(version: 20161118183841) do t.integer "housekeeping_incremental_repack_period", default: 10, null: false t.integer "housekeeping_full_repack_period", default: 50, null: false t.integer "housekeeping_gc_period", default: 200, null: false + t.boolean "html_emails_enabled", default: true end create_table "audit_events", force: :cascade do |t| diff --git a/lib/email_template_interceptor.rb b/lib/email_template_interceptor.rb new file mode 100644 index 00000000000..fb04a7824b8 --- /dev/null +++ b/lib/email_template_interceptor.rb @@ -0,0 +1,13 @@ +# Read about interceptors in http://guides.rubyonrails.org/action_mailer_basics.html#intercepting-emails +class EmailTemplateInterceptor + include Gitlab::CurrentSettings + + def self.delivering_email(message) + # Remove HTML part if HTML emails are disabled. + unless current_application_settings.html_emails_enabled + message.part.delete_if do |part| + part.content_type.try(:start_with?, 'text/html') + end + end + end +end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 932a5dc4862..0ad1cefbacc 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -1099,4 +1099,38 @@ describe Notify do is_expected.to have_body_text /#{diff_path}/ end end + + describe 'HTML emails setting' do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:multipart_mail) { Notify.project_was_moved_email(project.id, user.id, "gitlab/gitlab") } + + context 'when disabled' do + it 'only sends the text template' do + stub_application_setting(html_emails_enabled: false) + + EmailTemplateInterceptor.delivering_email(multipart_mail) + + expect(multipart_mail).to have_part_with('text/plain') + expect(multipart_mail).not_to have_part_with('text/html') + end + end + + context 'when enabled' do + it 'sends a multipart message' do + stub_application_setting(html_emails_enabled: true) + + EmailTemplateInterceptor.delivering_email(multipart_mail) + + expect(multipart_mail).to have_part_with('text/plain') + expect(multipart_mail).to have_part_with('text/html') + end + end + + matcher :have_part_with do |expected| + match do |actual| + actual.body.parts.any? { |part| part.content_type.try(:match, %r(#{expected})) } + end + end + end end -- cgit v1.2.1 From 59fa98dd8462fa2f7865828275b5e80e362e4a6e Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Tue, 29 Nov 2016 01:13:54 +0300 Subject: Enable ESLint and fix minor code style stuff in project_variables.js.es6. --- app/assets/javascripts/project_variables.js.es6 | 41 ++++++++++++------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/app/assets/javascripts/project_variables.js.es6 b/app/assets/javascripts/project_variables.js.es6 index 6c905f58c85..4ee2e49306d 100644 --- a/app/assets/javascripts/project_variables.js.es6 +++ b/app/assets/javascripts/project_variables.js.es6 @@ -1,44 +1,43 @@ -/* eslint-disable */ -((global) => { +(() => { const HIDDEN_VALUE_TEXT = '******'; class ProjectVariables { constructor() { - this.$reveal = $('.js-btn-toggle-reveal-values'); - - this.$reveal.on('click', this.toggleRevealState.bind(this)); + this.$revealBtn = $('.js-btn-toggle-reveal-values'); + this.$revealBtn.on('click', this.toggleRevealState.bind(this)); } - toggleRevealState(event) { - event.preventDefault(); + toggleRevealState(e) { + e.preventDefault(); - const $btn = $(event.currentTarget); - const oldStatus = $btn.attr('data-status'); + const oldStatus = this.$revealBtn.attr('data-status'); + let newStatus = 'hidden'; + let newAction = 'Reveal Values'; - if (oldStatus == 'hidden') { - [newStatus, newAction] = ['revealed', 'Hide Values']; - } else { - [newStatus, newAction] = ['hidden', 'Reveal Values']; + if (oldStatus === 'hidden') { + newStatus = 'revealed'; + newAction = 'Hide Values'; } - $btn.attr('data-status', newStatus); + this.$revealBtn.attr('data-status', newStatus); - let $variables = $('.variable-value'); + const $variables = $('.variable-value'); - $variables.each(function (_, variable) { - let $variable = $(variable); + $variables.each((_, variable) => { + const $variable = $(variable); let newText = HIDDEN_VALUE_TEXT; - if (newStatus == 'revealed') { + if (newStatus === 'revealed') { newText = $variable.attr('data-value'); } $variable.text(newText); }); - $btn.text(newAction); + this.$revealBtn.text(newAction); } } - global.ProjectVariables = ProjectVariables; -})(window.gl || (window.gl = {})); + window.gl = window.gl || {}; + window.gl.ProjectVariables = ProjectVariables; +})(); -- cgit v1.2.1 From b3ed4e0cf9dc66b49fba933455212d9c89b80d90 Mon Sep 17 00:00:00 2001 From: David Wagner <david@marvid.fr> Date: Fri, 18 Nov 2016 21:19:44 +0100 Subject: Homogenize dropdowns on Issue page Make sort and filter dropdowns look the same and tweak their icon and colors according to #24150. Signed-off-by: David Wagner <david@marvid.fr> --- app/assets/stylesheets/framework/buttons.scss | 2 +- app/assets/stylesheets/framework/dropdowns.scss | 87 ++++++++++++++-------- app/assets/stylesheets/framework/variables.scss | 12 +-- app/helpers/dropdowns_helper.rb | 2 +- app/views/shared/_sort_dropdown.html.haml | 4 +- .../shared/issuable/_label_dropdown.html.haml | 2 +- features/steps/shared/issuable.rb | 6 +- 7 files changed, 72 insertions(+), 43 deletions(-) diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 4a9aa0f8717..ffebef559c2 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -199,7 +199,7 @@ } .fa-caret-down, - .fa-caret-up { + .fa-chevron-down { margin-left: 5px; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 583c17e4a83..d7df1d91afc 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -21,51 +21,23 @@ .dropdown-menu-toggle { border-color: $dropdown-toggle-hover-border-color; - - .fa { - color: $dropdown-toggle-hover-icon-color; - } } } -.dropdown-menu-toggle { - position: relative; - width: 160px; - padding: 6px 20px 6px 10px; +.dropdown-toggle { + padding: 6px 8px 6px 10px; background-color: $dropdown-toggle-bg; color: $dropdown-toggle-color; font-size: 15px; text-align: left; border: 1px solid $border-color; border-radius: $border-radius-base; - text-overflow: ellipsis; white-space: nowrap; - overflow: hidden; - - .fa { - position: absolute; - top: 10px; - right: 8px; - color: $dropdown-toggle-icon-color; - - &.fa-spinner { - font-size: 16px; - margin-top: -8px; - } - } &.no-outline { outline: 0; } - &:hover, { - border-color: $dropdown-toggle-hover-border-color; - - .fa { - color: $dropdown-toggle-hover-icon-color; - } - } - &.large { width: 200px; } @@ -86,6 +58,61 @@ max-width: 100%; padding-right: 25px; } + + .fa { + color: $dropdown-toggle-icon-color; + } + + .fa-chevron-down { + font-size: $dropdown-chevron-size; + position: relative; + top: -3px; + margin-left: 5px; + } + + @mixin chevron-hover { + .fa-chevron-down { + color: $dropdown-toggle-hover-icon-color; + } + } + + &:hover { + @include chevron-hover; + + border-color: $dropdown-toggle-hover-border-color; + } + + &:focus:active { + @include chevron-hover; + + border-color: $dropdown-toggle-active-border-color; + } +} + +.dropdown-menu-toggle { + @extend .dropdown-toggle; + + padding-right: 20px; + + position: relative; + width: 160px; + text-overflow: ellipsis; + overflow: hidden; + + .fa { + position: absolute; + + &.fa-spinner { + font-size: 16px; + margin-top: -8px; + } + } + + .fa-chevron-down { + position: absolute; + top: 11px; + right: 8px; + } } .dropdown-menu, diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 750d99ebabe..88d6c3570c5 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -216,7 +216,7 @@ $dropdown-bg: #fff; $dropdown-link-color: #555; $dropdown-link-hover-bg: $row-hover; $dropdown-empty-row-bg: rgba(#000, .04); -$dropdown-border-color: rgba(#000, .1); +$dropdown-border-color: $border-color; $dropdown-shadow-color: rgba(#000, .1); $dropdown-divider-color: rgba(#000, .1); $dropdown-header-color: #959494; @@ -225,13 +225,15 @@ $dropdown-input-color: #555; $dropdown-input-focus-border: $focus-border-color; $dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, .4); $dropdown-loading-bg: rgba(#fff, .6); +$dropdown-chevron-size: 10px; $dropdown-toggle-bg: #fff; -$dropdown-toggle-color: #626262; -$dropdown-toggle-border-color: #eaeaea; -$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%); +$dropdown-toggle-color: #5c5c5c; +$dropdown-toggle-border-color: #e5e5e5; +$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 13%); +$dropdown-toggle-active-border-color: darken($dropdown-toggle-border-color, 14%); $dropdown-toggle-icon-color: #c4c4c4; -$dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color; +$dropdown-toggle-hover-icon-color: darken($dropdown-toggle-icon-color, 7%); /* * Buttons diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index cbab1fd5967..81e0b6bb5ae 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -43,7 +43,7 @@ module DropdownsHelper default_label = data_attr[:default_label] content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}") - output << icon('caret-down') + output << icon('chevron-down') output.html_safe end end diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml index 68e05cb72e1..ede3c7090d7 100644 --- a/app/views/shared/_sort_dropdown.html.haml +++ b/app/views/shared/_sort_dropdown.html.haml @@ -1,11 +1,11 @@ .dropdown.inline.prepend-left-10 - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle{type: 'button', data: {toggle: 'dropdown'}} %span.light - if @sort.present? = sort_options_hash[@sort] - else = sort_title_recently_created - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort %li = link_to page_filter_path(sort: sort_value_priority, label: true) do diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index 1d778bc88de..22b5a6aa11b 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -22,7 +22,7 @@ %button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data} %span.dropdown-toggle-text{ class: ("is-default" if selected.nil? || selected.empty?) } = multi_label_name(selected, "Labels") - = icon('caret-down') + = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable = render partial: "shared/issuable/label_page_default", locals: { title: dropdown_title, show_footer: show_footer, show_create: show_create } - if show_create && project && can?(current_user, :admin_label, project) diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb index aa666a954bc..79dde620265 100644 --- a/features/steps/shared/issuable.rb +++ b/features/steps/shared/issuable.rb @@ -110,14 +110,14 @@ module SharedIssuable end step 'I sort the list by "Oldest updated"' do - find('button.dropdown-toggle.btn').click + find('button.dropdown-toggle').click page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do click_link "Oldest updated" end end step 'I sort the list by "Least popular"' do - find('button.dropdown-toggle.btn').click + find('button.dropdown-toggle').click page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do click_link 'Least popular' @@ -125,7 +125,7 @@ module SharedIssuable end step 'I sort the list by "Most popular"' do - find('button.dropdown-toggle.btn').click + find('button.dropdown-toggle').click page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do click_link 'Most popular' -- cgit v1.2.1 From 4c6468aa1077eec8e953734f410ecff680f17caf Mon Sep 17 00:00:00 2001 From: David Wagner <david@marvid.fr> Date: Tue, 22 Nov 2016 20:33:52 +0100 Subject: Make open and hovered dropdown toggles look the same The chevron now has the same darker shade when the dropdown is opened it had when hovered on. Signed-off-by: David Wagner <david@marvid.fr> --- app/assets/stylesheets/framework/dropdowns.scss | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index d7df1d91afc..de6e154dfe6 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -8,6 +8,12 @@ } } +@mixin chevron-active { + .fa-chevron-down { + color: $dropdown-toggle-hover-icon-color; + } +} + .open { .dropdown-menu, .dropdown-menu-nav { @@ -19,7 +25,10 @@ } } + .dropdown-toggle, .dropdown-menu-toggle { + @include chevron-active; + border-color: $dropdown-toggle-hover-border-color; } } @@ -70,20 +79,14 @@ margin-left: 5px; } - @mixin chevron-hover { - .fa-chevron-down { - color: $dropdown-toggle-hover-icon-color; - } - } - &:hover { - @include chevron-hover; + @include chevron-active; border-color: $dropdown-toggle-hover-border-color; } &:focus:active { - @include chevron-hover; + @include chevron-active; border-color: $dropdown-toggle-active-border-color; } -- cgit v1.2.1 From a3a85c07aa87148f4baead1ac8d2f2679136c69a Mon Sep 17 00:00:00 2001 From: David Wagner <david@marvid.fr> Date: Tue, 22 Nov 2016 20:56:21 +0100 Subject: Update the Changelog [ci skip] Signed-off-by: David Wagner <david@marvid.fr> --- changelogs/unreleased/24150-consistent-dropdown-styles.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/24150-consistent-dropdown-styles.yml diff --git a/changelogs/unreleased/24150-consistent-dropdown-styles.yml b/changelogs/unreleased/24150-consistent-dropdown-styles.yml new file mode 100644 index 00000000000..a328d796c43 --- /dev/null +++ b/changelogs/unreleased/24150-consistent-dropdown-styles.yml @@ -0,0 +1,4 @@ +--- +title: Homogenize filter and sort dropdown look'n'feel +merge_request: 7583 +author: David Wagner -- cgit v1.2.1 From be0fb391d32bb549d57841009e2dd2bb92de8a78 Mon Sep 17 00:00:00 2001 From: David Wagner <david@marvid.fr> Date: Tue, 22 Nov 2016 22:28:07 +0100 Subject: Update some more sort/filter dropdowns Apart from Issues and Merge Requests pages, there are other sort/filter dropdowns that needed updating. Signed-off-by: David Wagner <david@marvid.fr> --- app/views/dashboard/todos/index.html.haml | 4 ++-- app/views/explore/groups/index.html.haml | 4 ++-- app/views/explore/projects/_filter.html.haml | 8 ++++---- app/views/projects/branches/index.html.haml | 4 ++-- app/views/projects/builds/_sidebar.html.haml | 2 +- app/views/projects/forks/index.html.haml | 4 ++-- app/views/projects/tags/index.html.haml | 4 ++-- app/views/search/_filter.html.haml | 4 ++-- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 472d698486b..62f52086be4 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -50,13 +50,13 @@ data: { data: todo_actions_options }}) .pull-right .dropdown.inline.prepend-left-10 - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'} %span.light - if @sort.present? = sort_options_hash[@sort] - else = sort_title_recently_created - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort %li = link_to todos_filter_path(sort: sort_value_priority) do diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index a1b39d9e1a0..4e5d965ccbe 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -17,13 +17,13 @@ .pull-right .dropdown.inline - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'} %span.light - if @sort.present? = sort_options_hash[@sort] - else = sort_title_recently_created - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right %li = link_to explore_groups_path(sort: sort_value_recently_created) do diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index 4cff14b096b..5ea154c36b4 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -1,13 +1,13 @@ - if current_user .dropdown - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %button.dropdown-toggle{href: '#', "data-toggle" => "dropdown"} = icon('globe') %span.light Visibility: - if params[:visibility_level].present? = visibility_level_label(params[:visibility_level].to_i) - else Any - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right %li = link_to filter_projects_path(visibility_level: nil) do @@ -20,14 +20,14 @@ - if @tags.present? .dropdown - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %button.dropdown-toggle{href: '#', "data-toggle" => "dropdown"} = icon('tags') %span.light Tags: - if params[:tag].present? = params[:tag] - else Any - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right %li = link_to filter_projects_path(tag: nil) do diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 2246316b540..5fd664c7a93 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -12,10 +12,10 @@ = search_field_tag :search, params[:search], { placeholder: 'Filter by branch name', id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false } .dropdown.inline - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'} %span.light = projects_sort_options_hash[@sort] - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right %li = link_to filter_branches_path(sort: sort_value_name) do diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index f5562046953..d5004f6a066 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -116,7 +116,7 @@ .title Stage %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'} %span.stage-selection More - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu - @build.pipeline.stages.each do |stage| %li diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index abf4f697f86..5ee3979c7e7 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -9,13 +9,13 @@ spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' } .dropdown - %button.dropdown-toggle.btn.sort-forks{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'} %span.light sort: - if @sort.present? = sort_options_hash[@sort] - else = sort_title_recently_created - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right %li - excluded_filters = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id] diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index b43b13de4ca..1d39f3a7534 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -12,10 +12,10 @@ = search_field_tag :search, params[:search], { placeholder: 'Filter by tag name', id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false } .dropdown.inline - %button.dropdown-toggle.btn{ type: 'button', data: { toggle: 'dropdown'} } + %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown'} } %span.light = projects_sort_options_hash[@sort] - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right %li = link_to filter_tags_path(sort: sort_value_name) do diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml index ef1c0296d49..938be20c7cf 100644 --- a/app/views/search/_filter.html.haml +++ b/app/views/search/_filter.html.haml @@ -3,7 +3,7 @@ - if params[:project_id].present? = hidden_field_tag :project_id, params[:project_id] .dropdown - %button.dropdown-menu-toggle.btn.js-search-group-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Group:" } } + %button.dropdown-menu-toggle.js-search-group-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Group:" } } %span.dropdown-toggle-text Group: - if @group.present? @@ -18,7 +18,7 @@ = dropdown_loading .dropdown.project-filter - %button.dropdown-menu-toggle.btn.js-search-project-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Project:" } } + %button.dropdown-menu-toggle.js-search-project-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Project:" } } %span.dropdown-toggle-text Project: - if @project.present? -- cgit v1.2.1 From 927042985fbfeeb1a348c8772e5a00084146bc87 Mon Sep 17 00:00:00 2001 From: David Wagner <david@marvid.fr> Date: Wed, 23 Nov 2016 18:52:38 +0100 Subject: dropdowns.scss: Fix style issues after review Signed-off-by: David Wagner <david@marvid.fr> --- app/assets/stylesheets/framework/dropdowns.scss | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index de6e154dfe6..6d77aadd753 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -28,7 +28,6 @@ .dropdown-toggle, .dropdown-menu-toggle { @include chevron-active; - border-color: $dropdown-toggle-hover-border-color; } } @@ -81,22 +80,18 @@ &:hover { @include chevron-active; - border-color: $dropdown-toggle-hover-border-color; } &:focus:active { @include chevron-active; - border-color: $dropdown-toggle-active-border-color; } } .dropdown-menu-toggle { @extend .dropdown-toggle; - padding-right: 20px; - position: relative; width: 160px; text-overflow: ellipsis; -- cgit v1.2.1 From 7de6aee7556d53ec1b327bd6d0cb40fbd3848592 Mon Sep 17 00:00:00 2001 From: Adam Niedzielski <adamsunday@gmail.com> Date: Tue, 29 Nov 2016 00:08:11 +0100 Subject: Fix pipelines info being hidden in merge request widget We do need these "ci-#{status}" classes because we use them in MergeRequestWidget to show correct divs. --- app/views/projects/merge_requests/widget/_heading.html.haml | 2 +- changelogs/unreleased/25055-pipelines-info-missing-from-mr-widget.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/25055-pipelines-info-missing-from-mr-widget.yml diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 18c72ed875c..6d9b91ad0e7 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,7 +1,7 @@ - if @pipeline .mr-widget-heading - %w[success success_with_warnings skipped canceled failed running pending].each do |status| - .ci_widget{ class: "ci-status-icon-#{status}", style: ("display:none" unless @pipeline.status == status) } + .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: ("display:none" unless @pipeline.status == status) } = ci_icon_for_status(status) %span Pipeline diff --git a/changelogs/unreleased/25055-pipelines-info-missing-from-mr-widget.yml b/changelogs/unreleased/25055-pipelines-info-missing-from-mr-widget.yml new file mode 100644 index 00000000000..dad9db0ffef --- /dev/null +++ b/changelogs/unreleased/25055-pipelines-info-missing-from-mr-widget.yml @@ -0,0 +1,4 @@ +--- +title: Fix pipelines info being hidden in merge request widget +merge_request: 7808 +author: -- cgit v1.2.1 From cb9475259dfdbd28c6297d518b9263feb0711b93 Mon Sep 17 00:00:00 2001 From: Drew Blessing <drew@blessing.io> Date: Mon, 28 Nov 2016 23:43:37 +0000 Subject: Remove `memberOf` OID in LDAP `user_filter` docs While not technically invalid, it is not necessary to have the `memberOf` OID in the `user_filter`. It clutters things up and causes confusion for users so it's better if we remove it from the docs. --- doc/administration/auth/ldap.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md index d3f216fb3bf..b8b63df091e 100644 --- a/doc/administration/auth/ldap.md +++ b/doc/administration/auth/ldap.md @@ -221,7 +221,7 @@ Tip: If you want to limit access to the nested members of an Active Directory group you can use the following syntax: ``` -(memberOf:1.2.840.113556.1.4.1941:=CN=My Group,DC=Example,DC=com) +(memberOf=CN=My Group,DC=Example,DC=com) ``` Please note that GitLab does not support the custom filter syntax used by -- cgit v1.2.1 From 3d7704ae5f62446b8b399c796c64d1f527666376 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@gitlab.com> Date: Thu, 10 Nov 2016 10:23:44 +0000 Subject: Merge branch 'zj-fix-label-creation-non-members' into 'security' Fix label creation non members Fixes https://gitlab.com/gitlab-org/gitlab-ce/issues/23416 See merge request !2006 --- app/services/issuable_base_service.rb | 8 ++- app/services/labels/find_or_create_service.rb | 7 +- .../zj-fix-label-creation-non-members.yml | 4 ++ lib/api/helpers.rb | 14 ---- lib/api/issues.rb | 74 +++++++++++----------- lib/api/merge_requests.rb | 10 --- spec/requests/api/issues_spec.rb | 14 +++- spec/requests/api/merge_requests_spec.rb | 27 ++++---- spec/services/labels/transfer_service_spec.rb | 2 +- 9 files changed, 78 insertions(+), 82 deletions(-) create mode 100644 changelogs/unreleased/zj-fix-label-creation-non-members.yml diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index d698b295e6d..ce68e433ab8 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -85,14 +85,15 @@ class IssuableBaseService < BaseService def find_or_create_label_ids labels = params.delete(:labels) + return unless labels - params[:label_ids] = labels.split(',').map do |label_name| + params[:label_ids] = labels.split(",").map do |label_name| service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip) label = service.execute - label.id - end + label.try(:id) + end.compact end def process_label_ids(attributes, existing_label_ids: nil) @@ -140,6 +141,7 @@ class IssuableBaseService < BaseService params.delete(:state_event) params[:author] ||= current_user + label_ids = process_label_ids(params) issuable.assign_attributes(params) diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb index d622f9edd33..cf4f7606c94 100644 --- a/app/services/labels/find_or_create_service.rb +++ b/app/services/labels/find_or_create_service.rb @@ -22,9 +22,14 @@ module Labels ).execute(skip_authorization: skip_authorization) end + # Only creates the label if current_user can do so, if the label does not exist + # and the user can not create the label, nil is returned def find_or_create_label new_label = available_labels.find_by(title: title) - new_label ||= project.labels.create(params) + + if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, project)) + new_label = project.labels.create(params) + end new_label end diff --git a/changelogs/unreleased/zj-fix-label-creation-non-members.yml b/changelogs/unreleased/zj-fix-label-creation-non-members.yml new file mode 100644 index 00000000000..ae4824f82fa --- /dev/null +++ b/changelogs/unreleased/zj-fix-label-creation-non-members.yml @@ -0,0 +1,4 @@ +--- +title: Non members cannot create labels through the API +merge_request: +author: diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 0d3ddb89dc3..79a83496eee 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -198,20 +198,6 @@ module API ActionController::Parameters.new(attrs).permit! end - # Helper method for validating all labels against its names - def validate_label_params(params) - errors = {} - - params[:labels].to_s.split(',').each do |label_name| - label = available_labels.find_or_initialize_by(title: label_name.strip) - next if label.valid? - - errors[label.title] = label.errors - end - - errors - end - # Checks the occurrences of datetime attributes, each attribute if present in the params hash must be in ISO 8601 # format (YYYY-MM-DDTHH:MM:SSZ) or a Bad Request error is invoked. # diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 2fea71870b8..029be7519f5 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -19,6 +19,15 @@ module API def filter_issues_milestone(issues, milestone) issues.includes(:milestone).where('milestones.title' => milestone) end + + def issue_params + new_params = declared(params, include_parent_namespace: false, include_missing: false).to_h + new_params = new_params.with_indifferent_access + new_params.delete(:id) + new_params.delete(:issue_id) + + new_params + end end resource :issues do @@ -86,6 +95,10 @@ module API end end + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do # Get a list of project issues # @@ -152,17 +165,10 @@ module API post ':id/issues' do required_attributes! [:title] - keys = [:title, :description, :assignee_id, :milestone_id, :due_date, :confidential] + keys = [:title, :description, :assignee_id, :milestone_id, :due_date, :confidential, :labels] keys << :created_at if current_user.admin? || user_project.owner == current_user attrs = attributes_for_keys(keys) - # Validate label names in advance - if (errors = validate_label_params(params)).any? - render_api_error!({ labels: errors }, 400) - end - - attrs[:labels] = params[:labels] if params[:labels] - # Convert and filter out invalid confidential flags attrs['confidential'] = to_boolean(attrs['confidential']) attrs.delete('confidential') if attrs['confidential'].nil? @@ -180,41 +186,35 @@ module API end end - # Update an existing issue - # - # Parameters: - # id (required) - The ID of a project - # issue_id (required) - The ID of a project issue - # title (optional) - The title of an issue - # description (optional) - The description of an issue - # assignee_id (optional) - The ID of a user to assign issue - # milestone_id (optional) - The ID of a milestone to assign issue - # labels (optional) - The labels of an issue - # state_event (optional) - The state event of an issue (close|reopen) - # updated_at (optional) - Date time string, ISO 8601 formatted - # due_date (optional) - Date time string in the format YEAR-MONTH-DAY - # confidential (optional) - Boolean parameter if the issue should be confidential - # Example Request: - # PUT /projects/:id/issues/:issue_id + desc 'Update an existing issue' do + success Entities::Issue + end + params do + requires :id, type: String, desc: 'The ID of a project' + requires :issue_id, type: Integer, desc: "The ID of a project issue" + optional :title, type: String, desc: 'The new title of the issue' + optional :description, type: String, desc: 'The description of an issue' + optional :assignee_id, type: Integer, desc: 'The ID of a user to assign issue' + optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue' + optional :labels, type: String, desc: 'The labels of an issue' + optional :state_event, type: String, values: ['close', 'reopen'], desc: 'The state event of an issue' + # TODO 9.0, use the Grape DateTime type here + optional :updated_at, type: String, desc: 'Date time string, ISO 8601 formatted' + optional :due_date, type: String, desc: 'Date time string in the format YEAR-MONTH-DAY' + # TODO 9.0, use the Grape boolean type here + optional :confidential, type: String, desc: 'Boolean parameter if the issue should be confidential' + end put ':id/issues/:issue_id' do issue = user_project.issues.find(params[:issue_id]) authorize! :update_issue, issue - keys = [:title, :description, :assignee_id, :milestone_id, :state_event, :due_date, :confidential] - keys << :updated_at if current_user.admin? || user_project.owner == current_user - attrs = attributes_for_keys(keys) - - # Validate label names in advance - if (errors = validate_label_params(params)).any? - render_api_error!({ labels: errors }, 400) - end - - attrs[:labels] = params[:labels] if params[:labels] # Convert and filter out invalid confidential flags - attrs['confidential'] = to_boolean(attrs['confidential']) - attrs.delete('confidential') if attrs['confidential'].nil? + params[:confidential] = to_boolean(params[:confidential]) + params.delete(:confidential) if params[:confidential].nil? + + params.delete(:updated_at) unless current_user.admin? || user_project.owner == current_user - issue = ::Issues::UpdateService.new(user_project, current_user, attrs).execute(issue) + issue = ::Issues::UpdateService.new(user_project, current_user, issue_params).execute(issue) if issue.valid? present issue, with: Entities::Issue, current_user: current_user, project: user_project diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index e82651a1578..90fa588b455 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -77,11 +77,6 @@ module API mr_params = declared_params - # Validate label names in advance - if (errors = validate_label_params(mr_params)).any? - render_api_error!({ labels: errors }, 400) - end - merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute if merge_request.valid? @@ -157,11 +152,6 @@ module API mr_params = declared_params(include_missing: false) - # Validate label names in advance - if (errors = validate_label_params(mr_params)).any? - render_api_error!({ labels: errors }, 400) - end - merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request) if merge_request.valid? diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 7bae055b241..b17553211d2 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -697,6 +697,14 @@ describe API::API, api: true do expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time) end end + + context 'the user can only read the issue' do + it 'cannot create new labels' do + expect do + post api("/projects/#{project.id}/issues", non_member), title: 'new issue', labels: 'label, label2' + end.not_to change { project.labels.count } + end + end end describe 'POST /projects/:id/issues with spam filtering' do @@ -839,8 +847,8 @@ describe API::API, api: true do end it 'removes all labels' do - put api("/projects/#{project.id}/issues/#{issue.id}", user), - labels: '' + put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: '' + expect(response).to have_http_status(200) expect(json_response['labels']).to eq([]) end @@ -892,8 +900,8 @@ describe API::API, api: true do update_time = 2.weeks.ago put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: 'label3', state_event: 'close', updated_at: update_time - expect(response).to have_http_status(200) + expect(response).to have_http_status(200) expect(json_response['labels']).to include 'label3' expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time) end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 37fcb2bc3a9..3ecf3eea5f5 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -402,14 +402,6 @@ describe API::API, api: true do end end - describe "PUT /projects/:id/merge_requests/:merge_request_id to close MR" do - it "returns merge_request" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close" - expect(response).to have_http_status(200) - expect(json_response['state']).to eq('closed') - end - end - describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do let(:pipeline) { create(:ci_pipeline_without_jobs) } @@ -486,6 +478,15 @@ describe API::API, api: true do end describe "PUT /projects/:id/merge_requests/:merge_request_id" do + context "to close a MR" do + it "returns merge_request" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close" + + expect(response).to have_http_status(200) + expect(json_response['state']).to eq('closed') + end + end + it "updates title and returns merge_request" do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: "New title" expect(response).to have_http_status(200) @@ -511,10 +512,10 @@ describe API::API, api: true do end it 'allows special label names' do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", - user), - title: 'new issue', - labels: 'label, label?, label&foo, ?, &' + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), + title: 'new issue', + labels: 'label, label?, label&foo, ?, &' + expect(response.status).to eq(200) expect(json_response['labels']).to include 'label' expect(json_response['labels']).to include 'label?' @@ -543,7 +544,7 @@ describe API::API, api: true do it "returns 404 if note is attached to non existent merge request" do post api("/projects/#{project.id}/merge_requests/404/comments", user), - note: 'My comment' + note: 'My comment' expect(response).to have_http_status(404) end end diff --git a/spec/services/labels/transfer_service_spec.rb b/spec/services/labels/transfer_service_spec.rb index ddf3527dc0f..13654a0881c 100644 --- a/spec/services/labels/transfer_service_spec.rb +++ b/spec/services/labels/transfer_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Labels::TransferService, services: true do describe '#execute' do - let(:user) { create(:user) } + let(:user) { create(:admin) } let(:group_1) { create(:group) } let(:group_2) { create(:group) } let(:group_3) { create(:group) } -- cgit v1.2.1 From 742cee756bf39d93fe5c7f207f8a54143ae6a384 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@gitlab.com> Date: Mon, 7 Nov 2016 17:09:22 +0000 Subject: Merge branch 'jej-22869' into 'security' Fix information disclosure in `Projects::BlobController#update` It was possible to discover private project names by modifying `from_merge_request`parameter in `Projects::BlobController#update`. This fixes that. - [ ] [CHANGELOG](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG.md) entry added - Tests - [x] Added for this feature/bug - [ ] All builds are passing - [x] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html) - [x] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides) - [x] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) https://gitlab.com/gitlab-org/gitlab-ce/issues/22869 See merge request !2023 --- app/controllers/projects/blob_controller.rb | 20 ++++----- app/views/projects/blob/edit.html.haml | 2 +- app/views/projects/diffs/_file.html.haml | 2 +- changelogs/unreleased/jej-22869.yml | 4 ++ spec/controllers/projects/blob_controller_spec.rb | 49 +++++++++++++++++++++++ spec/features/projects/blobs/edit_spec.rb | 45 +++++++++++++++++++++ 6 files changed, 108 insertions(+), 14 deletions(-) create mode 100644 changelogs/unreleased/jej-22869.yml create mode 100644 spec/features/projects/blobs/edit_spec.rb diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 56ced786311..9940263ae24 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -13,7 +13,6 @@ class Projects::BlobController < Projects::ApplicationController before_action :assign_blob_vars before_action :commit, except: [:new, :create] before_action :blob, except: [:new, :create] - before_action :from_merge_request, only: [:edit, :update] before_action :require_branch_head, only: [:edit, :update] before_action :editor_variables, except: [:show, :preview, :diff] before_action :validate_diff_params, only: :diff @@ -39,14 +38,6 @@ class Projects::BlobController < Projects::ApplicationController def update @path = params[:file_path] if params[:file_path].present? - after_edit_path = - if from_merge_request && @target_branch == @ref - diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) + - "##{hexdigest(@path)}" - else - namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path)) - end - create_commit(Files::UpdateService, success_path: after_edit_path, failure_view: :edit, failure_path: namespace_project_blob_path(@project.namespace, @project, @id)) @@ -124,9 +115,14 @@ class Projects::BlobController < Projects::ApplicationController render_404 end - def from_merge_request - # If blob edit was initiated from merge request page - @from_merge_request ||= MergeRequest.find_by(id: params[:from_merge_request_id]) + def after_edit_path + from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:from_merge_request_iid]) + if from_merge_request && @target_branch == @ref + diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) + + "##{hexdigest(@path)}" + else + namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path)) + end end def editor_variables diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 2a0352a71b7..a5dcd93f42e 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -27,5 +27,5 @@ = render 'shared/new_commit_form', placeholder: "Update #{@blob.name}" = hidden_field_tag 'last_commit_sha', @last_commit_sha = hidden_field_tag 'content', '', id: "file-content" - = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id] + = hidden_field_tag 'from_merge_request_iid', params[:from_merge_request_iid] = render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id) diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 120ba9ffcd2..6c33d80becd 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -9,7 +9,7 @@ = icon('comment') \ - if editable_diff?(diff_file) - - link_opts = @merge_request.id ? { from_merge_request_id: @merge_request.id } : {} + - link_opts = @merge_request.persisted? ? { from_merge_request_iid: @merge_request.iid } : {} = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path, blob: blob, link_opts: link_opts) diff --git a/changelogs/unreleased/jej-22869.yml b/changelogs/unreleased/jej-22869.yml new file mode 100644 index 00000000000..9d2edcfee42 --- /dev/null +++ b/changelogs/unreleased/jej-22869.yml @@ -0,0 +1,4 @@ +--- +title: Fix information disclosure in `Projects::BlobController#update` +merge_request: +author: diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index 52d13fb6f9e..1c2b0a4a45c 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -36,4 +36,53 @@ describe Projects::BlobController do end end end + + describe 'PUT update' do + let(:default_params) do + { + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: 'master/CHANGELOG', + target_branch: 'master', + content: 'Added changes', + commit_message: 'Update CHANGELOG' + } + end + + def blob_after_edit_path + namespace_project_blob_path(project.namespace, project, 'master/CHANGELOG') + end + + it 'redirects to blob' do + put :update, default_params + + expect(response).to redirect_to(blob_after_edit_path) + end + + context '?from_merge_request_iid' do + let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + let(:mr_params) { default_params.merge(from_merge_request_iid: merge_request.iid) } + + it 'redirects to MR diff' do + put :update, mr_params + + after_edit_path = diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) + file_anchor = "#file-path-#{Digest::SHA1.hexdigest('CHANGELOG')}" + expect(response).to redirect_to(after_edit_path + file_anchor) + end + + context "when user doesn't have access" do + before do + other_project = create(:empty_project) + merge_request.update!(source_project: other_project, target_project: other_project) + end + + it "it redirect to blob" do + put :update, mr_params + + expect(response).to redirect_to(blob_after_edit_path) + end + end + end + end end diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb new file mode 100644 index 00000000000..a820d07ab3b --- /dev/null +++ b/spec/features/projects/blobs/edit_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +feature 'Editing file blob', feature: true, js: true do + include WaitForAjax + + given(:user) { create(:user) } + given(:role) { :developer } + given(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master') } + given(:project) { merge_request.target_project } + + background do + login_as(user) + project.team << [user, role] + end + + def edit_and_commit + wait_for_ajax + first('.file-actions').click_link 'Edit' + execute_script('ace.edit("editor").setValue("class NextFeature\nend\n")') + click_button 'Commit Changes' + end + + context 'from MR diff' do + before do + visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) + edit_and_commit + end + + scenario 'returns me to the mr' do + expect(page).to have_content(merge_request.title) + end + end + + context 'from blob file path' do + before do + visit namespace_project_blob_path(project.namespace, project, '/feature/files/ruby/feature.rb') + edit_and_commit + end + + scenario 'updates content' do + expect(page).to have_content 'successfully committed' + expect(page).to have_content 'NextFeature' + end + end +end -- cgit v1.2.1 From 6d37fe952b5679d7586eaa569d0488dbb92032fe Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@gitlab.com> Date: Fri, 18 Nov 2016 13:51:52 +0000 Subject: Merge branch 'jej-fix-missing-access-check-on-issues' into 'security' Fix missing access checks on issue lookup using IssuableFinder Split from !2024 to partially solve https://gitlab.com/gitlab-org/gitlab-ce/issues/23867 :warning: - Potentially untested :bomb: - No test coverage :traffic_light: - Test coverage of some sort exists (a test failed when error raised) :vertical_traffic_light: - Test coverage of return value (a test failed when nil used) :white_check_mark: - Permissions check tested - [x] :white_check_mark: app/controllers/projects/branches_controller.rb:39 - `before_action :authorize_push_code!` helpes limit/prevent exploitation. Always checks for reporter access so fine with confidential issues, issues only visible to team, etc. - [x] :traffic_light: app/models/cycle_analytics/summary.rb:9 [`.count`] - [x] :white_check_mark: app/controllers/projects/todos_controller.rb:19 - [x] Potential double render in app/controllers/projects/todos_controller.rb - https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/2024/diffs#cedccb227af9bfdf88802767cb58d43c2b977439_24_24 See merge request !2030 --- app/controllers/projects/branches_controller.rb | 2 +- app/controllers/projects/cycle_analytics_controller.rb | 2 +- app/controllers/projects/todos_controller.rb | 8 +------- app/finders/issuable_finder.rb | 8 ++++++++ app/models/cycle_analytics.rb | 5 +++-- app/models/cycle_analytics/summary.rb | 5 +++-- .../jej-fix-missing-access-check-on-issues.yml | 4 ++++ spec/controllers/projects/branches_controller_spec.rb | 18 ++++++++++++++++++ spec/controllers/projects/todo_controller_spec.rb | 17 +++++++++++++++-- spec/models/cycle_analytics/code_spec.rb | 2 +- spec/models/cycle_analytics/issue_spec.rb | 2 +- spec/models/cycle_analytics/plan_spec.rb | 2 +- spec/models/cycle_analytics/production_spec.rb | 2 +- spec/models/cycle_analytics/review_spec.rb | 2 +- spec/models/cycle_analytics/staging_spec.rb | 2 +- spec/models/cycle_analytics/summary_spec.rb | 2 +- spec/models/cycle_analytics/test_spec.rb | 2 +- 17 files changed, 62 insertions(+), 23 deletions(-) create mode 100644 changelogs/unreleased/jej-fix-missing-access-check-on-issues.yml diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 6b9f37983c4..89d84809e3a 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -36,7 +36,7 @@ class Projects::BranchesController < Projects::ApplicationController execute(branch_name, ref) if params[:issue_iid] - issue = @project.issues.find_by(iid: params[:issue_iid]) + issue = IssuesFinder.new(current_user, project_id: @project.id).find_by(iid: params[:issue_iid]) SystemNoteService.new_issue_branch(issue, @project, current_user, branch_name) if issue end diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb index fd263960b93..ac639ef015b 100644 --- a/app/controllers/projects/cycle_analytics_controller.rb +++ b/app/controllers/projects/cycle_analytics_controller.rb @@ -6,7 +6,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController before_action :authorize_read_cycle_analytics! def show - @cycle_analytics = ::CycleAnalytics.new(@project, from: start_date(cycle_analytics_params)) + @cycle_analytics = ::CycleAnalytics.new(@project, current_user, from: start_date(cycle_analytics_params)) stats_values, cycle_analytics_json = generate_cycle_analytics_data diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb index 5685d0f4e7c..52517381c65 100644 --- a/app/controllers/projects/todos_controller.rb +++ b/app/controllers/projects/todos_controller.rb @@ -16,13 +16,7 @@ class Projects::TodosController < Projects::ApplicationController @issuable ||= begin case params[:issuable_type] when "issue" - issue = @project.issues.find(params[:issuable_id]) - - if can?(current_user, :read_issue, issue) - issue - else - render_404 - end + IssuesFinder.new(current_user, project_id: @project.id).find(params[:issuable_id]) when "merge_request" @project.merge_requests.find(params[:issuable_id]) end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index a48f22cee07..36005035f68 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -41,6 +41,14 @@ class IssuableFinder sort(items) end + def find(*params) + execute.find(*params) + end + + def find_by(*params) + execute.find_by(*params) + end + def group return @group if defined?(@group) diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb index cb8e088d21d..ba4ee6fcf9d 100644 --- a/app/models/cycle_analytics.rb +++ b/app/models/cycle_analytics.rb @@ -1,14 +1,15 @@ class CycleAnalytics STAGES = %i[issue plan code test review staging production].freeze - def initialize(project, from:) + def initialize(project, current_user, from:) @project = project + @current_user = current_user @from = from @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: from, branch: nil) end def summary - @summary ||= Summary.new(@project, from: @from) + @summary ||= Summary.new(@project, @current_user, from: @from) end def permissions(user:) diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb index b46db449bf3..82f53d17ddd 100644 --- a/app/models/cycle_analytics/summary.rb +++ b/app/models/cycle_analytics/summary.rb @@ -1,12 +1,13 @@ class CycleAnalytics class Summary - def initialize(project, from:) + def initialize(project, current_user, from:) @project = project + @current_user = current_user @from = from end def new_issues - @project.issues.created_after(@from).count + IssuesFinder.new(@current_user, project_id: @project.id).execute.created_after(@from).count end def commits diff --git a/changelogs/unreleased/jej-fix-missing-access-check-on-issues.yml b/changelogs/unreleased/jej-fix-missing-access-check-on-issues.yml new file mode 100644 index 00000000000..844fba9a107 --- /dev/null +++ b/changelogs/unreleased/jej-fix-missing-access-check-on-issues.yml @@ -0,0 +1,4 @@ +--- +title: Fix missing access checks on issue lookup using IssuableFinder +merge_request: +author: diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index f7cf006efd6..b88586b8678 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -94,6 +94,24 @@ describe Projects::BranchesController do branch_name: branch, issue_iid: issue.iid end + + context 'without issue feature access' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE) + project.team.truncate + end + + it "doesn't post a system note" do + expect(SystemNoteService).not_to receive(:new_issue_branch) + + post :create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + branch_name: branch, + issue_iid: issue.iid + end + end end end diff --git a/spec/controllers/projects/todo_controller_spec.rb b/spec/controllers/projects/todo_controller_spec.rb index 936320a3709..193a3f6b5a3 100644 --- a/spec/controllers/projects/todo_controller_spec.rb +++ b/spec/controllers/projects/todo_controller_spec.rb @@ -4,7 +4,7 @@ describe Projects::TodosController do include ApiHelpers let(:user) { create(:user) } - let(:project) { create(:project) } + let(:project) { create(:empty_project) } let(:issue) { create(:issue, project: project) } let(:merge_request) { create(:merge_request, source_project: project) } @@ -42,7 +42,7 @@ describe Projects::TodosController do end end - context 'when not authorized' do + context 'when not authorized for project' do it 'does not create todo for issue that user has no access to' do sign_in(user) expect do @@ -60,6 +60,19 @@ describe Projects::TodosController do expect(response).to have_http_status(302) end end + + context 'when not authorized for issue' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE) + sign_in(user) + end + + it "doesn't create todo" do + expect{ go }.not_to change { user.todos.count } + expect(response).to have_http_status(404) + end + end end end diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb index 7691d690db0..7771785ead3 100644 --- a/spec/models/cycle_analytics/code_spec.rb +++ b/spec/models/cycle_analytics/code_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#code', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, from: from_date) } + subject { CycleAnalytics.new(project, user, from: from_date) } context 'with deployment' do generate_cycle_analytics_spec( diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb index f649b44d367..5ed3d37f2fb 100644 --- a/spec/models/cycle_analytics/issue_spec.rb +++ b/spec/models/cycle_analytics/issue_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#issue', models: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, from: from_date) } + subject { CycleAnalytics.new(project, user, from: from_date) } generate_cycle_analytics_spec( phase: :issue, diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb index 2cdefbeef21..baf3e3241a1 100644 --- a/spec/models/cycle_analytics/plan_spec.rb +++ b/spec/models/cycle_analytics/plan_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#plan', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, from: from_date) } + subject { CycleAnalytics.new(project, user, from: from_date) } generate_cycle_analytics_spec( phase: :plan, diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb index 1f5e5cab92d..21b9c6e7150 100644 --- a/spec/models/cycle_analytics/production_spec.rb +++ b/spec/models/cycle_analytics/production_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#production', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, from: from_date) } + subject { CycleAnalytics.new(project, user, from: from_date) } generate_cycle_analytics_spec( phase: :production, diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb index 0ed080a42b1..158621d59a4 100644 --- a/spec/models/cycle_analytics/review_spec.rb +++ b/spec/models/cycle_analytics/review_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#review', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, from: from_date) } + subject { CycleAnalytics.new(project, user, from: from_date) } generate_cycle_analytics_spec( phase: :review, diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb index af1c4477ddb..dad653964b7 100644 --- a/spec/models/cycle_analytics/staging_spec.rb +++ b/spec/models/cycle_analytics/staging_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#staging', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, from: from_date) } + subject { CycleAnalytics.new(project, user, from: from_date) } generate_cycle_analytics_spec( phase: :staging, diff --git a/spec/models/cycle_analytics/summary_spec.rb b/spec/models/cycle_analytics/summary_spec.rb index 9d67bc82cba..725bc68b25f 100644 --- a/spec/models/cycle_analytics/summary_spec.rb +++ b/spec/models/cycle_analytics/summary_spec.rb @@ -4,7 +4,7 @@ describe CycleAnalytics::Summary, models: true do let(:project) { create(:project) } let(:from) { Time.now } let(:user) { create(:user, :admin) } - subject { described_class.new(project, from: from) } + subject { described_class.new(project, user, from: from) } describe "#new_issues" do it "finds the number of issues created after the 'from date'" do diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb index 02ddfeed9c1..2313724e8f3 100644 --- a/spec/models/cycle_analytics/test_spec.rb +++ b/spec/models/cycle_analytics/test_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#test', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, from: from_date) } + subject { CycleAnalytics.new(project, user, from: from_date) } generate_cycle_analytics_spec( phase: :test, -- cgit v1.2.1 From 3bf34face4cacf07ca973705c261369b1f596626 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@gitlab.com> Date: Tue, 22 Nov 2016 10:25:04 +0000 Subject: Merge branch 'jej-use-issuable-finder-instead-of-access-check' into 'security' Replace issue access checks with use of IssuableFinder Split from !2024 to partially solve https://gitlab.com/gitlab-org/gitlab-ce/issues/23867 ## Which fixes are in this MR? :warning: - Potentially untested :bomb: - No test coverage :traffic_light: - Test coverage of some sort exists (a test failed when error raised) :vertical_traffic_light: - Test coverage of return value (a test failed when nil used) :white_check_mark: - Permissions check tested ### Issue lookup with access check Using `visible_to_user` likely makes these security issues too. See [Code smells](#code-smells). - [x] :vertical_traffic_light: app/finders/notes_finder.rb:15 [`visible_to_user`] - [x] :traffic_light: app/views/layouts/nav/_project.html.haml:73 [`visible_to_user`] [`.count`] - [x] :white_check_mark: app/services/merge_requests/build_service.rb:84 [`issue.try(:confidential?)`] - [x] :white_check_mark: lib/api/issues.rb:112 [`visible_to_user`] - CHANGELOG: Prevented API returning issues set to 'Only team members' to everyone - [x] :white_check_mark: lib/api/helpers.rb:126 [`can?(current_user, :read_issue, issue)`] Maybe here too? - [x] :white_check_mark: lib/gitlab/search_results.rb:53 [`visible_to_user`] ### Previous discussions - [ ] https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/2024/diffs#b2ff264eddf9819d7693c14ae213d941494fe2b3_128_126 - [ ] https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/2024/diffs#7b6375270d22f880bdcb085e47b519b426a5c6c7_87_87 See merge request !2031 --- app/finders/issuable_finder.rb | 2 +- app/finders/notes_finder.rb | 2 +- app/models/project.rb | 4 +- app/services/merge_requests/build_service.rb | 2 +- app/views/layouts/nav/_project.html.haml | 2 +- ...use-issuable-finder-instead-of-access-check.yml | 4 ++ lib/api/helpers.rb | 4 +- lib/api/issues.rb | 2 +- lib/gitlab/search_results.rb | 2 +- spec/lib/gitlab/project_search_results_spec.rb | 9 ++++ spec/lib/gitlab/search_results_spec.rb | 51 ++++++++++++++-------- spec/models/project_spec.rb | 16 +++++-- spec/requests/api/issues_spec.rb | 18 ++++++++ spec/services/merge_requests/build_service_spec.rb | 12 +++++ 14 files changed, 97 insertions(+), 33 deletions(-) create mode 100644 changelogs/unreleased/jej-use-issuable-finder-instead-of-access-check.yml diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 36005035f68..9a74e36870b 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -21,7 +21,7 @@ class IssuableFinder attr_accessor :current_user, :params - def initialize(current_user, params) + def initialize(current_user, params = {}) @current_user = current_user @params = params end diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index 0b7832e6583..a653a6d59c6 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -12,7 +12,7 @@ class NotesFinder when "commit" project.notes.for_commit_id(target_id).non_diff_notes when "issue" - project.issues.visible_to_user(current_user).find(target_id).notes.inc_author + IssuesFinder.new(current_user, project_id: project.id).find(target_id).notes.inc_author when "merge_request" project.merge_requests.find(target_id).mr_and_commit_notes.inc_author when "snippet", "project_snippet" diff --git a/app/models/project.rb b/app/models/project.rb index c61e63461e0..f01cb613b85 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -687,9 +687,9 @@ class Project < ActiveRecord::Base self.id end - def get_issue(issue_id) + def get_issue(issue_id, current_user) if default_issues_tracker? - issues.find_by(iid: issue_id) + IssuesFinder.new(current_user, project_id: id).find_by(iid: issue_id) else ExternalIssue.new(issue_id, self) end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index dd0d738674e..bebfca7537b 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -81,7 +81,7 @@ module MergeRequests commit = commits.first merge_request.title = commit.title merge_request.description ||= commit.description.try(:strip) - elsif iid && (issue = merge_request.target_project.get_issue(iid)) && !issue.try(:confidential?) + elsif iid && issue = merge_request.target_project.get_issue(iid, current_user) case issue when Issue merge_request.title = "Resolve \"#{issue.title}\"" diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 99a58bbb676..701bcd3ab71 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -70,7 +70,7 @@ %span Issues - if @project.default_issues_tracker? - %span.badge.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count) + %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) - if project_nav_tab? :merge_requests = nav_link(controller: :merge_requests) do diff --git a/changelogs/unreleased/jej-use-issuable-finder-instead-of-access-check.yml b/changelogs/unreleased/jej-use-issuable-finder-instead-of-access-check.yml new file mode 100644 index 00000000000..c0b6f50052c --- /dev/null +++ b/changelogs/unreleased/jej-use-issuable-finder-instead-of-access-check.yml @@ -0,0 +1,4 @@ +--- +title: Replace issue access checks with use of IssuableFinder +merge_request: +author: diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 79a83496eee..34d9c3c6932 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -128,9 +128,7 @@ module API end def find_project_issue(id) - issue = user_project.issues.find(id) - not_found! unless can?(current_user, :read_issue, issue) - issue + IssuesFinder.new(current_user, project_id: user_project.id).find(id) end def paginate(relation) diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 029be7519f5..049b4fb214c 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -122,7 +122,7 @@ module API # GET /projects/:id/issues?milestone=1.0.0&state=closed # GET /issues?iid=42 get ":id/issues" do - issues = user_project.issues.inc_notes_with_associations.visible_to_user(current_user) + issues = IssuesFinder.new(current_user, project_id: user_project.id).execute.inc_notes_with_associations issues = filter_issues_state(issues, params[:state]) unless params[:state].nil? issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil? diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index 2690938fe82..47d8599e298 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -50,7 +50,7 @@ module Gitlab end def issues - issues = Issue.visible_to_user(current_user).where(project_id: project_ids_relation) + issues = IssuesFinder.new(current_user).execute.where(project_id: project_ids_relation) if query =~ /#(\d+)\z/ issues = issues.where(iid: $1) diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index a0fdad87eee..3cd9863ec6a 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -65,6 +65,14 @@ describe Gitlab::ProjectSearchResults, lib: true do end end + it 'does not list issues on private projects' do + issue = create(:issue, project: project) + + results = described_class.new(user, project, issue.title) + + expect(results.objects('issues')).not_to include issue + end + describe 'confidential issues' do let(:query) { 'issue' } let(:author) { create(:user) } @@ -72,6 +80,7 @@ describe Gitlab::ProjectSearchResults, lib: true do let(:non_member) { create(:user) } let(:member) { create(:user) } let(:admin) { create(:admin) } + let(:project) { create(:empty_project, :internal) } let!(:issue) { create(:issue, project: project, title: 'Issue 1') } let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) } let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) } diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index dfbefad6367..f23e3522625 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -12,35 +12,48 @@ describe Gitlab::SearchResults do let!(:milestone) { create(:milestone, project: project, title: 'foo') } let(:results) { described_class.new(user, Project.all, 'foo') } - describe '#projects_count' do - it 'returns the total amount of projects' do - expect(results.projects_count).to eq(1) + context 'as a user with access' do + before do + project.team << [user, :developer] end - end - describe '#issues_count' do - it 'returns the total amount of issues' do - expect(results.issues_count).to eq(1) + describe '#projects_count' do + it 'returns the total amount of projects' do + expect(results.projects_count).to eq(1) + end end - end - describe '#merge_requests_count' do - it 'returns the total amount of merge requests' do - expect(results.merge_requests_count).to eq(1) + describe '#issues_count' do + it 'returns the total amount of issues' do + expect(results.issues_count).to eq(1) + end + end + + describe '#merge_requests_count' do + it 'returns the total amount of merge requests' do + expect(results.merge_requests_count).to eq(1) + end end - end - describe '#milestones_count' do - it 'returns the total amount of milestones' do - expect(results.milestones_count).to eq(1) + describe '#milestones_count' do + it 'returns the total amount of milestones' do + expect(results.milestones_count).to eq(1) + end end end + it 'does not list issues on private projects' do + private_project = create(:empty_project, :private) + issue = create(:issue, project: private_project, title: 'foo') + + expect(results.objects('issues')).not_to include issue + end + describe 'confidential issues' do - let(:project_1) { create(:empty_project) } - let(:project_2) { create(:empty_project) } - let(:project_3) { create(:empty_project) } - let(:project_4) { create(:empty_project) } + let(:project_1) { create(:empty_project, :internal) } + let(:project_2) { create(:empty_project, :internal) } + let(:project_3) { create(:empty_project, :internal) } + let(:project_4) { create(:empty_project, :internal) } let(:query) { 'issue' } let(:limit_projects) { Project.where(id: [project_1.id, project_2.id, project_3.id]) } let(:author) { create(:user) } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index da38254d1bc..8abcce42ce0 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -361,10 +361,15 @@ describe Project, models: true do describe '#get_issue' do let(:project) { create(:empty_project) } let!(:issue) { create(:issue, project: project) } + let(:user) { create(:user) } + + before do + project.team << [user, :developer] + end context 'with default issues tracker' do it 'returns an issue' do - expect(project.get_issue(issue.iid)).to eq issue + expect(project.get_issue(issue.iid, user)).to eq issue end it 'returns count of open issues' do @@ -372,7 +377,12 @@ describe Project, models: true do end it 'returns nil when no issue found' do - expect(project.get_issue(999)).to be_nil + expect(project.get_issue(999, user)).to be_nil + end + + it "returns nil when user doesn't have access" do + user = create(:user) + expect(project.get_issue(issue.iid, user)).to eq nil end end @@ -382,7 +392,7 @@ describe Project, models: true do end it 'returns an ExternalIssue' do - issue = project.get_issue('FOO-1234') + issue = project.get_issue('FOO-1234', user) expect(issue).to be_kind_of(ExternalIssue) expect(issue.iid).to eq 'FOO-1234' expect(issue.project).to eq project diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index b17553211d2..ae7994af981 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -365,6 +365,24 @@ describe API::API, api: true do let(:base_url) { "/projects/#{project.id}" } let(:title) { milestone.title } + it "returns 404 on private projects for other users" do + private_project = create(:empty_project, :private) + create(:issue, project: private_project) + + get api("/projects/#{private_project.id}/issues", non_member) + + expect(response).to have_http_status(404) + end + + it 'returns no issues when user has access to project but not issues' do + restricted_project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + create(:issue, project: restricted_project) + + get api("/projects/#{restricted_project.id}/issues", non_member) + + expect(json_response).to eq([]) + end + it 'returns project issues without confidential issues for non project members' do get api("#{base_url}/issues", non_member) expect(response).to have_http_status(200) diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index 3f5df049ea2..dc945ca4868 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -24,6 +24,8 @@ describe MergeRequests::BuildService, services: true do end before do + project.team << [user, :guest] + allow(CompareService).to receive_message_chain(:new, :execute).and_return(compare) allow(project).to receive(:commit).and_return(commit_1) allow(project).to receive(:commit).and_return(commit_2) @@ -168,6 +170,16 @@ describe MergeRequests::BuildService, services: true do expect(merge_request.title).to eq("Resolve \"#{issue.title}\"") end + context 'when issue is not accessible to user' do + before do + project.team.truncate + end + + it 'uses branch title as the merge request title' do + expect(merge_request.title).to eq("#{issue.iid} fix issue") + end + end + context 'issue does not exist' do let(:source_branch) { "#{issue.iid.succ}-fix-issue" } -- cgit v1.2.1 From 25e1bbd1a80f3139504ad86f6728cde8a564d5da Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Tue, 29 Nov 2016 08:37:02 +0100 Subject: fix blob controller spec failure --- app/controllers/projects/blob_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 9940263ae24..398122b3073 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -119,7 +119,7 @@ class Projects::BlobController < Projects::ApplicationController from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:from_merge_request_iid]) if from_merge_request && @target_branch == @ref diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) + - "##{hexdigest(@path)}" + "#file-path-#{hexdigest(@path)}" else namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path)) end -- cgit v1.2.1 From 280afe0a6480185f61c4f107724367bd5a170b2a Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Tue, 29 Nov 2016 10:40:56 +0100 Subject: fix blob controller spec failure - updated not to use file-path- --- app/controllers/projects/blob_controller.rb | 2 +- spec/controllers/projects/blob_controller_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 398122b3073..9940263ae24 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -119,7 +119,7 @@ class Projects::BlobController < Projects::ApplicationController from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:from_merge_request_iid]) if from_merge_request && @target_branch == @ref diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) + - "#file-path-#{hexdigest(@path)}" + "##{hexdigest(@path)}" else namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path)) end diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index 1c2b0a4a45c..3efef757ae2 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -67,7 +67,7 @@ describe Projects::BlobController do put :update, mr_params after_edit_path = diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) - file_anchor = "#file-path-#{Digest::SHA1.hexdigest('CHANGELOG')}" + file_anchor = "##{Digest::SHA1.hexdigest('CHANGELOG')}" expect(response).to redirect_to(after_edit_path + file_anchor) end -- cgit v1.2.1 From adb3f3d4e494e8f8d41c1b9e676e395a49cd96b2 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Fri, 25 Nov 2016 12:51:12 +0100 Subject: Move MWPS document to new location --- doc/intro/README.md | 2 +- doc/user/project/merge_requests.md | 2 +- .../merge_requests/merge_when_build_succeeds.md | 47 ++-------------------- .../merge_requests/merge_when_pipeline_succeeds.md | 46 +++++++++++++++++++++ doc/workflow/README.md | 4 +- doc/workflow/merge_when_build_succeeds.md | 2 +- 6 files changed, 54 insertions(+), 49 deletions(-) create mode 100644 doc/user/project/merge_requests/merge_when_pipeline_succeeds.md diff --git a/doc/intro/README.md b/doc/intro/README.md index 6deed35b7c9..1df6a52ce8a 100644 --- a/doc/intro/README.md +++ b/doc/intro/README.md @@ -23,7 +23,7 @@ Create merge requests and review code. - [Fork a project and contribute to it](../workflow/forking_workflow.md) - [Create a new merge request](../gitlab-basics/add-merge-request.md) - [Automatically close issues from merge requests](../user/project/issues/automatic_issue_closing.md) -- [Automatically merge when pipeline succeeds](../user/project/merge_requests/merge_when_build_succeeds.md) +- [Automatically merge when pipeline succeeds](../user/project/merge_requests/merge_when_pipeline_succeeds.md) - [Revert any commit](../user/project/merge_requests/revert_changes.md) - [Cherry-pick any commit](../user/project/merge_requests/cherry_pick_changes.md) diff --git a/doc/user/project/merge_requests.md b/doc/user/project/merge_requests.md index e76428d41f3..be09337319f 100644 --- a/doc/user/project/merge_requests.md +++ b/doc/user/project/merge_requests.md @@ -26,7 +26,7 @@ more CI builds running, you can set it to be merged automatically when CI pipeline succeeds. This way, you don't have to wait for the pipeline to finish and remember to merge the request manually. -[Learn more about merging when pipeline succeeds.](merge_requests/merge_when_build_succeeds.md) +[Learn more about merging when pipeline succeeds.](merge_requests/merge_when_pipeline_succeeds.md) ## Resolve discussion comments in merge requests reviews diff --git a/doc/user/project/merge_requests/merge_when_build_succeeds.md b/doc/user/project/merge_requests/merge_when_build_succeeds.md index 75ad18b28cf..2167fdfbf7e 100644 --- a/doc/user/project/merge_requests/merge_when_build_succeeds.md +++ b/doc/user/project/merge_requests/merge_when_build_succeeds.md @@ -1,46 +1,5 @@ -# Merge When Pipeline Succeeds +This document was moved to [merge_when_pipeline_succeeds](merge_when_pipeline_succeeds.md). -When reviewing a merge request that looks ready to merge but still has one or -more CI builds running, you can set it to be merged automatically when the -builds pipeline succeeds. This way, you don't have to wait for the builds to -finish and remember to merge the request manually. +>[Introduced][ce-7135] by the "Rename MWBS service to Merge When Pipeline Succeeds" change. -![Enable](img/merge_when_build_succeeds_enable.png) - -When you hit the "Merge When Pipeline Succeeds" button, the status of the merge -request will be updated to represent the impending merge. If you cannot wait -for the pipeline to succeed and want to merge immediately, this option is -available in the dropdown menu on the right of the main button. - -Both team developers and the author of the merge request have the option to -cancel the automatic merge if they find a reason why it shouldn't be merged -after all. - -![Status](img/merge_when_build_succeeds_status.png) - -When the pipeline succeeds, the merge request will automatically be merged. -When the pipeline fails, the author gets a chance to retry any failed builds, -or to push new commits to fix the failure. - -When the builds are retried and succeed on the second try, the merge request -will automatically be merged after all. When the merge request is updated with -new commits, the automatic merge is automatically canceled to allow the new -changes to be reviewed. - -## Only allow merge requests to be merged if the pipeline succeeds - -> **Note:** -You need to have builds configured to enable this feature. - -You can prevent merge requests from being merged if their pipeline did not succeed. - -Navigate to your project's settings page, select the -**Only allow merge requests to be merged if the pipeline succeeds** check box and -hit **Save** for the changes to take effect. - -![Only allow merge if pipeline succeeds settings](img/merge_when_build_succeeds_only_if_succeeds_settings.png) - -From now on, every time the pipeline fails you will not be able to merge the -merge request from the UI, until you make all relevant builds pass. - -![Only allow merge if pipeline succeeds message](img/merge_when_build_succeeds_only_if_succeeds_msg.png) +[ce-7135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7135 diff --git a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md new file mode 100644 index 00000000000..75ad18b28cf --- /dev/null +++ b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md @@ -0,0 +1,46 @@ +# Merge When Pipeline Succeeds + +When reviewing a merge request that looks ready to merge but still has one or +more CI builds running, you can set it to be merged automatically when the +builds pipeline succeeds. This way, you don't have to wait for the builds to +finish and remember to merge the request manually. + +![Enable](img/merge_when_build_succeeds_enable.png) + +When you hit the "Merge When Pipeline Succeeds" button, the status of the merge +request will be updated to represent the impending merge. If you cannot wait +for the pipeline to succeed and want to merge immediately, this option is +available in the dropdown menu on the right of the main button. + +Both team developers and the author of the merge request have the option to +cancel the automatic merge if they find a reason why it shouldn't be merged +after all. + +![Status](img/merge_when_build_succeeds_status.png) + +When the pipeline succeeds, the merge request will automatically be merged. +When the pipeline fails, the author gets a chance to retry any failed builds, +or to push new commits to fix the failure. + +When the builds are retried and succeed on the second try, the merge request +will automatically be merged after all. When the merge request is updated with +new commits, the automatic merge is automatically canceled to allow the new +changes to be reviewed. + +## Only allow merge requests to be merged if the pipeline succeeds + +> **Note:** +You need to have builds configured to enable this feature. + +You can prevent merge requests from being merged if their pipeline did not succeed. + +Navigate to your project's settings page, select the +**Only allow merge requests to be merged if the pipeline succeeds** check box and +hit **Save** for the changes to take effect. + +![Only allow merge if pipeline succeeds settings](img/merge_when_build_succeeds_only_if_succeeds_settings.png) + +From now on, every time the pipeline fails you will not be able to merge the +merge request from the UI, until you make all relevant builds pass. + +![Only allow merge if pipeline succeeds message](img/merge_when_build_succeeds_only_if_succeeds_msg.png) diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 57a0d21c7a7..59a806de210 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -25,12 +25,12 @@ - [Merge Requests](../user/project/merge_requests.md) - [Authorization for merge requests](../user/project/merge_requests/authorization_for_merge_requests.md) - [Cherry-pick changes](../user/project/merge_requests/cherry_pick_changes.md) - - [Merge when pipeline succeeds](../user/project/merge_requests/merge_when_build_succeeds.md) + - [Merge when pipeline succeeds](../user/project/merge_requests/merge_when_pipeline_succeeds.md) - [Resolve discussion comments in merge requests reviews](../user/project/merge_requests/merge_request_discussion_resolution.md) - [Resolve merge conflicts in the UI](../user/project/merge_requests/resolve_conflicts.md) - [Revert changes in the UI](../user/project/merge_requests/revert_changes.md) - [Merge requests versions](../user/project/merge_requests/versions.md) - ["Work In Progress" merge requests](../user/project/merge_requests/work_in_progress_merge_requests.md) - [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md) -- [Importing from SVN, GitHub, BitBucket, etc](importing/README.md) +- [Importing from SVN, GitHub, Bitbucket, etc](importing/README.md) - [Todos](todos.md) diff --git a/doc/workflow/merge_when_build_succeeds.md b/doc/workflow/merge_when_build_succeeds.md index 95afd12ebdb..b4f6d6117de 100644 --- a/doc/workflow/merge_when_build_succeeds.md +++ b/doc/workflow/merge_when_build_succeeds.md @@ -1 +1 @@ -This document was moved to [user/project/merge_requests/merge_when_build_succeeds](../user/project/merge_requests/merge_when_build_succeeds.md). +This document was moved to [merge_when_pipeline_succeeds](../user/project/merge_requests/merge_when_pipeline_succeeds.md). -- cgit v1.2.1 From a32dd93a4c5c0ae647beac425ed23e4b9c3d27f2 Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Tue, 29 Nov 2016 12:41:11 +0100 Subject: fix started_at check --- app/serializers/analytics_build_entity.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/serializers/analytics_build_entity.rb b/app/serializers/analytics_build_entity.rb index 206a7eadbcf..a0db5b8f0f4 100644 --- a/app/serializers/analytics_build_entity.rb +++ b/app/serializers/analytics_build_entity.rb @@ -13,7 +13,7 @@ class AnalyticsBuildEntity < Grape::Entity end expose :duration, as: :total_time do |build| - build_started?(build) ? distance_of_time_as_hash(build.duration.to_f) : {} + build.duration ? distance_of_time_as_hash(build.duration.to_f) : {} end expose :branch do @@ -37,8 +37,4 @@ class AnalyticsBuildEntity < Grape::Entity def url_to(route, build, id = nil) public_send("#{route}_url", build.project.namespace, build.project, id || build) end - - def build_started?(build) - build.duration && build[:started_at] - end end -- cgit v1.2.1 From 8b3ab222c3370303e39aad9574c59b960fbfc299 Mon Sep 17 00:00:00 2001 From: Lee Matos <lee@gitlab.com> Date: Tue, 29 Nov 2016 01:47:06 +0000 Subject: Fixing typo & Clarifying Key name --- doc/integration/bitbucket.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index 556d71b8b76..9122dc62e39 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -123,7 +123,7 @@ To be able to access repositories on Bitbucket, GitLab will automatically register your public key with Bitbucket as a deploy key for the repositories to be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa` which translates to `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages and to -`/home/git/.ssh/bitbucket_rsa.pub` for installations from source. +`/home/git/.ssh/bitbucket_rsa` for installations from source. --- @@ -199,7 +199,7 @@ Your GitLab server is now able to connect to Bitbucket over SSH. You should be able to see the "Import projects from Bitbucket" option on the New Project page enabled. -## Acknowledgemts +## Acknowledgements Special thanks to the writer behind the following article: -- cgit v1.2.1 From 41bf093662a24cc6b68eba3503b56ac44b7f6e69 Mon Sep 17 00:00:00 2001 From: Timothy Andrew <mail@timothyandrew.net> Date: Tue, 29 Nov 2016 16:51:50 +0530 Subject: CE-specific changes gitlab-org/gitlab-ee#1137 - Extract all common {push,merge} access level model code into the `ProtectedBranchAccess` module - Use the HTTP verb to define controller specs --- app/models/concerns/protected_branch_access.rb | 9 +++++++++ app/models/protected_branch/merge_access_level.rb | 9 --------- app/models/protected_branch/push_access_level.rb | 6 +----- spec/controllers/autocomplete_controller_spec.rb | 4 ++-- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb index 7fd0905ee81..9dd4d9c6f24 100644 --- a/app/models/concerns/protected_branch_access.rb +++ b/app/models/concerns/protected_branch_access.rb @@ -2,6 +2,9 @@ module ProtectedBranchAccess extend ActiveSupport::Concern included do + belongs_to :protected_branch + delegate :project, to: :protected_branch + scope :master, -> { where(access_level: Gitlab::Access::MASTER) } scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) } end @@ -9,4 +12,10 @@ module ProtectedBranchAccess def humanize self.class.human_access_levels[self.access_level] end + + def check_access(user) + return true if user.is_admin? + + project.team.max_member_access(user.id) >= access_level + end end diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb index 806b3ccd275..771e3376613 100644 --- a/app/models/protected_branch/merge_access_level.rb +++ b/app/models/protected_branch/merge_access_level.rb @@ -1,9 +1,6 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base include ProtectedBranchAccess - belongs_to :protected_branch - delegate :project, to: :protected_branch - validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER, Gitlab::Access::DEVELOPER] } @@ -13,10 +10,4 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base Gitlab::Access::DEVELOPER => "Developers + Masters" }.with_indifferent_access end - - def check_access(user) - return true if user.is_admin? - - project.team.max_member_access(user.id) >= access_level - end end diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb index 92e9c51d883..14610cb42b7 100644 --- a/app/models/protected_branch/push_access_level.rb +++ b/app/models/protected_branch/push_access_level.rb @@ -1,9 +1,6 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base include ProtectedBranchAccess - belongs_to :protected_branch - delegate :project, to: :protected_branch - validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER, Gitlab::Access::DEVELOPER, Gitlab::Access::NO_ACCESS] } @@ -18,8 +15,7 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base def check_access(user) return false if access_level == Gitlab::Access::NO_ACCESS - return true if user.is_admin? - project.team.max_member_access(user.id) >= access_level + super end end diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index d9a86346c81..0d1545040f1 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -4,7 +4,7 @@ describe AutocompleteController do let!(:project) { create(:project) } let!(:user) { create(:user) } - context 'users and members' do + context 'GET users' do let!(:user2) { create(:user) } let!(:non_member) { create(:user) } @@ -180,7 +180,7 @@ describe AutocompleteController do end end - context 'projects' do + context 'GET projects' do let(:authorized_project) { create(:project) } let(:authorized_search_project) { create(:project, name: 'rugged') } -- cgit v1.2.1 From a48ef15620bd479bcbe1c47e60eaa4c84dbd0f8f Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Mon, 28 Nov 2016 16:44:24 +0200 Subject: Remove unnecessary database indexes --- .../unreleased/removing_unnecessary_indexes.yml | 4 +++ .../20161128142110_remove_unnecessary_indexes.rb | 33 ++++++++++++++++++++++ db/schema.rb | 9 ------ 3 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 changelogs/unreleased/removing_unnecessary_indexes.yml create mode 100644 db/migrate/20161128142110_remove_unnecessary_indexes.rb diff --git a/changelogs/unreleased/removing_unnecessary_indexes.yml b/changelogs/unreleased/removing_unnecessary_indexes.yml new file mode 100644 index 00000000000..01314ab5585 --- /dev/null +++ b/changelogs/unreleased/removing_unnecessary_indexes.yml @@ -0,0 +1,4 @@ +--- +title: Remove unnecessary database indices +merge_request: +author: diff --git a/db/migrate/20161128142110_remove_unnecessary_indexes.rb b/db/migrate/20161128142110_remove_unnecessary_indexes.rb new file mode 100644 index 00000000000..9deab19782e --- /dev/null +++ b/db/migrate/20161128142110_remove_unnecessary_indexes.rb @@ -0,0 +1,33 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveUnnecessaryIndexes < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + DOWNTIME = false + + def up + remove_index :labels, column: :group_id if index_exists?(:labels, :group_id) + remove_index :award_emoji, column: :user_id if index_exists?(:award_emoji, :user_id) + remove_index :ci_builds, column: :commit_id if index_exists?(:ci_builds, :commit_id) + remove_index :deployments, column: :project_id if index_exists?(:deployments, :project_id) + remove_index :deployments, column: ["project_id", "environment_id"] if index_exists?(:deployments, ["project_id", "environment_id"]) + remove_index :lists, column: :board_id if index_exists?(:lists, :board_id) + remove_index :milestones, column: :project_id if index_exists?(:milestones, :project_id) + remove_index :notes, column: :project_id if index_exists?(:notes, :project_id) + remove_index :users_star_projects, column: :user_id if index_exists?(:users_star_projects, :user_id) + end + + def down + add_concurrent_index :labels, :group_id + add_concurrent_index :award_emoji, :user_id + add_concurrent_index :ci_builds, :commit_id + add_concurrent_index :deployments, :project_id + add_concurrent_index :deployments, ["project_id", "environment_id"] + add_concurrent_index :lists, :board_id + add_concurrent_index :milestones, :project_id + add_concurrent_index :notes, :project_id + add_concurrent_index :users_star_projects, :user_id + end +end diff --git a/db/schema.rb b/db/schema.rb index 3d630a148f0..0d510c8a269 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -132,7 +132,6 @@ ActiveRecord::Schema.define(version: 20161128161412) do add_index "award_emoji", ["awardable_type", "awardable_id"], name: "index_award_emoji_on_awardable_type_and_awardable_id", using: :btree add_index "award_emoji", ["user_id", "name"], name: "index_award_emoji_on_user_id_and_name", using: :btree - add_index "award_emoji", ["user_id"], name: "index_award_emoji_on_user_id", using: :btree create_table "boards", force: :cascade do |t| t.integer "project_id", null: false @@ -220,7 +219,6 @@ ActiveRecord::Schema.define(version: 20161128161412) do 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 add_index "ci_builds", ["gl_project_id"], name: "index_ci_builds_on_gl_project_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 @@ -410,9 +408,7 @@ ActiveRecord::Schema.define(version: 20161128161412) do end add_index "deployments", ["project_id", "environment_id", "iid"], name: "index_deployments_on_project_id_and_environment_id_and_iid", using: :btree - add_index "deployments", ["project_id", "environment_id"], name: "index_deployments_on_project_id_and_environment_id", using: :btree add_index "deployments", ["project_id", "iid"], name: "index_deployments_on_project_id_and_iid", unique: true, using: :btree - add_index "deployments", ["project_id"], name: "index_deployments_on_project_id", using: :btree create_table "emails", force: :cascade do |t| t.integer "user_id", null: false @@ -570,7 +566,6 @@ ActiveRecord::Schema.define(version: 20161128161412) do end add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree - add_index "labels", ["group_id"], name: "index_labels_on_group_id", using: :btree create_table "lfs_objects", force: :cascade do |t| t.string "oid", null: false @@ -601,7 +596,6 @@ ActiveRecord::Schema.define(version: 20161128161412) do end add_index "lists", ["board_id", "label_id"], name: "index_lists_on_board_id_and_label_id", unique: true, using: :btree - add_index "lists", ["board_id"], name: "index_lists_on_board_id", using: :btree add_index "lists", ["label_id"], name: "index_lists_on_label_id", using: :btree create_table "members", force: :cascade do |t| @@ -727,7 +721,6 @@ ActiveRecord::Schema.define(version: 20161128161412) do add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} 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 add_index "milestones", ["title"], name: "index_milestones_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} @@ -790,7 +783,6 @@ ActiveRecord::Schema.define(version: 20161128161412) do 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 - add_index "notes", ["project_id"], name: "index_notes_on_project_id", using: :btree add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree create_table "notification_settings", force: :cascade do |t| @@ -1243,7 +1235,6 @@ ActiveRecord::Schema.define(version: 20161128161412) do add_index "users_star_projects", ["project_id"], name: "index_users_star_projects_on_project_id", using: :btree add_index "users_star_projects", ["user_id", "project_id"], name: "index_users_star_projects_on_user_id_and_project_id", unique: true, using: :btree - add_index "users_star_projects", ["user_id"], name: "index_users_star_projects_on_user_id", using: :btree create_table "web_hooks", force: :cascade do |t| t.string "url", limit: 2000 -- cgit v1.2.1 From a49e9949c6bc474c8bfd4016d9c6c3b59776772f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 28 Nov 2016 11:13:32 +0100 Subject: Rename `MergeRequest#pipeline` to `head_pipeline` --- .../projects/merge_requests_controller.rb | 15 ++++++++------- app/models/ci/pipeline.rb | 2 +- app/models/merge_request.rb | 6 +++--- app/services/merge_requests/base_service.rb | 4 ++-- .../projects/issues/_merge_requests.html.haml | 6 +++--- .../merge_requests/_merge_request.html.haml | 4 ++-- .../projects/merge_requests/widget/_show.html.haml | 2 +- lib/api/merge_requests.rb | 2 +- spec/models/merge_request_spec.rb | 18 +++++++++--------- spec/requests/api/merge_requests_spec.rb | 2 +- .../add_todo_when_build_fails_service_spec.rb | 12 +++++++++--- .../merge_when_build_succeeds_service_spec.rb | 22 ++++++++++++++++------ 12 files changed, 56 insertions(+), 39 deletions(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index a2225cc8343..f47df8b623b 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -325,16 +325,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request.update(merge_error: nil) if params[:merge_when_build_succeeds].present? - unless @merge_request.pipeline + unless @merge_request.head_pipeline @status = :failed return end - if @merge_request.pipeline.active? + if @merge_request.head_pipeline.active? MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params) .execute(@merge_request) @status = :merge_when_build_succeeds - elsif @merge_request.pipeline.success? + elsif @merge_request.head_pipeline.success? # This can be triggered when a user clicks the auto merge button while # the tests finish at about the same time MergeWorker.perform_async(@merge_request.id, current_user.id, params) @@ -398,7 +398,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def ci_status - pipeline = @merge_request.pipeline + pipeline = @merge_request.head_pipeline + if pipeline status = pipeline.status coverage = pipeline.try(:coverage) @@ -534,7 +535,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def define_widget_vars - @pipeline = @merge_request.pipeline + @pipeline = @merge_request.head_pipeline end def define_commit_vars @@ -563,7 +564,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def define_pipelines_vars @pipelines = @merge_request.all_pipelines - @pipeline = @merge_request.pipeline + @pipeline = @merge_request.head_pipeline @statuses_count = @pipeline.present? ? @pipeline.statuses.relevant.count : 0 end @@ -631,7 +632,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def merge_when_build_succeeds_active? params[:merge_when_build_succeeds].present? && - @merge_request.pipeline && @merge_request.pipeline.active? + @merge_request.head_pipeline && @merge_request.head_pipeline.active? end def build_merge_request diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 4294a10e9e3..fabbf97d4db 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -317,7 +317,7 @@ module Ci def merge_requests @merge_requests ||= project.merge_requests .where(source_branch: self.ref) - .select { |merge_request| merge_request.pipeline.try(:id) == self.id } + .select { |merge_request| merge_request.head_pipeline.try(:id) == self.id } end private diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 38d8c15e6b0..64990f8134e 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -678,7 +678,7 @@ class MergeRequest < ActiveRecord::Base def mergeable_ci_state? return true unless project.only_allow_merge_if_build_succeeds? - !pipeline || pipeline.success? || pipeline.skipped? + !head_pipeline || head_pipeline.success? || head_pipeline.skipped? end def environments @@ -774,10 +774,10 @@ class MergeRequest < ActiveRecord::Base commits.map(&:sha) end - def pipeline + def head_pipeline return unless diff_head_sha && source_project - @pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha) + @head_pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha) end def all_pipelines diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index 58f69a41e14..800fd39c424 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -55,7 +55,7 @@ module MergeRequests def pipeline_merge_requests(pipeline) merge_requests_for(pipeline.ref).each do |merge_request| - next unless pipeline == merge_request.pipeline + next unless pipeline == merge_request.head_pipeline yield merge_request end @@ -63,7 +63,7 @@ module MergeRequests def commit_status_merge_requests(commit_status) merge_requests_for(commit_status.ref).each do |merge_request| - pipeline = merge_request.pipeline + pipeline = merge_request.head_pipeline next unless pipeline next unless pipeline.sha == commit_status.sha diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index 747bfa554cb..d48923b422a 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -2,12 +2,12 @@ %h2.merge-requests-title = pluralize(@merge_requests.count, 'Related Merge Request') %ul.unstyled-list.related-merge-requests - - has_any_ci = @merge_requests.any?(&:pipeline) + - has_any_ci = @merge_requests.any?(&:head_pipeline) - @merge_requests.each do |merge_request| %li %span.merge-request-ci-status - - if merge_request.pipeline - = render_pipeline_status(merge_request.pipeline) + - if merge_request.head_pipeline + = render_pipeline_status(merge_request.head_pipeline) - elsif has_any_ci = icon('blank fw') %span.merge-request-id diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 9ffcc48eb80..fa189ae62d8 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -15,9 +15,9 @@ = icon('ban') CLOSED - - if merge_request.pipeline + - if merge_request.head_pipeline %li - = render_pipeline_status(merge_request.pipeline) + = render_pipeline_status(merge_request.head_pipeline) - if merge_request.open? && merge_request.broken? %li diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index 608fdf1c5f5..a8918c85dde 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -14,7 +14,7 @@ ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", ci_environments_status_url: "#{ci_environments_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", gitlab_icon: "#{asset_path 'gitlab_logo.png'}", - ci_status: "#{@merge_request.pipeline ? @merge_request.pipeline.status : ''}", + ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}", ci_message: { normal: "Build {{status}} for \"{{title}}\"", preparing: "{{status}} build for \"{{title}}\"" diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 90fa588b455..97baebc1d27 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -192,7 +192,7 @@ module API should_remove_source_branch: params[:should_remove_source_branch] } - if params[:merge_when_build_succeeds] && merge_request.pipeline && merge_request.pipeline.active? + if params[:merge_when_build_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active? ::MergeRequests::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user, merge_params). execute(merge_request) else diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 58ccd056328..26034cb1c7b 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -570,7 +570,7 @@ describe MergeRequest, models: true do end end - describe '#pipeline' do + describe '#head_pipeline' do describe 'when the source project exists' do it 'returns the latest pipeline' do pipeline = double(:ci_pipeline, ref: 'master') @@ -581,7 +581,7 @@ describe MergeRequest, models: true do with('master', '123abc'). and_return(pipeline) - expect(subject.pipeline).to eq(pipeline) + expect(subject.head_pipeline).to eq(pipeline) end end @@ -589,7 +589,7 @@ describe MergeRequest, models: true do it 'returns nil' do allow(subject).to receive(:source_project).and_return(nil) - expect(subject.pipeline).to be_nil + expect(subject.head_pipeline).to be_nil end end end @@ -857,7 +857,7 @@ describe MergeRequest, models: true do context 'and a failed pipeline is associated' do before do pipeline.update(status: 'failed') - allow(subject).to receive(:pipeline) { pipeline } + allow(subject).to receive(:head_pipeline) { pipeline } end it { expect(subject.mergeable_ci_state?).to be_falsey } @@ -866,7 +866,7 @@ describe MergeRequest, models: true do context 'and a successful pipeline is associated' do before do pipeline.update(status: 'success') - allow(subject).to receive(:pipeline) { pipeline } + allow(subject).to receive(:head_pipeline) { pipeline } end it { expect(subject.mergeable_ci_state?).to be_truthy } @@ -875,7 +875,7 @@ describe MergeRequest, models: true do context 'and a skipped pipeline is associated' do before do pipeline.update(status: 'skipped') - allow(subject).to receive(:pipeline) { pipeline } + allow(subject).to receive(:head_pipeline) { pipeline } end it { expect(subject.mergeable_ci_state?).to be_truthy } @@ -883,7 +883,7 @@ describe MergeRequest, models: true do context 'when no pipeline is associated' do before do - allow(subject).to receive(:pipeline) { nil } + allow(subject).to receive(:head_pipeline) { nil } end it { expect(subject.mergeable_ci_state?).to be_truthy } @@ -896,7 +896,7 @@ describe MergeRequest, models: true do context 'and a failed pipeline is associated' do before do pipeline.statuses << create(:commit_status, status: 'failed', project: project) - allow(subject).to receive(:pipeline) { pipeline } + allow(subject).to receive(:head_pipeline) { pipeline } end it { expect(subject.mergeable_ci_state?).to be_truthy } @@ -904,7 +904,7 @@ describe MergeRequest, models: true do context 'when no pipeline is associated' do before do - allow(subject).to receive(:pipeline) { nil } + allow(subject).to receive(:head_pipeline) { nil } end it { expect(subject.mergeable_ci_state?).to be_truthy } diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 3ecf3eea5f5..edc985b765b 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -466,7 +466,7 @@ describe API::API, api: true do end it "enables merge when build succeeds if the ci is active" do - allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline) + allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline) allow(pipeline).to receive(:active?).and_return(true) put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true diff --git a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb index a44312dd363..bb7830c7eea 100644 --- a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb +++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb @@ -20,13 +20,19 @@ describe MergeRequests::AddTodoWhenBuildFailsService do let(:todo_service) { TodoService.new } let(:merge_request) do - create(:merge_request, merge_user: user, source_branch: 'master', - target_branch: 'feature', source_project: project, target_project: project, + create(:merge_request, merge_user: user, + source_branch: 'master', + target_branch: 'feature', + source_project: project, + target_project: project, state: 'opened') end before do - allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline) + allow_any_instance_of(MergeRequest) + .to receive(:head_pipeline) + .and_return(pipeline) + allow(service).to receive(:todo_service).and_return(todo_service) end diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb index c0164138713..963d9573ac4 100644 --- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb @@ -21,7 +21,10 @@ describe MergeRequests::MergeWhenBuildSucceedsService do context 'first time enabling' do before do - allow(merge_request).to receive(:pipeline).and_return(pipeline) + allow(merge_request) + .to receive(:head_pipeline) + .and_return(pipeline) + service.execute(merge_request) end @@ -43,8 +46,12 @@ describe MergeRequests::MergeWhenBuildSucceedsService do let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch) } before do - allow(mr_merge_if_green_enabled).to receive(:pipeline).and_return(pipeline) - allow(mr_merge_if_green_enabled).to receive(:mergeable?).and_return(true) + allow(mr_merge_if_green_enabled).to receive(:head_pipeline) + .and_return(pipeline) + + allow(mr_merge_if_green_enabled).to receive(:mergeable?) + .and_return(true) + allow(pipeline).to receive(:success?).and_return(true) end @@ -138,9 +145,12 @@ describe MergeRequests::MergeWhenBuildSucceedsService do before do # This behavior of MergeRequest: we instantiate a new object - allow_any_instance_of(MergeRequest).to receive(:pipeline).and_wrap_original do - Ci::Pipeline.find(pipeline.id) - end + # + allow_any_instance_of(MergeRequest) + .to receive(:head_pipeline) + .and_wrap_original do + Ci::Pipeline.find(pipeline.id) + end end it "doesn't merge if any of stages failed" do -- cgit v1.2.1 From 651eccda62218532b24ff75384c943256bf70224 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 29 Nov 2016 11:57:16 +0100 Subject: Expose timestamp in build entity used by serializer --- app/serializers/build_entity.rb | 3 +++ spec/serializers/build_entity_spec.rb | 25 ++++++++++++++++--------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/app/serializers/build_entity.rb b/app/serializers/build_entity.rb index cf1c418a88e..b5384e6462b 100644 --- a/app/serializers/build_entity.rb +++ b/app/serializers/build_entity.rb @@ -16,6 +16,9 @@ class BuildEntity < Grape::Entity path_to(:play_namespace_project_build, build) end + expose :created_at + expose :updated_at + private def path_to(route, build) diff --git a/spec/serializers/build_entity_spec.rb b/spec/serializers/build_entity_spec.rb index 6dcfaec259e..60c9642ee2c 100644 --- a/spec/serializers/build_entity_spec.rb +++ b/spec/serializers/build_entity_spec.rb @@ -1,23 +1,30 @@ require 'spec_helper' describe BuildEntity do + let(:build) { create(:ci_build) } + let(:entity) do described_class.new(build, request: double) end subject { entity.as_json } - context 'when build is a regular job' do - let(:build) { create(:ci_build) } + it 'contains paths to build page and retry action' do + expect(subject).to include(:build_path, :retry_path) + end - it 'contains paths to build page and retry action' do - expect(subject).to include(:build_path, :retry_path) - expect(subject).not_to include(:play_path) - end + it 'does not contain sensitive information' do + expect(subject).not_to include(/token/) + expect(subject).not_to include(/variables/) + end + + it 'contains timestamps' do + expect(subject).to include(:created_at, :updated_at) + end - it 'does not contain sensitive information' do - expect(subject).not_to include(/token/) - expect(subject).not_to include(/variables/) + context 'when build is a regular job' do + it 'does not contain path to play action' do + expect(subject).not_to include(:play_path) end end -- cgit v1.2.1 From 867dcdf75012a7b7c8b131d46b275b28fa5bfa00 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Mon, 28 Nov 2016 18:18:48 -0200 Subject: Alert user when logged in user email is not the same as the invitation --- app/views/invites/show.html.haml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/views/invites/show.html.haml b/app/views/invites/show.html.haml index 2fd4859c1c6..882fdf1317d 100644 --- a/app/views/invites/show.html.haml +++ b/app/views/invites/show.html.haml @@ -6,7 +6,7 @@ - if inviter = @member.created_by by = link_to inviter.name, user_url(inviter) - to join + to join - case @member.source - when Project - project = @member.source @@ -20,11 +20,18 @@ = link_to group.name, group_url(group) as #{@member.human_access}. -- if @member.source.users.include?(current_user) +- is_member = @member.source.users.include?(current_user) + +- if is_member %p However, you are already a member of this #{@member.source.is_a?(Group) ? "group" : "project"}. Sign in using a different account to accept the invitation. -- else + +- if @member.invite_email != current_user.email + %p + Note that this invitation was sent to #{mail_to @member.invite_email}, but you are signed in as #{link_to current_user.to_reference, user_url(current_user)} with email #{mail_to current_user.email}. + +- unless is_member .actions = link_to "Accept invitation", accept_invite_url(@token), method: :post, class: "btn btn-success" = link_to "Decline", decline_invite_url(@token), method: :post, class: "btn btn-danger prepend-left-10" -- cgit v1.2.1 From 6a7bc1c12219aedbbc3343f8e00aadbaab9ec266 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Mon, 28 Nov 2016 18:21:45 -0200 Subject: Add a CHANGELOG entry --- changelogs/unreleased/improve-invite-accept-page.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/improve-invite-accept-page.yml diff --git a/changelogs/unreleased/improve-invite-accept-page.yml b/changelogs/unreleased/improve-invite-accept-page.yml new file mode 100644 index 00000000000..8a09a5ae42f --- /dev/null +++ b/changelogs/unreleased/improve-invite-accept-page.yml @@ -0,0 +1,4 @@ +--- +title: Add note to the invite page when the logged in user email is not the same as the invitation +merge_request: +author: -- cgit v1.2.1 From 49491e63b818b757bd05c9b1d0b447f1baf30db0 Mon Sep 17 00:00:00 2001 From: Stan Hu <stanhu@gmail.com> Date: Mon, 28 Nov 2016 23:38:14 -0800 Subject: Update Sidekiq-cron to fix compatibility issues with Sidekiq 4.2.1 The "Enqueue Now" button would not work in the admin panel due to changes in the Web extension interface. Closes #24376 --- Gemfile | 2 +- Gemfile.lock | 9 +++++---- changelogs/unreleased/sh-update-sidekiq-cron.yml | 4 ++++ 3 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 changelogs/unreleased/sh-update-sidekiq-cron.yml diff --git a/Gemfile b/Gemfile index 1d686199557..350ecd0ca02 100644 --- a/Gemfile +++ b/Gemfile @@ -133,7 +133,7 @@ gem 'acts-as-taggable-on', '~> 4.0' # Background jobs gem 'sidekiq', '~> 4.2' -gem 'sidekiq-cron', '~> 0.4.0' +gem 'sidekiq-cron', '~> 0.4.4' gem 'redis-namespace', '~> 1.5.2' gem 'sidekiq-limit_fetch', '~> 3.4' diff --git a/Gemfile.lock b/Gemfile.lock index bf9702b2562..40aa91423c0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -614,7 +614,8 @@ GEM rubyntlm (0.5.2) rubypants (0.2.0) rubyzip (1.2.0) - rufus-scheduler (3.1.10) + rufus-scheduler (3.3.0) + tzinfo rugged (0.24.0) safe_yaml (1.0.4) sanitize (2.1.0) @@ -650,10 +651,10 @@ GEM connection_pool (~> 2.2, >= 2.2.0) rack-protection (~> 1.5) redis (~> 3.2, >= 3.2.1) - sidekiq-cron (0.4.0) + sidekiq-cron (0.4.4) redis-namespace (>= 1.5.2) rufus-scheduler (>= 2.0.24) - sidekiq (>= 4.0.0) + sidekiq (>= 4.2.1) sidekiq-limit_fetch (3.4.0) sidekiq (>= 4) simplecov (0.12.0) @@ -925,7 +926,7 @@ DEPENDENCIES sham_rack (~> 1.3.6) shoulda-matchers (~> 2.8.0) sidekiq (~> 4.2) - sidekiq-cron (~> 0.4.0) + sidekiq-cron (~> 0.4.4) sidekiq-limit_fetch (~> 3.4) simplecov (= 0.12.0) slack-notifier (~> 1.2.0) diff --git a/changelogs/unreleased/sh-update-sidekiq-cron.yml b/changelogs/unreleased/sh-update-sidekiq-cron.yml new file mode 100644 index 00000000000..d79ba817a18 --- /dev/null +++ b/changelogs/unreleased/sh-update-sidekiq-cron.yml @@ -0,0 +1,4 @@ +--- +title: Update Sidekiq-cron to fix compatibility issues with Sidekiq 4.2.1 +merge_request: +author: -- cgit v1.2.1 From 5e05f9c5c2cfaf14cc0337c23ff54f86c5d5f5a8 Mon Sep 17 00:00:00 2001 From: Nick Thomas <nick@gitlab.com> Date: Fri, 4 Nov 2016 11:43:02 +0000 Subject: Add StackProf to the Gemfile, along with a utility to get a profile for a spec --- Gemfile | 2 + Gemfile.lock | 2 + bin/rspec-stackprof | 16 ++++++ doc/development/performance.md | 110 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+) create mode 100755 bin/rspec-stackprof diff --git a/Gemfile b/Gemfile index 1d686199557..1d86daf580b 100644 --- a/Gemfile +++ b/Gemfile @@ -309,6 +309,8 @@ group :development, :test do gem 'knapsack', '~> 1.11.0' gem 'activerecord_sane_schema_dumper', '0.2' + + gem 'stackprof', '~> 0.2.10' end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index bf9702b2562..50e69425980 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -691,6 +691,7 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) + stackprof (0.2.10) state_machines (0.4.0) state_machines-activemodel (0.4.0) activemodel (>= 4.1, < 5.1) @@ -937,6 +938,7 @@ DEPENDENCIES spring-commands-teaspoon (~> 0.0.2) sprockets (~> 3.7.0) sprockets-es6 (~> 0.9.2) + stackprof (~> 0.2.10) state_machines-activerecord (~> 0.4.0) sys-filesystem (~> 1.1.6) teaspoon (~> 1.1.0) diff --git a/bin/rspec-stackprof b/bin/rspec-stackprof new file mode 100755 index 00000000000..df79feb201d --- /dev/null +++ b/bin/rspec-stackprof @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby + +require 'stackprof' +$:.unshift 'spec' +require 'rails_helper' + +filename = ARGV[0].split('/').last +interval = ENV.fetch('INTERVAL', 1000).to_i +limit = ENV.fetch('LIMIT', 20) +output_file = "tmp/#{filename}.dump" + +StackProf.run(mode: :wall, out: output_file, interval: interval) do + RSpec::Core::Runner.run(ARGV, $stderr, $stdout) +end + +system("stackprof #{output_file} --text --limit #{limit}") diff --git a/doc/development/performance.md b/doc/development/performance.md index 8337c2d9cb3..5c43ae7b79a 100644 --- a/doc/development/performance.md +++ b/doc/development/performance.md @@ -101,6 +101,116 @@ In short: 5. If you must write a benchmark use the benchmark-ips Gem instead of Ruby's `Benchmark` module. +## Profiling + +By collecting snapshots of process state at regular intervals, profiling allows +you to see where time is spent in a process. The [StackProf](https://github.com/tmm1/stackprof) +gem is included in GitLab's development environment, allowing you to investigate +the behaviour of suspect code in detail. + +It's important to note that profiling an application *alters its performance*, +and will generally be done *in an unrepresentative environment*. In particular, +a method is not necessarily troublesome just because it is executed many times, +or takes a long time to execute. Profiles are tools you can use to better +understand what is happening in an application - using that information wisely +is up to you! + +Keeping that in mind, to create a profile, identify (or create) a spec that +exercises the troublesome code path, then run it using the `bin/rspec-stackprof` +helper, e.g.: + +``` +$ LIMIT=10 bin/rspec-stackprof spec/policies/project_policy_spec.rb +8/8 |====== 100 ======>| Time: 00:00:18 + +Finished in 18.19 seconds (files took 4.8 seconds to load) +8 examples, 0 failures + +================================== + Mode: wall(1000) + Samples: 17033 (5.59% miss rate) + GC: 1901 (11.16%) +================================== + TOTAL (pct) SAMPLES (pct) FRAME + 6000 (35.2%) 2566 (15.1%) Sprockets::Cache::FileStore#get + 2018 (11.8%) 888 (5.2%) ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#exec_no_cache + 1338 (7.9%) 640 (3.8%) ActiveRecord::ConnectionAdapters::PostgreSQL::DatabaseStatements#execute + 3125 (18.3%) 394 (2.3%) Sprockets::Cache::FileStore#safe_open + 913 (5.4%) 301 (1.8%) ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#exec_cache + 288 (1.7%) 288 (1.7%) ActiveRecord::Attribute#initialize + 246 (1.4%) 246 (1.4%) Sprockets::Cache::FileStore#safe_stat + 295 (1.7%) 193 (1.1%) block (2 levels) in class_attribute + 187 (1.1%) 187 (1.1%) block (4 levels) in class_attribute +``` + +You can limit the specs that are run by passing any arguments `rspec` would +normally take. + +The output is sorted by the `Samples` column by default. This is the number of +samples taken where the method is the one currently being executed. The `Total` +column shows the number of samples taken where the method, or any of the methods +it calls, were being executed. + +To create a graphical view of the call stack: + +```shell +$ stackprof tmp/project_policy_spec.rb.dump --graphviz > project_policy_spec.dot +$ dot -Tsvg project_policy_spec.dot > project_policy_spec.svg +``` + +To load the profile in [kcachegrind](https://kcachegrind.github.io/): + +``` +$ stackprof tmp/project_policy_spec.dump --callgrind > project_policy_spec.callgrind +$ kcachegrind project_policy_spec.callgrind # Linux +$ qcachegrind project_policy_spec.callgrind # Mac +``` + +It may be useful to zoom in on a specific method, e.g.: + +``` +$ stackprof tmp/project_policy_spec.rb.dump --method warm_asset_cache +TestEnv#warm_asset_cache (/Users/lupine/dev/gitlab.com/gitlab-org/gitlab-development-kit/gitlab/spec/support/test_env.rb:164) + samples: 0 self (0.0%) / 6288 total (36.9%) + callers: + 6288 ( 100.0%) block (2 levels) in <top (required)> + callees (6288 total): + 6288 ( 100.0%) Capybara::RackTest::Driver#visit + code: + | 164 | def warm_asset_cache + | 165 | return if warm_asset_cache? + | 166 | return unless defined?(Capybara) + | 167 | + 6288 (36.9%) | 168 | Capybara.current_session.driver.visit '/' + | 169 | end +$ stackprof tmp/project_policy_spec.rb.dump --method BasePolicy#abilities +BasePolicy#abilities (/Users/lupine/dev/gitlab.com/gitlab-org/gitlab-development-kit/gitlab/app/policies/base_policy.rb:79) + samples: 0 self (0.0%) / 50 total (0.3%) + callers: + 25 ( 50.0%) BasePolicy.abilities + 25 ( 50.0%) BasePolicy#collect_rules + callees (50 total): + 25 ( 50.0%) ProjectPolicy#rules + 25 ( 50.0%) BasePolicy#collect_rules + code: + | 79 | def abilities + | 80 | return RuleSet.empty if @user && @user.blocked? + | 81 | return anonymous_abilities if @user.nil? + 50 (0.3%) | 82 | collect_rules { rules } + | 83 | end +``` + +Since the profile includes the work done by the test suite as well as the +application code, these profiles can be used to investigate slow tests as well. +However, for smaller runs (like this example), this means that the cost of +setting up the test suite will tend to dominate. + +It's also possible to modify the application code in-place to output profiles +whenever a particular code path is triggered without going through the test +suite first. See the +[StackProf documentation](https://github.com/tmm1/stackprof/blob/master/README.md) +for details. + ## Importance of Changes When working on performance improvements, it's important to always ask yourself -- cgit v1.2.1 From ddf288f0d0d6bed07303b8e34288c41114f3a5ab Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" <lbennett@gitlab.com> Date: Sat, 26 Nov 2016 13:55:47 +0000 Subject: Moved groups above projects --- .../projects/_zero_authorized_projects.html.haml | 23 +++++++++++----------- ...ld-be-below-new-group-on-the-welcome-screen.yml | 4 ++++ 2 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 changelogs/unreleased/24135-new-project-should-be-below-new-group-on-the-welcome-screen.yml diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml index fdea834ff45..4a55aac0df6 100644 --- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml +++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml @@ -4,6 +4,18 @@ Welcome to GitLab %p.blank-state-text Code, test, and deploy together + +- if current_user.can_create_group? + .blank-state + .blank-state-icon + = custom_icon("group", size: 50) + %h3.blank-state-title + You can create a group for several dependent projects. + %p.blank-state-text + Groups are the best way to manage projects and members. + = link_to new_group_path, class: "btn btn-new" do + New group + .blank-state .blank-state-icon = custom_icon("project", size: 50) @@ -21,17 +33,6 @@ = link_to new_project_path, class: "btn btn-new" do New project -- if current_user.can_create_group? - .blank-state - .blank-state-icon - = custom_icon("group", size: 50) - %h3.blank-state-title - You can create a group for several dependent projects. - %p.blank-state-text - Groups are the best way to manage projects and members. - = link_to new_group_path, class: "btn btn-new" do - New group - -if publicish_project_count > 0 .blank-state .blank-state-icon diff --git a/changelogs/unreleased/24135-new-project-should-be-below-new-group-on-the-welcome-screen.yml b/changelogs/unreleased/24135-new-project-should-be-below-new-group-on-the-welcome-screen.yml new file mode 100644 index 00000000000..855e4e1ba1d --- /dev/null +++ b/changelogs/unreleased/24135-new-project-should-be-below-new-group-on-the-welcome-screen.yml @@ -0,0 +1,4 @@ +--- +title: Moved new projects button below new group button on the welcome screen +merge_request: 7770 +author: -- cgit v1.2.1 From 30c3eed89b023f84d9f1fdd8ddb6af4c7e7a05eb Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra <dimitriehoekstra@gmail.com> Date: Sun, 27 Nov 2016 23:49:59 +0100 Subject: Adds hoverstates for collapsed Issue/Merge Request sidebar --- app/assets/stylesheets/pages/issuable.scss | 16 +++++++++++----- ...rstates-for-collapsed-issue-merge-request-sidebar.yml | 4 ++++ 2 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 changelogs/unreleased/25011-hoverstates-for-collapsed-issue-merge-request-sidebar.yml diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 773155fe80a..7aad99eee4e 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -132,7 +132,7 @@ display: none; } - .btn-clipboard { + .btn-clipboard:hover { color: $gl-gray; } } @@ -235,6 +235,10 @@ padding-bottom: 10px; color: #999; + &:hover { + color: $gl-gray; + } + span { display: block; margin-top: 0; @@ -244,15 +248,17 @@ display: none; } + .avatar:hover { + border-color: #999; + } + .btn-clipboard { border: none; + color: #999; &:hover { background: transparent; - } - - i { - color: #999; + color: $gl-gray; } } } diff --git a/changelogs/unreleased/25011-hoverstates-for-collapsed-issue-merge-request-sidebar.yml b/changelogs/unreleased/25011-hoverstates-for-collapsed-issue-merge-request-sidebar.yml new file mode 100644 index 00000000000..2c3ba1dfe44 --- /dev/null +++ b/changelogs/unreleased/25011-hoverstates-for-collapsed-issue-merge-request-sidebar.yml @@ -0,0 +1,4 @@ +--- +title: Adds hoverstates for collapsed Issue/Merge Request sidebar +merge_request: !7777 +author: -- cgit v1.2.1 From 46859cb984523affdcbc5ba9697883b290c1bf0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Tue, 29 Nov 2016 15:56:50 +0100 Subject: Fix a transient spec failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sorting by created_at can lead to uncertainties if two records are created at the same time. Signed-off-by: Rémy Coutable <remy@rymai.me> --- spec/requests/projects/cycle_analytics_events_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb index 705dbb7d1c0..5c90fd9bad9 100644 --- a/spec/requests/projects/cycle_analytics_events_spec.rb +++ b/spec/requests/projects/cycle_analytics_events_spec.rb @@ -40,7 +40,7 @@ describe 'cycle analytics events' do expect(json_response['events']).not_to be_empty - first_mr_iid = MergeRequest.order(created_at: :desc).pluck(:iid).first.to_s + first_mr_iid = project.merge_requests.order(id: :desc).pluck(:iid).first.to_s expect(json_response['events'].first['iid']).to eq(first_mr_iid) end -- cgit v1.2.1 From c145413d1acb58addb199f0e5bfd909e0a695a5c Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" <lbennett@gitlab.com> Date: Mon, 14 Nov 2016 22:37:13 +0000 Subject: Remove JSX/React eslint plugins. Change airbnb eslint config package to `eslint-config-airbnb-base` and update plugins. Change `airbnb` to `airbnb-base` for .eslintrc `extends` value. Added changelog entry Made sure all plugins and envs are set Corrected new failing specs --- .eslintignore | 1 + .eslintrc | 7 +++---- app/assets/javascripts/labels_select.js | 2 +- app/assets/javascripts/notes.js | 2 +- .../unreleased/remove-jsx-react-eslint-plugins.yml | 5 +++++ package.json | 10 ++++------ spec/javascripts/build_spec.js.es6 | 2 +- .../environments/environment_actions_spec.js.es6 | 4 ++-- .../environments/environment_item_spec.js.es6 | 20 ++++++++++---------- .../environments/environments_store_spec.js.es6 | 8 ++++---- spec/javascripts/smart_interval_spec.js.es6 | 2 +- .../vue_common_components/commit_spec.js.es6 | 12 ++++++------ 12 files changed, 39 insertions(+), 36 deletions(-) create mode 100644 changelogs/unreleased/remove-jsx-react-eslint-plugins.yml diff --git a/.eslintignore b/.eslintignore index d9c2233c9d7..93de4b10dfe 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ +/coverage/ /coverage-javascript/ /public/ /tmp/ diff --git a/.eslintrc b/.eslintrc index 788a88487d8..b80dcec9d1d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,15 +1,14 @@ { "env": { + "jquery": true, "browser": true, "es6": true }, - "extends": "airbnb", + "extends": "airbnb-base", "globals": { - "$": false, "_": false, "gl": false, - "gon": false, - "jQuery": false + "gon": false }, "plugins": [ "filenames" diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 812d5cde685..f334f35594d 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, no-undef, semi, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread, padded-blocks, max-len */ +/* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, no-undef, semi, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread, padded-blocks, max-len */ (function() { this.LabelsSelect = (function() { function LabelsSelect() { diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 47e7b6f831b..0ca0e255595 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, no-undef, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, semi, indent, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape, radix, padded-blocks, max-len */ +/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, no-undef, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, semi, indent, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape, radix, padded-blocks, max-len */ /*= require autosave */ /*= require autosize */ diff --git a/changelogs/unreleased/remove-jsx-react-eslint-plugins.yml b/changelogs/unreleased/remove-jsx-react-eslint-plugins.yml new file mode 100644 index 00000000000..6e02998b3a8 --- /dev/null +++ b/changelogs/unreleased/remove-jsx-react-eslint-plugins.yml @@ -0,0 +1,5 @@ +--- +title: Changed eslint airbnb config to the base airbnb config and corrected eslintrc + plugins and envs +merge_request: 7470 +author: Luke "Jared" Bennett diff --git a/package.json b/package.json index 350e4cd80c9..961989f8012 100644 --- a/package.json +++ b/package.json @@ -6,13 +6,11 @@ "eslint-report": "npm run eslint -- --format html --output-file ./eslint-report.html" }, "devDependencies": { - "eslint": "^3.1.1", - "eslint-config-airbnb": "^12.0.0", + "eslint": "^3.10.1", + "eslint-config-airbnb-base": "^10.0.1", "eslint-plugin-filenames": "^1.1.0", - "eslint-plugin-import": "^1.16.0", - "eslint-plugin-jasmine": "^1.8.1", - "eslint-plugin-jsx-a11y": "^2.2.3", - "eslint-plugin-react": "^6.4.1", + "eslint-plugin-import": "^2.2.0", + "eslint-plugin-jasmine": "^2.1.0", "istanbul": "^0.4.5" } } diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6 index d694727880f..3983cad4c13 100644 --- a/spec/javascripts/build_spec.js.es6 +++ b/spec/javascripts/build_spec.js.es6 @@ -109,7 +109,7 @@ describe('Build', () => { expect($.ajax.calls.count()).toBe(2); let [{ url, dataType, success, context }] = $.ajax.calls.argsFor(1); expect(url).toBe( - `${BUILD_URL}/trace.json?state=${encodeURIComponent(INITIAL_BUILD_TRACE_STATE)}` + `${BUILD_URL}/trace.json?state=${encodeURIComponent(INITIAL_BUILD_TRACE_STATE)}`, ); expect(dataType).toBe('json'); expect(success).toEqual(jasmine.any(Function)); diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6 index c9ac7a73fd0..76e81233e89 100644 --- a/spec/javascripts/environments/environment_actions_spec.js.es6 +++ b/spec/javascripts/environments/environment_actions_spec.js.es6 @@ -28,10 +28,10 @@ describe('Actions Component', () => { }); expect( - component.$el.querySelectorAll('.dropdown-menu li').length + component.$el.querySelectorAll('.dropdown-menu li').length, ).toEqual(actionsMock.length); expect( - component.$el.querySelector('.dropdown-menu li a').getAttribute('href') + component.$el.querySelector('.dropdown-menu li a').getAttribute('href'), ).toEqual(actionsMock[0].play_path); }); }); diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6 index 3c15e3b7719..14e90a9dd1b 100644 --- a/spec/javascripts/environments/environment_item_spec.js.es6 +++ b/spec/javascripts/environments/environment_item_spec.js.es6 @@ -141,18 +141,18 @@ describe('Environment item', () => { describe('With deployment', () => { it('should render deployment internal id', () => { expect( - component.$el.querySelector('.deployment-column span').textContent + component.$el.querySelector('.deployment-column span').textContent, ).toContain(environment.last_deployment.iid); expect( - component.$el.querySelector('.deployment-column span').textContent + component.$el.querySelector('.deployment-column span').textContent, ).toContain('#'); }); describe('With user information', () => { it('should render user avatar with link to profile', () => { expect( - component.$el.querySelector('.js-deploy-user-container').getAttribute('href') + component.$el.querySelector('.js-deploy-user-container').getAttribute('href'), ).toEqual(environment.last_deployment.user.web_url); }); }); @@ -160,13 +160,13 @@ describe('Environment item', () => { describe('With build url', () => { it('Should link to build url provided', () => { expect( - component.$el.querySelector('.build-link').getAttribute('href') + component.$el.querySelector('.build-link').getAttribute('href'), ).toEqual(environment.last_deployment.deployable.build_path); }); it('Should render deployable name and id', () => { expect( - component.$el.querySelector('.build-link').getAttribute('href') + component.$el.querySelector('.build-link').getAttribute('href'), ).toEqual(environment.last_deployment.deployable.build_path); }); }); @@ -174,7 +174,7 @@ describe('Environment item', () => { describe('With commit information', () => { it('should render commit component', () => { expect( - component.$el.querySelector('.js-commit-component') + component.$el.querySelector('.js-commit-component'), ).toBeDefined(); }); }); @@ -183,7 +183,7 @@ describe('Environment item', () => { describe('With manual actions', () => { it('Should render actions component', () => { expect( - component.$el.querySelector('.js-manual-actions-container') + component.$el.querySelector('.js-manual-actions-container'), ).toBeDefined(); }); }); @@ -191,7 +191,7 @@ describe('Environment item', () => { describe('With external URL', () => { it('should render external url component', () => { expect( - component.$el.querySelector('.js-external-url-container') + component.$el.querySelector('.js-external-url-container'), ).toBeDefined(); }); }); @@ -199,7 +199,7 @@ describe('Environment item', () => { describe('With stop action', () => { it('Should render stop action component', () => { expect( - component.$el.querySelector('.js-stop-component-container') + component.$el.querySelector('.js-stop-component-container'), ).toBeDefined(); }); }); @@ -207,7 +207,7 @@ describe('Environment item', () => { describe('With retry action', () => { it('Should render rollback component', () => { expect( - component.$el.querySelector('.js-rollback-component-container') + component.$el.querySelector('.js-rollback-component-container'), ).toBeDefined(); }); }); diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6 index 9b0b3cb1c65..17c00acf63e 100644 --- a/spec/javascripts/environments/environments_store_spec.js.es6 +++ b/spec/javascripts/environments/environments_store_spec.js.es6 @@ -5,11 +5,11 @@ //= require ./mock_data (() => { - beforeEach(() => { - gl.environmentsList.EnvironmentsStore.create(); - }); - describe('Store', () => { + beforeEach(() => { + gl.environmentsList.EnvironmentsStore.create(); + }); + it('should start with a blank state', () => { expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(0); expect(gl.environmentsList.EnvironmentsStore.state.stoppedCounter).toBe(0); diff --git a/spec/javascripts/smart_interval_spec.js.es6 b/spec/javascripts/smart_interval_spec.js.es6 index 651d1f0f975..ed6166a25a8 100644 --- a/spec/javascripts/smart_interval_spec.js.es6 +++ b/spec/javascripts/smart_interval_spec.js.es6 @@ -37,7 +37,7 @@ const intervalConfig = this.smartInterval.cfg; const iterationCount = 4; const maxIntervalAfterIterations = intervalConfig.startingInterval * - Math.pow(intervalConfig.incrementByFactorOf, (iterationCount - 1)); // 40 + (intervalConfig.incrementByFactorOf ** (iterationCount - 1)); // 40 const currentInterval = interval.getCurrentInterval(); // Provide some flexibility for performance of testing environment diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_common_components/commit_spec.js.es6 index 0e3b82967c1..b1dbc8bd5fa 100644 --- a/spec/javascripts/vue_common_components/commit_spec.js.es6 +++ b/spec/javascripts/vue_common_components/commit_spec.js.es6 @@ -74,26 +74,26 @@ describe('Commit component', () => { describe('Given commit title and author props', () => { it('Should render a link to the author profile', () => { expect( - component.$el.querySelector('.commit-title .avatar-image-container').getAttribute('href') + component.$el.querySelector('.commit-title .avatar-image-container').getAttribute('href'), ).toEqual(props.author.web_url); }); it('Should render the author avatar with title and alt attributes', () => { expect( - component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('title') + component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('title'), ).toContain(props.author.username); expect( - component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('alt') + component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('alt'), ).toContain(`${props.author.username}'s avatar`); }); }); it('should render the commit title', () => { expect( - component.$el.querySelector('a.commit-row-message').getAttribute('href') + component.$el.querySelector('a.commit-row-message').getAttribute('href'), ).toEqual(props.commit_url); expect( - component.$el.querySelector('a.commit-row-message').textContent + component.$el.querySelector('a.commit-row-message').textContent, ).toContain(props.title); }); }); @@ -119,7 +119,7 @@ describe('Commit component', () => { }); expect( - component.$el.querySelector('.commit-title span').textContent + component.$el.querySelector('.commit-title span').textContent, ).toContain('Cant find HEAD commit for this branch'); }); }); -- cgit v1.2.1 From f08385142d427dc73c2a7e668652e2a5d9c7ea88 Mon Sep 17 00:00:00 2001 From: tauriedavis <taurie@gitlab.com> Date: Tue, 29 Nov 2016 10:00:02 -0800 Subject: Add blue back to sub nav active --- app/assets/stylesheets/framework/nav.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index a2787ede53c..1839ffa0976 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -94,6 +94,7 @@ &.active a { border-bottom: none; + color: $link-underline-blue; } a { @@ -103,7 +104,6 @@ &:hover, &:active, &:focus { - color: $black; border-bottom: none; } } -- cgit v1.2.1 From 530d222815b62180145d3cbe89cd3602835de8ad Mon Sep 17 00:00:00 2001 From: Ryan Harris <harrisryan1@gmail.com> Date: Tue, 29 Nov 2016 13:39:57 -0500 Subject: Edit /spec/features/profiles/preferences_spec.rb to match changes in 084d90ac --- spec/features/profiles/preferences_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb index d14a1158b67..a6b841c0210 100644 --- a/spec/features/profiles/preferences_spec.rb +++ b/spec/features/profiles/preferences_spec.rb @@ -73,7 +73,7 @@ describe 'Profile > Preferences', feature: true do expect(page.current_path).to eq starred_dashboard_projects_path end - click_link 'Your Projects' + click_link 'Your projects' expect(page).not_to have_content("You don't have starred projects yet") expect(page.current_path).to eq dashboard_projects_path -- cgit v1.2.1 From 500c0d5e419ecc39e823bdbfd49e4c5842621fa2 Mon Sep 17 00:00:00 2001 From: Luis Alonso Chavez Armendariz <lchavez@nearsoft.com> Date: Tue, 29 Nov 2016 11:44:07 -0700 Subject: Fix appearance in error pages --- app/views/errors/access_denied.html.haml | 16 +++++--- app/views/errors/encoding.html.haml | 14 ++++--- app/views/errors/git_not_found.html.haml | 17 ++++---- app/views/errors/not_found.html.haml | 14 ++++--- app/views/errors/omniauth_error.html.haml | 16 +++++--- app/views/layouts/errors.html.haml | 65 +++++++++++++++++++++++++++---- changelogs/unreleased/issue_24363.yml | 4 ++ public/404.html | 5 ++- public/422.html | 5 ++- public/500.html | 5 ++- public/502.html | 5 ++- public/503.html | 5 ++- 12 files changed, 129 insertions(+), 42 deletions(-) create mode 100644 changelogs/unreleased/issue_24363.yml diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml index c034bbe430e..8bddbef3562 100644 --- a/app/views/errors/access_denied.html.haml +++ b/app/views/errors/access_denied.html.haml @@ -1,6 +1,10 @@ -- page_title "Access Denied" -%h1 403 -%h3 Access Denied -%hr -%p You are not allowed to access this page. -%p Read more about project permissions #{link_to "here", help_page_path("user/permissions"), class: "vlink"} +- content_for(:title, 'Access Denied') +%img{:alt => "GitLab Logo", + :src => image_path('logo.svg')} + %h1 + 403 +.container + %h3 Access Denied + %hr + %p You are not allowed to access this page. + %p Read more about project permissions #{link_to "here", help_page_path("user/permissions"), class: "vlink"} diff --git a/app/views/errors/encoding.html.haml b/app/views/errors/encoding.html.haml index 90cfbebfcc6..064ff14ad2c 100644 --- a/app/views/errors/encoding.html.haml +++ b/app/views/errors/encoding.html.haml @@ -1,5 +1,9 @@ -- page_title "Encoding Error" -%h1 500 -%h3 Encoding Error -%hr -%p Page can't be loaded because of an encoding error. +- content_for(:title, 'Encoding Error') +%img{:alt => "GitLab Logo", + :src => image_path('logo.svg')} + %h1 + 500 +.container + %h3 Encoding Error + %hr + %p Page can't be loaded because of an encoding error. diff --git a/app/views/errors/git_not_found.html.haml b/app/views/errors/git_not_found.html.haml index ff5d4cc1506..c5c12a410ac 100644 --- a/app/views/errors/git_not_found.html.haml +++ b/app/views/errors/git_not_found.html.haml @@ -1,7 +1,10 @@ -- page_title "Git Resource Not Found" -%h1 404 -%h3 Git Resource Not found -%hr -%p - Application can't get access to some branch or commit in your repository. It - may have been moved. +- content_for(:title, 'Git Resource Not Found') +%img{:alt => "GitLab Logo", + :src => image_path('logo.svg')} + %h1 + 404 +.container + %h3 Git Resource Not found + %hr + %p Application can't get access to some branch or commit in your repository. It + may have been moved diff --git a/app/views/errors/not_found.html.haml b/app/views/errors/not_found.html.haml index 3756b98ebb2..50a54a93cb5 100644 --- a/app/views/errors/not_found.html.haml +++ b/app/views/errors/not_found.html.haml @@ -1,5 +1,9 @@ -- page_title "Not Found" -%h1 404 -%h3 The resource you were looking for doesn't exist. -%hr -%p You may have mistyped the address or the page may have moved. +- content_for(:title, 'Not Found') +%img{:alt => "GitLab Logo", + :src => image_path('logo.svg')} + %h1 + 404 +.container + %h3 The resource you were looking for doesn't exist. + %hr + %p You may have mistyped the address or the page may have moved. diff --git a/app/views/errors/omniauth_error.html.haml b/app/views/errors/omniauth_error.html.haml index 3e70e98a24c..d91f1878cb6 100644 --- a/app/views/errors/omniauth_error.html.haml +++ b/app/views/errors/omniauth_error.html.haml @@ -1,9 +1,13 @@ -- page_title "Auth Error" -%h1 422 -%h3 Sign-in using #{@provider} auth failed -%hr -%p Sign-in failed because #{@error}. -%p There are couple of steps you can take: +- content_for(:title, 'Auth Error') +%img{:alt => "GitLab Logo", + :src => image_path('logo.svg')} + %h1 + 422 +.container + %h3 Sign-in using #{@provider} auth failed + %hr + %p Sign-in failed because #{@error}. + %p There are couple of steps you can take: %ul %li Try logging in using your email diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml index 7fbe065df00..a3b925f6afd 100644 --- a/app/views/layouts/errors.html.haml +++ b/app/views/layouts/errors.html.haml @@ -1,10 +1,59 @@ !!! 5 %html{ lang: "en"} - = render "layouts/head" - %body{class: "#{user_application_theme} application navless"} - = Gon::Base.render_data - = render "layouts/header/empty" - .container.navless-container - = render "layouts/flash" - .error-page - = yield + %head + %meta{:content => "width=device-width, initial-scale=1, maximum-scale=1", :name => "viewport"} + %title= yield(:title) + :css + body { + color: #666; + text-align: center; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + margin: auto; + font-size: 14px; + } + + h1 { + font-size: 56px; + line-height: 100px; + font-weight: normal; + color: #456; + } + + h2 { + font-size: 24px; + color: #666; + line-height: 1.5em; + } + + h3 { + color: #456; + font-size: 20px; + font-weight: normal; + line-height: 28px; + } + + hr { + max-width: 800px; + margin: 18px auto; + border: 0; + border-top: 1px solid #EEE; + border-bottom: 1px solid white; + } + + img { + max-width: 40vw; + display: block; + margin: 40px auto; + } + + .container { + margin: auto 20px; + } + + ul { + margin: auto; + text-align: left; + display:inline-block; + } +%body + = yield diff --git a/changelogs/unreleased/issue_24363.yml b/changelogs/unreleased/issue_24363.yml new file mode 100644 index 00000000000..0298890b477 --- /dev/null +++ b/changelogs/unreleased/issue_24363.yml @@ -0,0 +1,4 @@ +--- +title: Fix appearance in error pages +merge_request: +author: Luis Alonso Chavez Armendariz diff --git a/public/404.html b/public/404.html index 92b7f4da0b9..11b29d09a82 100644 --- a/public/404.html +++ b/public/404.html @@ -42,6 +42,8 @@ img { max-width: 40vw; + display: block; + margin: 40px auto; } .container { @@ -51,8 +53,9 @@ </head> <body> + <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" + alt="GitLab Logo" /> <h1> - <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo" /><br /> 404 </h1> <div class="container"> diff --git a/public/422.html b/public/422.html index f625f8a33b7..9bd7cb4b7c8 100644 --- a/public/422.html +++ b/public/422.html @@ -42,6 +42,8 @@ img { max-width: 40vw; + display: block; + margin: 40px auto; } .container { @@ -51,8 +53,9 @@ </head> <body> + <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" + alt="GitLab Logo" /> <h1> - <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo" /><br /> 422 </h1> <div class="container"> diff --git a/public/500.html b/public/500.html index d76c66ba92a..f92e8839f8d 100644 --- a/public/500.html +++ b/public/500.html @@ -42,6 +42,8 @@ img { max-width: 40vw; + display: block; + margin: 40px auto; } .container { @@ -51,8 +53,9 @@ </head> <body> + <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" + alt="GitLab Logo" /> <h1> - <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo" /><br /> 500 </h1> <div class="container"> diff --git a/public/502.html b/public/502.html index 1a3c7efc769..c2be4f130a9 100644 --- a/public/502.html +++ b/public/502.html @@ -42,6 +42,8 @@ img { max-width: 40vw; + display: block; + margin: 40px auto; } .container { @@ -51,8 +53,9 @@ </head> <body> + <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" + alt="GitLab Logo" /> <h1> - <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo" /><br /> 502 </h1> <div class="container"> diff --git a/public/503.html b/public/503.html index c1c4e3ffdb8..8850ffce362 100644 --- a/public/503.html +++ b/public/503.html @@ -42,6 +42,8 @@ img { max-width: 40vw; + display: block; + margin: 40px auto; } .container { @@ -51,8 +53,9 @@ </head> <body> + <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" + alt="GitLab Logo" /> <h1> - <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo" /><br /> 503 </h1> <div class="container"> -- cgit v1.2.1 From 086da9214d2bde1f652f197b0e130f60eef12c2e Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Mon, 28 Nov 2016 20:15:18 -0600 Subject: fetch local parameters in _generic_commit_status.html.haml similar to how _build.html.haml handles them --- .../_generic_commit_status.html.haml | 24 ++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index 0b99e9f8756..ceaab8c7d45 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -1,3 +1,11 @@ +- ref = local_assigns.fetch(:ref, nil) +- commit_sha = local_assigns.fetch(:commit_sha, nil) +- retried = local_assigns.fetch(:retried, false) +- pipeline_link = local_assigns.fetch(:pipeline_link, false) +- stage = local_assigns.fetch(:stage, false) +- coverage = local_assigns.fetch(:coverage, false) +- runner = local_assigns.fetch(:runner, false) + %tr.generic_commit_status %td.status - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url @@ -12,10 +20,10 @@ - else %strong ##{generic_commit_status.id} - - if defined?(retried) && retried + - if retried = icon('warning', class: 'text-warning has-tooltip', title: 'Status was retried.') - - if defined?(pipeline_link) && pipeline_link + - if pipeline_link %td = link_to pipeline_path(generic_commit_status.pipeline) do %span.pipeline-id ##{generic_commit_status.pipeline.id} @@ -25,25 +33,25 @@ - else %span.monospace API - - if defined?(commit_sha) && commit_sha + - if commit_sha %td = link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "monospace" - - if defined?(ref) && ref + - if ref %td - if generic_commit_status.ref = link_to generic_commit_status.ref, namespace_project_commits_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.ref) - else .light none - - if defined?(runner) && runner + - if runner %td - if generic_commit_status.try(:runner) = runner_link(generic_commit_status.runner) - else .light none - - if defined?(stage) && stage + - if stage %td = generic_commit_status.stage @@ -55,7 +63,7 @@ - generic_commit_status.tags.each do |tag| %span.label.label-primary = tag - - if defined?(retried) && retried + - if retried %span.label.label-warning retried %td.duration @@ -68,7 +76,7 @@ = icon("calendar") %span #{time_ago_with_tooltip(generic_commit_status.finished_at)} - - if defined?(coverage) && coverage + - if coverage %td.coverage - if generic_commit_status.try(:coverage) #{generic_commit_status.coverage}% -- cgit v1.2.1 From 3e0651c7cd0c8dc402dc45387e08ae04731ab46c Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Mon, 28 Nov 2016 20:59:48 -0600 Subject: reorder generic commit status columns to match build status partial --- .../_generic_commit_status.html.haml | 51 ++++++++++++---------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index ceaab8c7d45..d9b34ba3c43 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -1,12 +1,12 @@ +- admin = local_assigns.fetch(:admin, false) - ref = local_assigns.fetch(:ref, nil) - commit_sha = local_assigns.fetch(:commit_sha, nil) - retried = local_assigns.fetch(:retried, false) - pipeline_link = local_assigns.fetch(:pipeline_link, false) - stage = local_assigns.fetch(:stage, false) - coverage = local_assigns.fetch(:coverage, false) -- runner = local_assigns.fetch(:runner, false) -%tr.generic_commit_status +%tr.generic_commit_status{class: ('retried' if retried)} %td.status - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url = ci_status_with_icon(generic_commit_status.status, generic_commit_status.target_url) @@ -16,13 +16,34 @@ %td.generic_commit_status-link - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url = link_to generic_commit_status.target_url do - %strong ##{generic_commit_status.id} + %span.build-link ##{generic_commit_status.id} - else - %strong ##{generic_commit_status.id} + %span.build-link ##{generic_commit_status.id} + + - if ref + - if generic_commit_status.ref + .icon-container + = generic_commit_status.tags.any? ? icon('tag') : icon('code-fork') + = link_to generic_commit_status.ref, namespace_project_commits_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.ref) + - else + .light none + .icon-container.commit-icon + = custom_icon("icon_commit") + + - if commit_sha + = link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "commit-id monospace" - if retried = icon('warning', class: 'text-warning has-tooltip', title: 'Status was retried.') + .label-container + - if generic_commit_status.tags.any? + - generic_commit_status.tags.each do |tag| + %span.label.label-primary + = tag + - if retried + %span.label.label-warning retried + - if pipeline_link %td = link_to pipeline_path(generic_commit_status.pipeline) do @@ -33,18 +54,12 @@ - else %span.monospace API - - if commit_sha - %td - = link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "monospace" - - - if ref + - if admin %td - - if generic_commit_status.ref - = link_to generic_commit_status.ref, namespace_project_commits_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.ref) - - else - .light none + - if generic_commit_status.project + = link_to generic_commit_status.project.name_with_namespace, admin_namespace_project_path(generic_commit_status.project.namespace, generic_commit_status.project) - - if runner + - if admin %td - if generic_commit_status.try(:runner) = runner_link(generic_commit_status.runner) @@ -58,14 +73,6 @@ %td = generic_commit_status.name - %td - - if generic_commit_status.tags.any? - - generic_commit_status.tags.each do |tag| - %span.label.label-primary - = tag - - if retried - %span.label.label-warning retried - %td.duration - if generic_commit_status.duration = icon("clock-o") -- cgit v1.2.1 From a2ea78a09d689d2f53c22859f8304e6c1288bf17 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Mon, 28 Nov 2016 21:04:50 -0600 Subject: reformat build duration and finish time to match /ci/builds/_build.html.haml --- .../_generic_commit_status.html.haml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index d9b34ba3c43..4fff082202d 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -73,17 +73,21 @@ %td = generic_commit_status.name - %td.duration + %td - if generic_commit_status.duration - = icon("clock-o") - = time_interval_in_words(generic_commit_status.duration) + %p.duration + = custom_icon("icon_timer") + = duration_in_numbers(generic_commit_status.duration) - %td.timestamp - if generic_commit_status.finished_at - = icon("calendar") - %span #{time_ago_with_tooltip(generic_commit_status.finished_at)} + %p.finished-at + = icon("calendar") + %span #{time_ago_with_tooltip(generic_commit_status.finished_at)} - - if coverage - %td.coverage + %td.coverage + - if coverage - if generic_commit_status.try(:coverage) #{generic_commit_status.coverage}% + + %td + -# empty column to match number of columns in ci/builds/_build.html.haml -- cgit v1.2.1 From 093dd5cef8d5130101218ce258aceb92a9d60a6a Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Mon, 28 Nov 2016 21:08:02 -0600 Subject: remove redundant if statement --- app/views/projects/ci/builds/_build.html.haml | 2 -- .../projects/generic_commit_statuses/_generic_commit_status.html.haml | 2 -- 2 files changed, 4 deletions(-) diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 8d9c15d0dc6..33b4e9329a2 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -66,8 +66,6 @@ %td - if build.project = link_to build.project.name_with_namespace, admin_namespace_project_path(build.project.namespace, build.project) - - - if admin %td - if build.try(:runner) = runner_link(build.runner) diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index 4fff082202d..131883b8106 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -58,8 +58,6 @@ %td - if generic_commit_status.project = link_to generic_commit_status.project.name_with_namespace, admin_namespace_project_path(generic_commit_status.project.namespace, generic_commit_status.project) - - - if admin %td - if generic_commit_status.try(:runner) = runner_link(generic_commit_status.runner) -- cgit v1.2.1 From b7deda9405e3c7355bd470b4288b636e95bcb5b2 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Mon, 28 Nov 2016 21:08:29 -0600 Subject: collapse nested if statement --- app/views/projects/ci/builds/_build.html.haml | 5 ++--- .../generic_commit_statuses/_generic_commit_status.html.haml | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 33b4e9329a2..e75547c815f 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -91,9 +91,8 @@ %span #{time_ago_with_tooltip(build.finished_at)} %td.coverage - - if coverage - - if build.try(:coverage) - #{build.coverage}% + - if coverage && build.try(:coverage) + #{build.coverage}% %td .pull-right diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index 131883b8106..7f751d9ae2e 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -83,9 +83,8 @@ %span #{time_ago_with_tooltip(generic_commit_status.finished_at)} %td.coverage - - if coverage - - if generic_commit_status.try(:coverage) - #{generic_commit_status.coverage}% + - if coverage && generic_commit_status.try(:coverage) + #{generic_commit_status.coverage}% %td -# empty column to match number of columns in ci/builds/_build.html.haml -- cgit v1.2.1 From 9c6eaf8009ea4cb5a4b33610aad32a2da30f3d09 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Mon, 28 Nov 2016 21:29:12 -0600 Subject: add CHANGELOG.md entry for !7811 --- changelogs/unreleased/24710-fix-generic-commit-status-table-row.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/24710-fix-generic-commit-status-table-row.yml diff --git a/changelogs/unreleased/24710-fix-generic-commit-status-table-row.yml b/changelogs/unreleased/24710-fix-generic-commit-status-table-row.yml new file mode 100644 index 00000000000..07cb53d5278 --- /dev/null +++ b/changelogs/unreleased/24710-fix-generic-commit-status-table-row.yml @@ -0,0 +1,4 @@ +--- +title: Update generic/external build status to match normal build status template +merge_request: 7811 +author: -- cgit v1.2.1 From 9ed87e5c365e42256111b11d65140e03668b2cc0 Mon Sep 17 00:00:00 2001 From: tauriedavis <taurie@gitlab.com> Date: Mon, 28 Nov 2016 11:53:41 -0800 Subject: 25044 Make md header tabs match nav tabs --- app/assets/stylesheets/framework/markdown_area.scss | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 42087c91530..4bd7ff8fefd 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -56,16 +56,9 @@ .md-header { .nav-links { - .active { - a { - border-bottom-color: #000; - } - } - a { padding-top: 0; line-height: 19px; - border-bottom: 1px solid $border-color; &.btn.btn-xs { padding: 2px 5px; -- cgit v1.2.1 From 09826be231aba3471766378007fc1e748c05c154 Mon Sep 17 00:00:00 2001 From: Alex Jordan <alex@strugee.net> Date: Tue, 29 Nov 2016 21:05:30 +0000 Subject: Rewrite an HTTP link to use HTTPS --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c9bb5af6da2..61204630fd2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # GitLab [![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master) -[![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](http://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby) +[![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby) [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42) -- cgit v1.2.1 From f5e8337c7bb7e218303a713440e31f44a66471d7 Mon Sep 17 00:00:00 2001 From: Semyon Pupkov <mail@semyonpupkov.com> Date: Tue, 29 Nov 2016 14:49:43 +0500 Subject: Do not raise error in AutocompleteController#users when not authorized https://gitlab.com/gitlab-org/gitlab-ce/issues/25031 --- app/controllers/autocomplete_controller.rb | 2 +- .../unreleased/25031-do-not-raise-error-in-autocomplete.yml | 4 ++++ spec/controllers/autocomplete_controller_spec.rb | 9 +++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/25031-do-not-raise-error-in-autocomplete.yml diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 5c44637fdee..5f13353baa1 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -11,7 +11,7 @@ class AutocompleteController < ApplicationController @users = @users.reorder(:name) @users = @users.page(params[:page]) - if params[:todo_filter].present? + if params[:todo_filter].present? && current_user @users = @users.todo_authors(current_user.id, params[:todo_state_filter]) end diff --git a/changelogs/unreleased/25031-do-not-raise-error-in-autocomplete.yml b/changelogs/unreleased/25031-do-not-raise-error-in-autocomplete.yml new file mode 100644 index 00000000000..862de7c5db1 --- /dev/null +++ b/changelogs/unreleased/25031-do-not-raise-error-in-autocomplete.yml @@ -0,0 +1,4 @@ +--- +title: Do not raise error in AutocompleteController#users when not authorized +merge_request: 7817 +author: Semyon Pupkov diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index 0d1545040f1..ea2fd90a9b0 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -144,6 +144,15 @@ describe AutocompleteController do it { expect(body).to be_kind_of(Array) } it { expect(body.size).to eq 0 } end + + describe 'GET #users with todo filter' do + it 'gives an array of users' do + get :users, todo_filter: true + + expect(response.status).to eq 200 + expect(body).to be_kind_of(Array) + end + end end context 'author of issuable included' do -- cgit v1.2.1 From f7351b040b0b0737372d18201ddc955942e0e016 Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Tue, 29 Nov 2016 15:08:11 +0800 Subject: Speed up Group security access specs This is the Group equivalent of 13ad9a745a392e0bf0cedd0e1f318c1acee9b969 --- .../security/group/internal_access_spec.rb | 123 +++++++++------------ .../features/security/group/private_access_spec.rb | 123 +++++++++------------ spec/features/security/group/public_access_spec.rb | 123 +++++++++------------ spec/support/matchers/access_matchers.rb | 31 +++--- 4 files changed, 181 insertions(+), 219 deletions(-) diff --git a/spec/features/security/group/internal_access_spec.rb b/spec/features/security/group/internal_access_spec.rb index 35fcef7a712..87cce32d6c6 100644 --- a/spec/features/security/group/internal_access_spec.rb +++ b/spec/features/security/group/internal_access_spec.rb @@ -3,25 +3,12 @@ require 'rails_helper' describe 'Internal Group access', feature: true do include AccessMatchers - let(:group) { create(:group, :internal) } + let(:group) { create(:group, :internal) } let(:project) { create(:project, :internal, group: group) } - - let(:owner) { create(:user) } - let(:master) { create(:user) } - let(:developer) { create(:user) } - let(:reporter) { create(:user) } - let(:guest) { create(:user) } - - let(:project_guest) { create(:user) } - - before do - group.add_owner(owner) - group.add_master(master) - group.add_developer(developer) - group.add_reporter(reporter) - group.add_guest(guest) - - project.team << [project_guest, :guest] + let(:project_guest) do + create(:user) do |user| + project.add_guest(user) + end end describe "Group should be internal" do @@ -34,75 +21,75 @@ describe 'Internal Group access', feature: true do describe 'GET /groups/:path' do subject { group_path(group) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for project_guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(group) } + it { is_expected.to be_allowed_for(:master).of(group) } + it { is_expected.to be_allowed_for(:developer).of(group) } + it { is_expected.to be_allowed_for(:reporter).of(group) } + it { is_expected.to be_allowed_for(:guest).of(group) } + it { is_expected.to be_allowed_for(project_guest) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe 'GET /groups/:path/issues' do subject { issues_group_path(group) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for project_guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(group) } + it { is_expected.to be_allowed_for(:master).of(group) } + it { is_expected.to be_allowed_for(:developer).of(group) } + it { is_expected.to be_allowed_for(:reporter).of(group) } + it { is_expected.to be_allowed_for(:guest).of(group) } + it { is_expected.to be_allowed_for(project_guest) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe 'GET /groups/:path/merge_requests' do subject { merge_requests_group_path(group) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for project_guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(group) } + it { is_expected.to be_allowed_for(:master).of(group) } + it { is_expected.to be_allowed_for(:developer).of(group) } + it { is_expected.to be_allowed_for(:reporter).of(group) } + it { is_expected.to be_allowed_for(:guest).of(group) } + it { is_expected.to be_allowed_for(project_guest) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe 'GET /groups/:path/group_members' do subject { group_group_members_path(group) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for project_guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(group) } + it { is_expected.to be_allowed_for(:master).of(group) } + it { is_expected.to be_allowed_for(:developer).of(group) } + it { is_expected.to be_allowed_for(:reporter).of(group) } + it { is_expected.to be_allowed_for(:guest).of(group) } + it { is_expected.to be_allowed_for(project_guest) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe 'GET /groups/:path/edit' do subject { edit_group_path(group) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_denied_for master } - it { is_expected.to be_denied_for developer } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for project_guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :visitor } - it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(group) } + it { is_expected.to be_denied_for(:master).of(group) } + it { is_expected.to be_denied_for(:developer).of(group) } + it { is_expected.to be_denied_for(:reporter).of(group) } + it { is_expected.to be_denied_for(:guest).of(group) } + it { is_expected.to be_denied_for(project_guest) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:visitor) } + it { is_expected.to be_denied_for(:external) } end end diff --git a/spec/features/security/group/private_access_spec.rb b/spec/features/security/group/private_access_spec.rb index 75a93342628..1d6b3e77c22 100644 --- a/spec/features/security/group/private_access_spec.rb +++ b/spec/features/security/group/private_access_spec.rb @@ -3,25 +3,12 @@ require 'rails_helper' describe 'Private Group access', feature: true do include AccessMatchers - let(:group) { create(:group, :private) } + let(:group) { create(:group, :private) } let(:project) { create(:project, :private, group: group) } - - let(:owner) { create(:user) } - let(:master) { create(:user) } - let(:developer) { create(:user) } - let(:reporter) { create(:user) } - let(:guest) { create(:user) } - - let(:project_guest) { create(:user) } - - before do - group.add_owner(owner) - group.add_master(master) - group.add_developer(developer) - group.add_reporter(reporter) - group.add_guest(guest) - - project.team << [project_guest, :guest] + let(:project_guest) do + create(:user) do |user| + project.add_guest(user) + end end describe "Group should be private" do @@ -34,75 +21,75 @@ describe 'Private Group access', feature: true do describe 'GET /groups/:path' do subject { group_path(group) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for project_guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(group) } + it { is_expected.to be_allowed_for(:master).of(group) } + it { is_expected.to be_allowed_for(:developer).of(group) } + it { is_expected.to be_allowed_for(:reporter).of(group) } + it { is_expected.to be_allowed_for(:guest).of(group) } + it { is_expected.to be_allowed_for(project_guest) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe 'GET /groups/:path/issues' do subject { issues_group_path(group) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for project_guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(group) } + it { is_expected.to be_allowed_for(:master).of(group) } + it { is_expected.to be_allowed_for(:developer).of(group) } + it { is_expected.to be_allowed_for(:reporter).of(group) } + it { is_expected.to be_allowed_for(:guest).of(group) } + it { is_expected.to be_allowed_for(project_guest) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe 'GET /groups/:path/merge_requests' do subject { merge_requests_group_path(group) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for project_guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(group) } + it { is_expected.to be_allowed_for(:master).of(group) } + it { is_expected.to be_allowed_for(:developer).of(group) } + it { is_expected.to be_allowed_for(:reporter).of(group) } + it { is_expected.to be_allowed_for(:guest).of(group) } + it { is_expected.to be_allowed_for(project_guest) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe 'GET /groups/:path/group_members' do subject { group_group_members_path(group) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for project_guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(group) } + it { is_expected.to be_allowed_for(:master).of(group) } + it { is_expected.to be_allowed_for(:developer).of(group) } + it { is_expected.to be_allowed_for(:reporter).of(group) } + it { is_expected.to be_allowed_for(:guest).of(group) } + it { is_expected.to be_allowed_for(project_guest) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe 'GET /groups/:path/edit' do subject { edit_group_path(group) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_denied_for master } - it { is_expected.to be_denied_for developer } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for project_guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :visitor } - it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(group) } + it { is_expected.to be_denied_for(:master).of(group) } + it { is_expected.to be_denied_for(:developer).of(group) } + it { is_expected.to be_denied_for(:reporter).of(group) } + it { is_expected.to be_denied_for(:guest).of(group) } + it { is_expected.to be_denied_for(project_guest) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:visitor) } + it { is_expected.to be_denied_for(:external) } end end diff --git a/spec/features/security/group/public_access_spec.rb b/spec/features/security/group/public_access_spec.rb index 6c5ee93970b..d7d76177269 100644 --- a/spec/features/security/group/public_access_spec.rb +++ b/spec/features/security/group/public_access_spec.rb @@ -3,25 +3,12 @@ require 'rails_helper' describe 'Public Group access', feature: true do include AccessMatchers - let(:group) { create(:group, :public) } + let(:group) { create(:group, :public) } let(:project) { create(:project, :public, group: group) } - - let(:owner) { create(:user) } - let(:master) { create(:user) } - let(:developer) { create(:user) } - let(:reporter) { create(:user) } - let(:guest) { create(:user) } - - let(:project_guest) { create(:user) } - - before do - group.add_owner(owner) - group.add_master(master) - group.add_developer(developer) - group.add_reporter(reporter) - group.add_guest(guest) - - project.team << [project_guest, :guest] + let(:project_guest) do + create(:user) do |user| + project.add_guest(user) + end end describe "Group should be public" do @@ -34,75 +21,75 @@ describe 'Public Group access', feature: true do describe 'GET /groups/:path' do subject { group_path(group) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for project_guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :external } - it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(group) } + it { is_expected.to be_allowed_for(:master).of(group) } + it { is_expected.to be_allowed_for(:developer).of(group) } + it { is_expected.to be_allowed_for(:reporter).of(group) } + it { is_expected.to be_allowed_for(:guest).of(group) } + it { is_expected.to be_allowed_for(project_guest) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:external) } + it { is_expected.to be_allowed_for(:visitor) } end describe 'GET /groups/:path/issues' do subject { issues_group_path(group) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for project_guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :external } - it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(group) } + it { is_expected.to be_allowed_for(:master).of(group) } + it { is_expected.to be_allowed_for(:developer).of(group) } + it { is_expected.to be_allowed_for(:reporter).of(group) } + it { is_expected.to be_allowed_for(:guest).of(group) } + it { is_expected.to be_allowed_for(project_guest) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:external) } + it { is_expected.to be_allowed_for(:visitor) } end describe 'GET /groups/:path/merge_requests' do subject { merge_requests_group_path(group) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for project_guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :external } - it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(group) } + it { is_expected.to be_allowed_for(:master).of(group) } + it { is_expected.to be_allowed_for(:developer).of(group) } + it { is_expected.to be_allowed_for(:reporter).of(group) } + it { is_expected.to be_allowed_for(:guest).of(group) } + it { is_expected.to be_allowed_for(project_guest) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:external) } + it { is_expected.to be_allowed_for(:visitor) } end describe 'GET /groups/:path/group_members' do subject { group_group_members_path(group) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for project_guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :external } - it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(group) } + it { is_expected.to be_allowed_for(:master).of(group) } + it { is_expected.to be_allowed_for(:developer).of(group) } + it { is_expected.to be_allowed_for(:reporter).of(group) } + it { is_expected.to be_allowed_for(:guest).of(group) } + it { is_expected.to be_allowed_for(project_guest) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:external) } + it { is_expected.to be_allowed_for(:visitor) } end describe 'GET /groups/:path/edit' do subject { edit_group_path(group) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_denied_for master } - it { is_expected.to be_denied_for developer } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for project_guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :visitor } - it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(group) } + it { is_expected.to be_denied_for(:master).of(group) } + it { is_expected.to be_denied_for(:developer).of(group) } + it { is_expected.to be_denied_for(:reporter).of(group) } + it { is_expected.to be_denied_for(:guest).of(group) } + it { is_expected.to be_denied_for(project_guest) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:visitor) } + it { is_expected.to be_denied_for(:external) } end end diff --git a/spec/support/matchers/access_matchers.rb b/spec/support/matchers/access_matchers.rb index 691d7e05f57..ceddb656596 100644 --- a/spec/support/matchers/access_matchers.rb +++ b/spec/support/matchers/access_matchers.rb @@ -7,7 +7,7 @@ module AccessMatchers extend RSpec::Matchers::DSL include Warden::Test::Helpers - def emulate_user(user, project = nil) + def emulate_user(user, membership = nil) case user when :user login_as(create(:user)) @@ -19,16 +19,17 @@ module AccessMatchers login_as(create(:user, external: true)) when User login_as(user) - when :owner - raise ArgumentError, "cannot emulate owner without project" unless project - - login_as(project.owner) - when *Gitlab::Access.sym_options.keys - raise ArgumentError, "cannot emulate user #{user} without project" unless project + when *Gitlab::Access.sym_options_with_owner.keys + raise ArgumentError, "cannot emulate #{user} without membership parent" unless membership role = user - user = create(:user) - project.public_send(:"add_#{role}", user) + + if role == :owner && membership.owner + user = membership.owner + else + user = create(:user) + membership.public_send(:"add_#{role}", user) + end login_as(user) else @@ -47,14 +48,14 @@ module AccessMatchers matcher :be_allowed_for do |user| match do |url| - emulate_user(user, @project) + emulate_user(user, @membership) visit(url) status_code != 404 && current_path != new_user_session_path end - chain :of do |project| - @project = project + chain :of do |membership| + @membership = membership end description { description_for(user, 'allowed') } @@ -62,14 +63,14 @@ module AccessMatchers matcher :be_denied_for do |user| match do |url| - emulate_user(user, @project) + emulate_user(user, @membership) visit(url) status_code == 404 || current_path == new_user_session_path end - chain :of do |project| - @project = project + chain :of do |membership| + @membership = membership end description { description_for(user, 'denied') } -- cgit v1.2.1 From 11de793c27d0d92d5a9feb90a830fcff90dd7f0c Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Thu, 24 Nov 2016 03:35:38 +0600 Subject: new system note design for commit discussion --- app/assets/stylesheets/pages/notes.scss | 120 ++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index e66c1f8d072..538ad22daa4 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -172,6 +172,126 @@ ul.notes { &.timeline-entry { padding: 14px 10px; } + + .system-note { + font-size: 14px; + padding: 0; + clear: both; + + &.timeline-entry::after { + clear: none; + } + + .system-note-message { + display: inline-block; + + &::first-letter { + text-transform: lowercase; + } + + a { + color: $gl-link-color; + text-decoration: none; + } + + p { + display: inline-block; + margin: 0; + + &::first-letter { + text-transform: lowercase; + } + } + } + + .timeline-content { + padding: 14px 12px; + } + + .note-body { + overflow: hidden; + + .system-note-commit-list-toggler { + display: none; + padding: 10px 0 0; + cursor: pointer; + position: relative; + z-index: 2; + + &:hover { + color: $gl-link-color; + text-decoration: underline; + } + } + + .note-text { + & p:first-child { + display: none; + } + + &.system-note-commit-list { + max-height: 63px; + overflow: hidden; + display: block; + + ul { + margin: 3px 0 3px 15px !important; + + li { + font-family: $monospace_font; + font-size: 12px; + } + } + + p:first-child { + display: none; + } + + p:last-child { + a { + color: $gl-text-color; + + &:hover { + color: $gl-link-color; + } + } + } + + &::after { + content: ''; + width: 100%; + height: 67px; + position: absolute; + left: 0; + bottom: 0; + background: linear-gradient(rgba($gray-light, 0.1) -100px, $white-light 100%); + } + + &.hide-shade { + max-height: 100%; + overflow: auto; + + &::after { + display: none; + background: transparent; + } + } + } + } + } + + .timeline-icon { + display: none; + + .avatar { + visibility: hidden; + + .discussion-body & { + visibility: visible; + } + } + } + } } &.is-editting { -- cgit v1.2.1 From 5c6a748ec98fb1f5a8c1365a0273698385ac98fb Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Thu, 24 Nov 2016 04:18:00 +0600 Subject: changelog entry added --- .../unreleased/24894-style-system-note-in-commit-discussion.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/24894-style-system-note-in-commit-discussion.yml diff --git a/changelogs/unreleased/24894-style-system-note-in-commit-discussion.yml b/changelogs/unreleased/24894-style-system-note-in-commit-discussion.yml new file mode 100644 index 00000000000..7ddf0b46d4c --- /dev/null +++ b/changelogs/unreleased/24894-style-system-note-in-commit-discussion.yml @@ -0,0 +1,4 @@ +--- +title: Fixes system note style in commit discussion +merge_request: 7721 +author: -- cgit v1.2.1 From 28638356a20e64d5fb1ac6016fbe0f3645b16ecd Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Tue, 29 Nov 2016 14:07:25 +0600 Subject: removes redundant styles --- app/assets/stylesheets/pages/notes.scss | 116 -------------------------------- 1 file changed, 116 deletions(-) diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 538ad22daa4..dd079859630 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -174,123 +174,7 @@ ul.notes { } .system-note { - font-size: 14px; padding: 0; - clear: both; - - &.timeline-entry::after { - clear: none; - } - - .system-note-message { - display: inline-block; - - &::first-letter { - text-transform: lowercase; - } - - a { - color: $gl-link-color; - text-decoration: none; - } - - p { - display: inline-block; - margin: 0; - - &::first-letter { - text-transform: lowercase; - } - } - } - - .timeline-content { - padding: 14px 12px; - } - - .note-body { - overflow: hidden; - - .system-note-commit-list-toggler { - display: none; - padding: 10px 0 0; - cursor: pointer; - position: relative; - z-index: 2; - - &:hover { - color: $gl-link-color; - text-decoration: underline; - } - } - - .note-text { - & p:first-child { - display: none; - } - - &.system-note-commit-list { - max-height: 63px; - overflow: hidden; - display: block; - - ul { - margin: 3px 0 3px 15px !important; - - li { - font-family: $monospace_font; - font-size: 12px; - } - } - - p:first-child { - display: none; - } - - p:last-child { - a { - color: $gl-text-color; - - &:hover { - color: $gl-link-color; - } - } - } - - &::after { - content: ''; - width: 100%; - height: 67px; - position: absolute; - left: 0; - bottom: 0; - background: linear-gradient(rgba($gray-light, 0.1) -100px, $white-light 100%); - } - - &.hide-shade { - max-height: 100%; - overflow: auto; - - &::after { - display: none; - background: transparent; - } - } - } - } - } - - .timeline-icon { - display: none; - - .avatar { - visibility: hidden; - - .discussion-body & { - visibility: visible; - } - } - } } } -- cgit v1.2.1 From 89bd04cda1660fb328ece9c7cc3d94d4e87fec6a Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Tue, 29 Nov 2016 17:54:16 +0600 Subject: shows commits SHAs in monospcase and commit messaages in normal font --- app/assets/stylesheets/pages/notes.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index e66c1f8d072..3ef4c51d6f1 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -97,7 +97,7 @@ ul.notes { ul { margin: 3px 0 3px 15px !important; - li { + .gfm-commit { font-family: $monospace_font; font-size: 12px; } -- cgit v1.2.1 From df085e41433529193636c5b40e59bd5e7cd3e20c Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Tue, 29 Nov 2016 17:56:12 +0600 Subject: makes ul bullets fully visible --- app/assets/stylesheets/pages/notes.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 3ef4c51d6f1..30922d96ae9 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -95,7 +95,7 @@ ul.notes { display: block; ul { - margin: 3px 0 3px 15px !important; + margin: 3px 0 3px 16 px !important; .gfm-commit { font-family: $monospace_font; -- cgit v1.2.1 From e77c867dba7f559f7156bfc8800d7d78c9dd31f9 Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Tue, 29 Nov 2016 18:05:21 +0600 Subject: adjust padding in list --- app/assets/stylesheets/pages/notes.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 30922d96ae9..38f34a7ca9d 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -95,7 +95,7 @@ ul.notes { display: block; ul { - margin: 3px 0 3px 16 px !important; + margin: 3px 0 3px 16px !important; .gfm-commit { font-family: $monospace_font; -- cgit v1.2.1 From 49ae66fe95fb898129635aace008dcd510e677d9 Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Tue, 29 Nov 2016 18:07:20 +0600 Subject: adjust commit list ul height --- app/assets/stylesheets/pages/notes.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 38f34a7ca9d..beef5c0a24f 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -90,7 +90,7 @@ ul.notes { } &.system-note-commit-list { - max-height: 63px; + max-height: 70px; overflow: hidden; display: block; -- cgit v1.2.1 From fbbf177e3b604bebce3b10f8eea8920ff5606fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Wed, 28 Sep 2016 12:45:46 +0200 Subject: New `gitlab:workhorse:install` rake task MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- ...l-rake-task-similar-to-gitlab-shell-install.yml | 4 + doc/install/installation.md | 28 +++-- doc/update/8.12-to-8.13.md | 2 +- lib/tasks/gitlab/shell.rake | 42 ++------ lib/tasks/gitlab/task_helpers.rake | 35 +++++- lib/tasks/gitlab/workhorse.rake | 23 ++++ spec/support/stub_configuration.rb | 4 + spec/tasks/gitlab/task_helpers_spec.rb | 87 +++++++++++++++ spec/tasks/gitlab/workhorse_rake_spec.rb | 117 +++++++++++++++++++++ 9 files changed, 295 insertions(+), 47 deletions(-) create mode 100644 changelogs/unreleased/22719-provide-a-new-gitlab-workhorse-install-rake-task-similar-to-gitlab-shell-install.yml create mode 100644 lib/tasks/gitlab/workhorse.rake create mode 100644 spec/tasks/gitlab/task_helpers_spec.rb create mode 100644 spec/tasks/gitlab/workhorse_rake_spec.rb diff --git a/changelogs/unreleased/22719-provide-a-new-gitlab-workhorse-install-rake-task-similar-to-gitlab-shell-install.yml b/changelogs/unreleased/22719-provide-a-new-gitlab-workhorse-install-rake-task-similar-to-gitlab-shell-install.yml new file mode 100644 index 00000000000..54bd313f075 --- /dev/null +++ b/changelogs/unreleased/22719-provide-a-new-gitlab-workhorse-install-rake-task-similar-to-gitlab-shell-install.yml @@ -0,0 +1,4 @@ +--- +title: New `gitlab:workhorse:install` rake task +merge_request: 6574 +author: diff --git a/doc/install/installation.md b/doc/install/installation.md index ee02d6024d9..25b186b63f7 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -175,7 +175,7 @@ We recommend using a PostgreSQL database. For MySQL check the ```bash sudo -u postgres psql -d template1 -c "CREATE USER git CREATEDB;" ``` - + 1. Create the `pg_trgm` extension (required for GitLab 8.6+): ```bash @@ -396,15 +396,25 @@ GitLab Shell is an SSH access and repository management software developed speci ### Install gitlab-workhorse -GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/). -If you are not using Linux you may have to run `gmake` instead of -`make` below. +GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/). The +following command-line will install GitLab-Workhorse in `home/git/gitlab-workhorse` +which is the recommended location. - cd /home/git - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git - cd gitlab-workhorse - sudo -u git -H git checkout v1.0.1 - sudo -u git -H make + cd /home/git/gitlab + + sudo -u git -H bundle exec rake gitlab:workhorse:install[/home/git/gitlab-workhorse] RAILS_ENV=production + +You can specify a different Git repository by providing `GITLAB_WORKHORSE_REPO`: + + cd /home/git/gitlab + + sudo -u git -H bundle exec rake gitlab:workhorse:install[/home/git/gitlab-workhorse] GITLAB_WORKHORSE_REPO=https://gitlab.com/gitlab-org/gitlab-ce.git RAILS_ENV=production + +You can specify a different version to use by providing `GITLAB_WORKHORSE_VERSION`: + + cd /home/git/gitlab + + sudo -u git -H bundle exec rake gitlab:workhorse:install[/home/git/gitlab-workhorse] GITLAB_WORKHORSE_VERSION=0.8.1 RAILS_ENV=production ### Initialize Database and Activate Advanced Features diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md index c0084d9d59c..8c0d3f78b55 100644 --- a/doc/update/8.12-to-8.13.md +++ b/doc/update/8.12-to-8.13.md @@ -166,7 +166,7 @@ See [smtp_settings.rb.sample] as an example. Ensure you're still up-to-date with the latest init script changes: sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab - + For Ubuntu 16.04.1 LTS: sudo systemctl daemon-reload diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index 58761a129d4..5a09cd7ce41 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -5,42 +5,23 @@ namespace :gitlab do warn_user_is_not_gitlab default_version = Gitlab::Shell.version_required - default_version_tag = 'v' + default_version - args.with_defaults(tag: default_version_tag, repo: "https://gitlab.com/gitlab-org/gitlab-shell.git") + default_version_tag = "v#{default_version}" + args.with_defaults(tag: default_version_tag, repo: 'https://gitlab.com/gitlab-org/gitlab-shell.git') - user = Gitlab.config.gitlab.user - home_dir = Rails.env.test? ? Rails.root.join('tmp/tests') : Gitlab.config.gitlab.user_home gitlab_url = Gitlab.config.gitlab.url # gitlab-shell requires a / at the end of the url gitlab_url += '/' unless gitlab_url.end_with?('/') target_dir = Gitlab.config.gitlab_shell.path - # Clone if needed - if File.directory?(target_dir) - Dir.chdir(target_dir) do - system(*%W(Gitlab.config.git.bin_path} fetch --tags --quiet)) - system(*%W(Gitlab.config.git.bin_path} checkout --quiet #{default_version_tag})) - end - else - system(*%W(#{Gitlab.config.git.bin_path} clone -- #{args.repo} #{target_dir})) - end + checkout_or_clone_tag(tag: default_version_tag, repo: args.repo, target_dir: target_dir) # Make sure we're on the right tag Dir.chdir(target_dir) do - # First try to checkout without fetching - # to avoid stalling tests if the Internet is down. - reseted = reset_to_commit(args) - - unless reseted - system(*%W(#{Gitlab.config.git.bin_path} fetch origin)) - reset_to_commit(args) - end - config = { - user: user, + user: Gitlab.config.gitlab.user, gitlab_url: gitlab_url, http_settings: {self_signed_cert: false}.stringify_keys, - auth_file: File.join(home_dir, ".ssh", "authorized_keys"), + auth_file: File.join(user_home, ".ssh", "authorized_keys"), redis: { bin: %x{which redis-cli}.chomp, namespace: "resque:gitlab" @@ -74,7 +55,7 @@ namespace :gitlab do # be an issue since it is more than likely that there are no "normal" # user accounts on a gitlab server). The alternative is for the admin to # install a ruby (1.9.3+) in the global path. - File.open(File.join(home_dir, ".ssh", "environment"), "w+") do |f| + File.open(File.join(user_home, ".ssh", "environment"), "w+") do |f| f.puts "PATH=#{ENV['PATH']}" end @@ -142,15 +123,4 @@ namespace :gitlab do puts "Quitting...".color(:red) exit 1 end - - def reset_to_commit(args) - tag, status = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} describe -- #{args.tag})) - - unless status.zero? - tag, status = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} describe -- origin/#{args.tag})) - end - - tag = tag.strip - system(*%W(#{Gitlab.config.git.bin_path} reset --hard #{tag})) - end end diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake index 74be413423a..85c3a3a9b0f 100644 --- a/lib/tasks/gitlab/task_helpers.rake +++ b/lib/tasks/gitlab/task_helpers.rake @@ -70,7 +70,7 @@ namespace :gitlab do # Runs the given command # - # Returns nil if the command was not found + # Returns '' if the command was not found # Returns the output of the command otherwise # # see also #run_and_match @@ -137,4 +137,37 @@ namespace :gitlab do def repository_storage_paths_args Gitlab.config.repositories.storages.values end + + def user_home + Rails.env.test? ? Rails.root.join('tmp/tests') : Gitlab.config.gitlab.user_home + end + + def checkout_or_clone_tag(tag:, repo:, target_dir:) + if Dir.exist?(target_dir) + Dir.chdir(target_dir) do + run_command(%W[#{Gitlab.config.git.bin_path} fetch --tags --quiet]) + run_command(%W[#{Gitlab.config.git.bin_path} checkout --quiet #{tag}]) + end + else + run_command(%W[#{Gitlab.config.git.bin_path} clone -- #{repo} #{target_dir}]) + end + + # Make sure we're on the right tag + Dir.chdir(target_dir) do + # First try to checkout without fetching + # to avoid stalling tests if the Internet is down. + reset_to_tag(tag) + end + end + + def reset_to_tag(tag_wanted) + tag, status = Gitlab::Popen.popen(%W[#{Gitlab.config.git.bin_path} describe -- #{tag_wanted}]) + + unless status.zero? + run_command(%W(#{Gitlab.config.git.bin_path} fetch origin)) + tag = run_command(%W[#{Gitlab.config.git.bin_path} describe -- origin/#{tag_wanted}]) + end + + run_command(%W[#{Gitlab.config.git.bin_path} reset --hard #{tag.strip}]) + end end diff --git a/lib/tasks/gitlab/workhorse.rake b/lib/tasks/gitlab/workhorse.rake new file mode 100644 index 00000000000..5964cb83b89 --- /dev/null +++ b/lib/tasks/gitlab/workhorse.rake @@ -0,0 +1,23 @@ +namespace :gitlab do + namespace :workhorse do + desc "GitLab | Install or upgrade gitlab-workhorse" + task :install, [:dir] => :environment do |t, args| + warn_user_is_not_gitlab + unless args.dir.present? + abort "Please specify the directory where you want to install gitlab-workhorse:\n rake gitlab:workhorse:install[/home/git/gitlab-workhorse]" + end + + tag = "v#{ENV['GITLAB_WORKHORSE_VERSION'] || Gitlab::Workhorse.version}" + repo = ENV['GITLAB_WORKHORSE_REPO'] || 'https://gitlab.com/gitlab-org/gitlab-workhorse.git' + + checkout_or_clone_tag(tag: tag, repo: repo, target_dir: args.dir) + + _, status = Gitlab::Popen.popen(%w[which gmake]) + command = status.zero? ? 'gmake' : 'make' + + Dir.chdir(args.dir) do + run_command([command]) + end + end + end +end diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb index f40ee862df8..c0847bbd7f1 100644 --- a/spec/support/stub_configuration.rb +++ b/spec/support/stub_configuration.rb @@ -17,6 +17,10 @@ module StubConfiguration allow(Gitlab.config.gravatar).to receive_messages(messages) end + def stub_gitlab_workhorse_setting(messages) + allow(Gitlab.config.gitlab_workhorse).to receive_messages(messages) + end + def stub_incoming_email_setting(messages) allow(Gitlab.config.incoming_email).to receive_messages(messages) end diff --git a/spec/tasks/gitlab/task_helpers_spec.rb b/spec/tasks/gitlab/task_helpers_spec.rb new file mode 100644 index 00000000000..2a2d4a39ba8 --- /dev/null +++ b/spec/tasks/gitlab/task_helpers_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' +require 'rake' + +describe 'gitlab:workhorse namespace rake task' do + before :all do + Rake.application.rake_require 'tasks/gitlab/task_helpers' + + # empty task as env is already loaded + Rake::Task.define_task :environment + end + + describe '#checkout_or_clone_tag' do + let(:repo) { 'https://gitlab.com/gitlab-org/gitlab-test.git' } + let(:clone_path) { Rails.root.join('tmp/tests/task_helpers_tests').to_s } + let(:tag) { 'v1.1.0' } + before do + FileUtils.rm_rf(clone_path) + allow_any_instance_of(Object).to receive(:run_command) + expect_any_instance_of(Object).to receive(:reset_to_tag).with(tag) + end + + after do + FileUtils.rm_rf(clone_path) + end + + context 'target_dir does not exist' do + it 'clones the repo, retrieve the tag from origin, and checkout the tag' do + expect(Dir).to receive(:chdir).and_call_original + expect_any_instance_of(Object). + to receive(:run_command).with(%W[#{Gitlab.config.git.bin_path} clone -- #{repo} #{clone_path}]) { FileUtils.mkdir_p(clone_path) } # Fake the cloning + + checkout_or_clone_tag(tag: tag, repo: repo, target_dir: clone_path) + end + end + + context 'target_dir exists' do + before do + FileUtils.mkdir_p(clone_path) + end + + it 'fetch and checkout the tag' do + expect(Dir).to receive(:chdir).twice.and_call_original + expect_any_instance_of(Object). + to receive(:run_command).with(%W[#{Gitlab.config.git.bin_path} fetch --tags --quiet]) + expect_any_instance_of(Object). + to receive(:run_command).with(%W[#{Gitlab.config.git.bin_path} checkout --quiet #{tag}]) + + checkout_or_clone_tag(tag: tag, repo: repo, target_dir: clone_path) + end + end + end + + describe '#reset_to_tag' do + let(:tag) { 'v1.1.0' } + before do + expect_any_instance_of(Object). + to receive(:run_command).with(%W[#{Gitlab.config.git.bin_path} reset --hard #{tag}]) + end + + context 'when the tag is not checked out locally' do + before do + expect(Gitlab::Popen). + to receive(:popen).with(%W[#{Gitlab.config.git.bin_path} describe -- #{tag}]).and_return(['', 42]) + end + + it 'fetch origin, ensure the tag exists, and resets --hard to the given tag' do + expect_any_instance_of(Object). + to receive(:run_command).with(%W[#{Gitlab.config.git.bin_path} fetch origin]) + expect_any_instance_of(Object). + to receive(:run_command).with(%W[#{Gitlab.config.git.bin_path} describe -- origin/#{tag}]).and_return(tag) + + reset_to_tag(tag) + end + end + + context 'when the tag is checked out locally' do + before do + expect(Gitlab::Popen). + to receive(:popen).with(%W[#{Gitlab.config.git.bin_path} describe -- #{tag}]).and_return([tag, 0]) + end + + it 'resets --hard to the given tag' do + reset_to_tag(tag) + end + end + end +end diff --git a/spec/tasks/gitlab/workhorse_rake_spec.rb b/spec/tasks/gitlab/workhorse_rake_spec.rb new file mode 100644 index 00000000000..c8ad004282a --- /dev/null +++ b/spec/tasks/gitlab/workhorse_rake_spec.rb @@ -0,0 +1,117 @@ +require 'spec_helper' +require 'rake' + +describe 'gitlab:workhorse namespace rake task' do + before :all do + Rake.application.rake_require 'tasks/gitlab/task_helpers' + Rake.application.rake_require 'tasks/gitlab/workhorse' + + # empty task as env is already loaded + Rake::Task.define_task :environment + end + + def run_rake_task(task_name, *args) + Rake::Task[task_name].reenable + Rake.application.invoke_task("#{task_name}[#{args.join(',')}]") + end + + describe 'install' do + let(:repo) { 'https://gitlab.com/gitlab-org/gitlab-workhorse.git' } + let(:clone_path) { Rails.root.join('tmp/tests/gitlab-workhorse').to_s } + let(:tag) { "v#{File.read(Rails.root.join(Gitlab::Workhorse::VERSION_FILE)).chomp}" } + before do + # avoid writing task output to spec progress + allow($stdout).to receive :write + allow(ENV).to receive(:[]) + end + + context 'no dir given' do + it 'aborts and display a help message' do + expect { run_rake_task('gitlab:workhorse:install') }.to raise_error /Please specify the directory where you want to install gitlab-workhorse/ + end + end + + context 'when an underlying Git command fail' do + it 'aborts and display a help message' do + expect_any_instance_of(Object). + to receive(:checkout_or_clone_tag).and_raise 'Git error' + + expect { run_rake_task('gitlab:workhorse:install', clone_path) }.to raise_error 'Git error' + end + end + + describe 'checkout or clone' do + before do + expect(Dir).to receive(:chdir).with(clone_path) + end + + it 'calls checkout_or_clone_tag with the right arguments' do + expect_any_instance_of(Object). + to receive(:checkout_or_clone_tag).with(tag: tag, repo: repo, target_dir: clone_path) + + run_rake_task('gitlab:workhorse:install', clone_path) + end + + context 'given a specific repo' do + before do + expect(ENV).to receive(:[]).with('GITLAB_WORKHORSE_REPO').and_return('https://gitlab.com/user1/gitlab-workhorse.git') + end + + it 'calls checkout_or_clone_tag with the given repo' do + expect_any_instance_of(Object). + to receive(:checkout_or_clone_tag).with(tag: tag, repo: 'https://gitlab.com/user1/gitlab-workhorse.git', target_dir: clone_path) + + run_rake_task('gitlab:workhorse:install', clone_path) + end + end + + context 'given a specific version' do + before do + allow(ENV).to receive(:[]).with('GITLAB_WORKHORSE_VERSION').and_return('42.42.0') + end + + it 'calls checkout_or_clone_tag with the given repo' do + expect_any_instance_of(Object). + to receive(:checkout_or_clone_tag).with(tag: 'v42.42.0', repo: repo, target_dir: clone_path) + + run_rake_task('gitlab:workhorse:install', clone_path) + end + end + end + + describe 'gmake/make' do + before do + FileUtils.mkdir_p(clone_path) + expect(Dir).to receive(:chdir).with(clone_path).and_call_original + end + + context 'gmake is available' do + before do + expect_any_instance_of(Object).to receive(:checkout_or_clone_tag) + allow_any_instance_of(Object).to receive(:run_command).with(['gmake']).and_return(true) + end + + it 'calls gmake in the gitlab-workhorse directory' do + expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['/usr/bin/gmake', 0]) + expect_any_instance_of(Object).to receive(:run_command).with(['gmake']).and_return(true) + + run_rake_task('gitlab:workhorse:install', clone_path) + end + end + + context 'gmake is not available' do + before do + expect_any_instance_of(Object).to receive(:checkout_or_clone_tag) + allow_any_instance_of(Object).to receive(:run_command).with(['make']).and_return(true) + end + + it 'calls make in the gitlab-workhorse directory' do + expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['', 42]) + expect_any_instance_of(Object).to receive(:run_command).with(['make']).and_return(true) + + run_rake_task('gitlab:workhorse:install', clone_path) + end + end + end + end +end -- cgit v1.2.1 From a9c250eaddf758f99ac8c868dc86f4df0cc157f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Thu, 24 Nov 2016 14:19:09 +0100 Subject: Add #run_command! to task helpers to raise a TaskFailedError if status is not 0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- doc/install/installation.md | 2 +- doc/update/8.14-to-8.15.md | 202 +++++++++++++++++++++++++++++++ lib/tasks/gitlab/task_helpers.rake | 36 ++++-- lib/tasks/gitlab/workhorse.rake | 2 +- spec/tasks/gitlab/task_helpers_spec.rb | 22 ++-- spec/tasks/gitlab/workhorse_rake_spec.rb | 8 +- 6 files changed, 246 insertions(+), 26 deletions(-) create mode 100644 doc/update/8.14-to-8.15.md diff --git a/doc/install/installation.md b/doc/install/installation.md index 25b186b63f7..de650f0900d 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -408,7 +408,7 @@ You can specify a different Git repository by providing `GITLAB_WORKHORSE_REPO`: cd /home/git/gitlab - sudo -u git -H bundle exec rake gitlab:workhorse:install[/home/git/gitlab-workhorse] GITLAB_WORKHORSE_REPO=https://gitlab.com/gitlab-org/gitlab-ce.git RAILS_ENV=production + sudo -u git -H bundle exec rake gitlab:workhorse:install[/home/git/gitlab-workhorse] GITLAB_WORKHORSE_REPO=https://example.com/gitlab-workhorse.git RAILS_ENV=production You can specify a different version to use by providing `GITLAB_WORKHORSE_VERSION`: diff --git a/doc/update/8.14-to-8.15.md b/doc/update/8.14-to-8.15.md new file mode 100644 index 00000000000..9e6a3b45394 --- /dev/null +++ b/doc/update/8.14-to-8.15.md @@ -0,0 +1,202 @@ +# From 8.14 to 8.15 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 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. Update Ruby + +We will continue supporting Ruby < 2.3 for the time being but we recommend you +upgrade to Ruby 2.3 if you're running a source installation, as this is the same +version that ships with our Omnibus package. + +You can check which version you are running with `ruby -v`. + +Download and compile Ruby: + +```bash +mkdir /tmp/ruby && cd /tmp/ruby +curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.1.tar.gz +echo 'c39b4001f7acb4e334cb60a0f4df72d434bef711 ruby-2.3.1.tar.gz' | shasum --check - && tar xzf ruby-2.3.1.tar.gz +cd ruby-2.3.1 +./configure --disable-install-rdoc +make +sudo make install +``` + +Install Bundler: + +```bash +sudo gem install bundler --no-ri --no-rdoc +``` + +### 4. 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-15-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 8-15-stable-ee +``` + +### 5. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v4.0.0 +``` + +### 6. Update gitlab-workhorse + +Install and compile gitlab-workhorse. This requires +[Go 1.5](https://golang.org/dl) which should already be on your system from +GitLab 8.1. + +```bash +sudo -u git -H bundle exec rake gitlab:workhorse:install[/home/git/gitlab-workhorse] RAILS_ENV=production +``` + +### 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 + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# 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 +``` + +### 8. Update configuration files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +git diff origin/8-13-stable:config/gitlab.yml.example origin/8-15-stable:config/gitlab.yml.example +``` + +#### Git configuration + +Configure Git to generate packfile bitmaps (introduced in Git 2.0) on +the GitLab server during `git gc`. + +```sh +sudo -u git -H git config --global repack.writeBitmaps true +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +# For HTTPS configurations +git diff origin/8-13-stable:lib/support/nginx/gitlab-ssl origin/8-15-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-13-stable:lib/support/nginx/gitlab origin/8-15-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-workhorse 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-15-stable/lib/support/init.d/gitlab.default.example#L38 + +#### SMTP configuration + +If you're installing from source and use SMTP to deliver mail, you will need to add the following line +to config/initializers/smtp_settings.rb: + +```ruby +ActionMailer::Base.delivery_method = :smtp +``` + +See [smtp_settings.rb.sample] as an example. + +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-15-stable/config/initializers/smtp_settings.rb.sample#L13 + +#### Init script + +Ensure you're still up-to-date with the latest init script changes: + + sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab + +For Ubuntu 16.04.1 LTS: + + sudo systemctl daemon-reload + +### 9. Start application + + sudo service gitlab start + sudo service nginx restart + +### 10. 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.14) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.13 to 8.14](8.13-to-8.14.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. diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake index 85c3a3a9b0f..c0759b96602 100644 --- a/lib/tasks/gitlab/task_helpers.rake +++ b/lib/tasks/gitlab/task_helpers.rake @@ -1,4 +1,5 @@ module Gitlab + class TaskFailedError < StandardError; end class TaskAbortedByUserError < StandardError; end end @@ -81,6 +82,18 @@ namespace :gitlab do '' # if the command does not exist, return an empty string end + # Runs the given command and raise a Gitlab::TaskFailedError exception if + # the command does not exit with 0 + # + # Returns the output of the command otherwise + def run_command!(command) + output, status = Gitlab::Popen.popen(command) + + raise Gitlab::TaskFailedError unless status.zero? + + output + end + def uid_for(user_name) run_command(%W(id -u #{user_name})).chomp.to_i end @@ -145,11 +158,11 @@ namespace :gitlab do def checkout_or_clone_tag(tag:, repo:, target_dir:) if Dir.exist?(target_dir) Dir.chdir(target_dir) do - run_command(%W[#{Gitlab.config.git.bin_path} fetch --tags --quiet]) - run_command(%W[#{Gitlab.config.git.bin_path} checkout --quiet #{tag}]) + run_command!(%W[#{Gitlab.config.git.bin_path} fetch --tags --quiet]) + run_command!(%W[#{Gitlab.config.git.bin_path} checkout --quiet #{tag}]) end else - run_command(%W[#{Gitlab.config.git.bin_path} clone -- #{repo} #{target_dir}]) + run_command!(%W[#{Gitlab.config.git.bin_path} clone -- #{repo} #{target_dir}]) end # Make sure we're on the right tag @@ -161,13 +174,18 @@ namespace :gitlab do end def reset_to_tag(tag_wanted) - tag, status = Gitlab::Popen.popen(%W[#{Gitlab.config.git.bin_path} describe -- #{tag_wanted}]) + tag = + begin + run_command!(%W[#{Gitlab.config.git.bin_path} describe -- #{tag_wanted}]) + rescue Gitlab::TaskFailedError + run_command!(%W[#{Gitlab.config.git.bin_path} fetch origin]) + run_command!(%W[#{Gitlab.config.git.bin_path} describe -- origin/#{tag_wanted}]) + end - unless status.zero? - run_command(%W(#{Gitlab.config.git.bin_path} fetch origin)) - tag = run_command(%W[#{Gitlab.config.git.bin_path} describe -- origin/#{tag_wanted}]) + if tag + run_command!(%W[#{Gitlab.config.git.bin_path} reset --hard #{tag.strip}]) + else + raise Gitlab::TaskFailedError end - - run_command(%W[#{Gitlab.config.git.bin_path} reset --hard #{tag.strip}]) end end diff --git a/lib/tasks/gitlab/workhorse.rake b/lib/tasks/gitlab/workhorse.rake index 5964cb83b89..563744dd0c7 100644 --- a/lib/tasks/gitlab/workhorse.rake +++ b/lib/tasks/gitlab/workhorse.rake @@ -16,7 +16,7 @@ namespace :gitlab do command = status.zero? ? 'gmake' : 'make' Dir.chdir(args.dir) do - run_command([command]) + run_command!([command]) end end end diff --git a/spec/tasks/gitlab/task_helpers_spec.rb b/spec/tasks/gitlab/task_helpers_spec.rb index 2a2d4a39ba8..dccb3b4cf9a 100644 --- a/spec/tasks/gitlab/task_helpers_spec.rb +++ b/spec/tasks/gitlab/task_helpers_spec.rb @@ -15,7 +15,7 @@ describe 'gitlab:workhorse namespace rake task' do let(:tag) { 'v1.1.0' } before do FileUtils.rm_rf(clone_path) - allow_any_instance_of(Object).to receive(:run_command) + allow_any_instance_of(Object).to receive(:run_command!) expect_any_instance_of(Object).to receive(:reset_to_tag).with(tag) end @@ -27,7 +27,7 @@ describe 'gitlab:workhorse namespace rake task' do it 'clones the repo, retrieve the tag from origin, and checkout the tag' do expect(Dir).to receive(:chdir).and_call_original expect_any_instance_of(Object). - to receive(:run_command).with(%W[#{Gitlab.config.git.bin_path} clone -- #{repo} #{clone_path}]) { FileUtils.mkdir_p(clone_path) } # Fake the cloning + to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} clone -- #{repo} #{clone_path}]) { FileUtils.mkdir_p(clone_path) } # Fake the cloning checkout_or_clone_tag(tag: tag, repo: repo, target_dir: clone_path) end @@ -41,9 +41,9 @@ describe 'gitlab:workhorse namespace rake task' do it 'fetch and checkout the tag' do expect(Dir).to receive(:chdir).twice.and_call_original expect_any_instance_of(Object). - to receive(:run_command).with(%W[#{Gitlab.config.git.bin_path} fetch --tags --quiet]) + to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} fetch --tags --quiet]) expect_any_instance_of(Object). - to receive(:run_command).with(%W[#{Gitlab.config.git.bin_path} checkout --quiet #{tag}]) + to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} checkout --quiet #{tag}]) checkout_or_clone_tag(tag: tag, repo: repo, target_dir: clone_path) end @@ -54,20 +54,20 @@ describe 'gitlab:workhorse namespace rake task' do let(:tag) { 'v1.1.0' } before do expect_any_instance_of(Object). - to receive(:run_command).with(%W[#{Gitlab.config.git.bin_path} reset --hard #{tag}]) + to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} reset --hard #{tag}]) end context 'when the tag is not checked out locally' do before do - expect(Gitlab::Popen). - to receive(:popen).with(%W[#{Gitlab.config.git.bin_path} describe -- #{tag}]).and_return(['', 42]) + expect_any_instance_of(Object). + to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} describe -- #{tag}]).and_raise(Gitlab::TaskFailedError) end it 'fetch origin, ensure the tag exists, and resets --hard to the given tag' do expect_any_instance_of(Object). - to receive(:run_command).with(%W[#{Gitlab.config.git.bin_path} fetch origin]) + to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} fetch origin]) expect_any_instance_of(Object). - to receive(:run_command).with(%W[#{Gitlab.config.git.bin_path} describe -- origin/#{tag}]).and_return(tag) + to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} describe -- origin/#{tag}]).and_return(tag) reset_to_tag(tag) end @@ -75,8 +75,8 @@ describe 'gitlab:workhorse namespace rake task' do context 'when the tag is checked out locally' do before do - expect(Gitlab::Popen). - to receive(:popen).with(%W[#{Gitlab.config.git.bin_path} describe -- #{tag}]).and_return([tag, 0]) + expect_any_instance_of(Object). + to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} describe -- #{tag}]).and_return(tag) end it 'resets --hard to the given tag' do diff --git a/spec/tasks/gitlab/workhorse_rake_spec.rb b/spec/tasks/gitlab/workhorse_rake_spec.rb index c8ad004282a..87bc1b128bf 100644 --- a/spec/tasks/gitlab/workhorse_rake_spec.rb +++ b/spec/tasks/gitlab/workhorse_rake_spec.rb @@ -88,12 +88,12 @@ describe 'gitlab:workhorse namespace rake task' do context 'gmake is available' do before do expect_any_instance_of(Object).to receive(:checkout_or_clone_tag) - allow_any_instance_of(Object).to receive(:run_command).with(['gmake']).and_return(true) + allow_any_instance_of(Object).to receive(:run_command!).with(['gmake']).and_return(true) end it 'calls gmake in the gitlab-workhorse directory' do expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['/usr/bin/gmake', 0]) - expect_any_instance_of(Object).to receive(:run_command).with(['gmake']).and_return(true) + expect_any_instance_of(Object).to receive(:run_command!).with(['gmake']).and_return(true) run_rake_task('gitlab:workhorse:install', clone_path) end @@ -102,12 +102,12 @@ describe 'gitlab:workhorse namespace rake task' do context 'gmake is not available' do before do expect_any_instance_of(Object).to receive(:checkout_or_clone_tag) - allow_any_instance_of(Object).to receive(:run_command).with(['make']).and_return(true) + allow_any_instance_of(Object).to receive(:run_command!).with(['make']).and_return(true) end it 'calls make in the gitlab-workhorse directory' do expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['', 42]) - expect_any_instance_of(Object).to receive(:run_command).with(['make']).and_return(true) + expect_any_instance_of(Object).to receive(:run_command!).with(['make']).and_return(true) run_rake_task('gitlab:workhorse:install', clone_path) end -- cgit v1.2.1 From 5b052605b7eb7281f88040962e530ac80bbe3774 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Wed, 30 Nov 2016 10:13:47 +0100 Subject: Extend code review docs with chapter about the right balance --- doc/development/code_review.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/doc/development/code_review.md b/doc/development/code_review.md index c5c23b5c0b8..709cefcb5c6 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -70,10 +70,36 @@ experience, refactors the existing code). Then: - After a round of line notes, it can be helpful to post a summary note such as "LGTM :thumbsup:", or "Just a couple things to address." - Avoid accepting a merge request before the build succeeds. Of course, "Merge - When Build Succeeds" (MWBS) is fine. + When Pipeline Succeeds" is fine. - If you set the MR to "Merge When Build Succeeds", you should take over subsequent revisions for anything that would be spotted after that. +## The right balance + +One of the most difficult things during the code review is finding the right +balance in how deep the reviewer can interfere with the code created by a +reviewee. + +- Learning how to find the right balance takes time, that is why we have + minibosses that become merge request endbosses after some time spent on + reviewing merge requests. +- Finding bugs and improving code style is important, but thinking about good + design is important as well. Building abstractions and good design is what + makes it possible to hide complexity and is a leverage for the future work. +- Asking reviewee to change the design sometimes means the complete rewrite of + the contributed code. It is usually a good idea to ask other merge request + endboss before doing it, but have the courage to do it when you believe it is + important. +- There is a difference in doing things right and doing things right now. + Ideally, we should do the former, but in the real world we need the latter as + well. The good example is a security fix which should be released as soon as + possible. Asking reviewee to do the major refactoring in the merge request + that is an urgent fix should be avoided. +- Doing things well today is usually better than doing something perfectly + tomorrow. Shipping a kludge today is usually worse than doing something well + tomorrow. When you are not able to find the right balance, ask other people + about their opinion. + ## Credits Largely based on the [thoughtbot code review guide]. -- cgit v1.2.1 From 0fbb5a86dbe054af91c20d36697fda273445dd2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ger=C5=91?= <davidpgero@gmail.com> Date: Sat, 29 Oct 2016 12:51:01 +0200 Subject: Add Human Readable Timestamp to backup tar file --- .../23718-backup-rake-task-human-readable.yml | 4 +++ doc/raketasks/backup_restore.md | 2 +- lib/backup/manager.rb | 18 ++++++++----- spec/tasks/gitlab/backup_rake_spec.rb | 31 ++++++++++++++++++++++ 4 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 changelogs/unreleased/23718-backup-rake-task-human-readable.yml diff --git a/changelogs/unreleased/23718-backup-rake-task-human-readable.yml b/changelogs/unreleased/23718-backup-rake-task-human-readable.yml new file mode 100644 index 00000000000..2e7583244ac --- /dev/null +++ b/changelogs/unreleased/23718-backup-rake-task-human-readable.yml @@ -0,0 +1,4 @@ +--- +title: Add Human Readable format for rake backup +merge_request: 7188 +author: David Gerő diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 17485b11c09..f42bb6a81a2 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -353,7 +353,7 @@ restore: ```shell # This command will overwrite the contents of your GitLab database! -sudo gitlab-rake gitlab:backup:restore BACKUP=1393513186 +sudo gitlab-rake gitlab:backup:restore BACKUP=1393513186_2014_02_27 ``` Restart and check GitLab: diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 0dfffaf0bc6..96c20100541 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -14,7 +14,7 @@ module Backup s[:gitlab_version] = Gitlab::VERSION s[:tar_version] = tar_version s[:skipped] = ENV["SKIP"] - tar_file = "#{s[:backup_created_at].to_i}_gitlab_backup.tar" + tar_file = s[:backup_created_at].strftime('%s_%Y_%m_%d') + '_gitlab_backup.tar' Dir.chdir(Gitlab.config.backup.path) do File.open("#{Gitlab.config.backup.path}/backup_information.yml", @@ -83,10 +83,14 @@ module Backup 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/ } - file_list.sort.each do |timestamp| - if Time.at(timestamp) < (Time.now - keep_time) - if Kernel.system(*%W(rm #{timestamp}_gitlab_backup.tar)) + file_list.map! do |path_string| + if path_string =~ /(\d+)(?:_\d{4}_\d{2}_\d{2})?_gitlab_backup\.tar/ + { timestamp: $1.to_i, path: path_string } + end + end + file_list.sort.each do |file| + if Time.at(file[:timestamp]) < (Time.now - keep_time) + if Kernel.system(*%W(rm #{file[:path]})) removed += 1 end end @@ -103,7 +107,7 @@ module Backup Dir.chdir(Gitlab.config.backup.path) # check for existing backups in the backup dir - file_list = Dir.glob("*_gitlab_backup.tar").each.map { |f| f.split(/_/).first.to_i } + file_list = Dir.glob("*_gitlab_backup.tar") puts "no backups found" if file_list.count == 0 if file_list.count > 1 && ENV["BACKUP"].nil? @@ -112,7 +116,7 @@ module Backup exit 1 end - tar_file = ENV["BACKUP"].nil? ? File.join("#{file_list.first}_gitlab_backup.tar") : File.join(ENV["BACKUP"] + "_gitlab_backup.tar") + tar_file = ENV["BACKUP"].nil? ? file_list.first : file_list.grep(ENV['BACKUP']).first unless File.exist?(tar_file) puts "The specified backup doesn't exist!" diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 287d83344db..2b244c13961 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -333,4 +333,35 @@ describe 'gitlab:app namespace rake task' do expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error end end + + describe "Human Readable Backup Name" do + def tars_glob + Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar')) + end + + before :all do + @origin_cd = Dir.pwd + + reenable_backup_sub_tasks + + FileUtils.rm tars_glob + + # Redirect STDOUT and run the rake task + orig_stdout = $stdout + $stdout = StringIO.new + run_rake_task('gitlab:backup:create') + $stdout = orig_stdout + + @backup_tar = tars_glob.first + end + + after :all do + FileUtils.rm(@backup_tar) + Dir.chdir @origin_cd + end + + it 'name has human readable time' do + expect(@backup_tar).to match(/\d+_\d{4}_\d{2}_\d{2}_gitlab_backup.tar$/) + end + end end # gitlab:app namespace -- cgit v1.2.1 From 34053a49703613d08bab7010a970b0ea8096ebad Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 29 Nov 2016 16:40:03 +0000 Subject: Fixed GFM autocomplete regex Without this fix it is possible that the autocomplete popup will stay open with the very first match, no matter if there is a match further in the string. Closes #25119 --- app/assets/javascripts/gfm_auto_complete.js.es6 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 89fe13b7a45..10769b7fd4f 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -59,12 +59,13 @@ // Tweaked to commands to start without a space only if char before is a non-word character // https://github.com/ichord/At.js var _a, _y, regexp, match; + subtext = subtext.split(' ').pop(); flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); _a = decodeURI("%C3%80"); _y = decodeURI("%C3%BF"); - regexp = new RegExp("(?:\\B|\\W|\\s)" + flag + "([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)|([^\\x00-\\xff]*)$", 'gi'); + regexp = new RegExp("(?:\\B|\\W|\\s)" + flag + "(?!\\W)([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)|([^\\x00-\\xff]*)$", 'gi'); match = regexp.exec(subtext); -- cgit v1.2.1 From b193e8497444a19e4ea541f73f82eb7e21aa8879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Tue, 29 Nov 2016 19:21:25 +0100 Subject: Move task helpers to a module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- doc/install/installation.md | 8 +- doc/update/8.14-to-8.15.md | 2 +- lib/tasks/gitlab/helpers.rake | 8 + lib/tasks/gitlab/task_helpers.rake | 191 --------------------- lib/tasks/gitlab/task_helpers.rb | 190 ++++++++++++++++++++ lib/tasks/gitlab/workhorse.rake | 2 +- spec/rake_helper.rb | 2 +- spec/support/rake_helpers.rb | 4 +- spec/support/stub_configuration.rb | 4 - spec/tasks/gitlab/backup_rake_spec.rb | 2 +- .../gitlab/mail_google_schema_whitelisting.rb | 2 +- spec/tasks/gitlab/task_helpers_spec.rb | 89 +++++----- spec/tasks/gitlab/users_rake_spec.rb | 2 +- spec/tasks/gitlab/workhorse_rake_spec.rb | 16 +- 14 files changed, 262 insertions(+), 260 deletions(-) create mode 100644 lib/tasks/gitlab/helpers.rake delete mode 100644 lib/tasks/gitlab/task_helpers.rake create mode 100644 lib/tasks/gitlab/task_helpers.rb diff --git a/doc/install/installation.md b/doc/install/installation.md index de650f0900d..77adb4c9f7b 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -397,24 +397,24 @@ GitLab Shell is an SSH access and repository management software developed speci ### Install gitlab-workhorse GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/). The -following command-line will install GitLab-Workhorse in `home/git/gitlab-workhorse` +following command-line will install GitLab-Workhorse in `/home/git/gitlab-workhorse` which is the recommended location. cd /home/git/gitlab - sudo -u git -H bundle exec rake gitlab:workhorse:install[/home/git/gitlab-workhorse] RAILS_ENV=production + sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production You can specify a different Git repository by providing `GITLAB_WORKHORSE_REPO`: cd /home/git/gitlab - sudo -u git -H bundle exec rake gitlab:workhorse:install[/home/git/gitlab-workhorse] GITLAB_WORKHORSE_REPO=https://example.com/gitlab-workhorse.git RAILS_ENV=production + sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" GITLAB_WORKHORSE_REPO=https://example.com/gitlab-workhorse.git RAILS_ENV=production You can specify a different version to use by providing `GITLAB_WORKHORSE_VERSION`: cd /home/git/gitlab - sudo -u git -H bundle exec rake gitlab:workhorse:install[/home/git/gitlab-workhorse] GITLAB_WORKHORSE_VERSION=0.8.1 RAILS_ENV=production + sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" GITLAB_WORKHORSE_VERSION=0.8.1 RAILS_ENV=production ### Initialize Database and Activate Advanced Features diff --git a/doc/update/8.14-to-8.15.md b/doc/update/8.14-to-8.15.md index 9e6a3b45394..576b943b98c 100644 --- a/doc/update/8.14-to-8.15.md +++ b/doc/update/8.14-to-8.15.md @@ -82,7 +82,7 @@ Install and compile gitlab-workhorse. This requires GitLab 8.1. ```bash -sudo -u git -H bundle exec rake gitlab:workhorse:install[/home/git/gitlab-workhorse] RAILS_ENV=production +sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production ``` ### 7. Install libs, migrations, etc. diff --git a/lib/tasks/gitlab/helpers.rake b/lib/tasks/gitlab/helpers.rake new file mode 100644 index 00000000000..dd2d5861481 --- /dev/null +++ b/lib/tasks/gitlab/helpers.rake @@ -0,0 +1,8 @@ +require 'tasks/gitlab/task_helpers' + +# Prevent StateMachine warnings from outputting during a cron task +StateMachines::Machine.ignore_method_conflicts = true if ENV['CRON'] + +namespace :gitlab do + include Gitlab::TaskHelpers +end diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake deleted file mode 100644 index c0759b96602..00000000000 --- a/lib/tasks/gitlab/task_helpers.rake +++ /dev/null @@ -1,191 +0,0 @@ -module Gitlab - class TaskFailedError < StandardError; end - class TaskAbortedByUserError < StandardError; end -end - -require 'rainbow/ext/string' - -# Prevent StateMachine warnings from outputting during a cron task -StateMachines::Machine.ignore_method_conflicts = true if ENV['CRON'] - -namespace :gitlab do - - # Ask if the user wants to continue - # - # Returns "yes" the user chose to continue - # Raises Gitlab::TaskAbortedByUserError if the user chose *not* to continue - def ask_to_continue - answer = prompt("Do you want to continue (yes/no)? ".color(:blue), %w{yes no}) - raise Gitlab::TaskAbortedByUserError unless answer == "yes" - end - - # Check which OS is running - # - # It will primarily use lsb_relase to determine the OS. - # It has fallbacks to Debian, SuSE, OS X and systems running systemd. - def os_name - os_name = run_command(%W(lsb_release -irs)) - os_name ||= if File.readable?('/etc/system-release') - File.read('/etc/system-release') - end - os_name ||= if File.readable?('/etc/debian_version') - debian_version = File.read('/etc/debian_version') - "Debian #{debian_version}" - end - os_name ||= if File.readable?('/etc/SuSE-release') - File.read('/etc/SuSE-release') - end - os_name ||= if os_x_version = run_command(%W(sw_vers -productVersion)) - "Mac OS X #{os_x_version}" - end - os_name ||= if File.readable?('/etc/os-release') - File.read('/etc/os-release').match(/PRETTY_NAME=\"(.+)\"/)[1] - end - os_name.try(:squish!) - end - - # Prompt the user to input something - # - # message - the message to display before input - # choices - array of strings of acceptable answers or nil for any answer - # - # Returns the user's answer - def prompt(message, choices = nil) - begin - print(message) - answer = STDIN.gets.chomp - end while choices.present? && !choices.include?(answer) - answer - end - - # Runs the given command and matches the output against the given pattern - # - # Returns nil if nothing matched - # Returns the MatchData if the pattern matched - # - # see also #run_command - # see also String#match - def run_and_match(command, regexp) - run_command(command).try(:match, regexp) - end - - # Runs the given command - # - # Returns '' if the command was not found - # Returns the output of the command otherwise - # - # see also #run_and_match - def run_command(command) - output, _ = Gitlab::Popen.popen(command) - output - rescue Errno::ENOENT - '' # if the command does not exist, return an empty string - end - - # Runs the given command and raise a Gitlab::TaskFailedError exception if - # the command does not exit with 0 - # - # Returns the output of the command otherwise - def run_command!(command) - output, status = Gitlab::Popen.popen(command) - - raise Gitlab::TaskFailedError unless status.zero? - - output - end - - def uid_for(user_name) - run_command(%W(id -u #{user_name})).chomp.to_i - end - - def gid_for(group_name) - begin - Etc.getgrnam(group_name).gid - rescue ArgumentError # no group - "group #{group_name} doesn't exist" - end - end - - def warn_user_is_not_gitlab - unless @warned_user_not_gitlab - gitlab_user = Gitlab.config.gitlab.user - current_user = run_command(%W(whoami)).chomp - unless current_user == gitlab_user - puts " Warning ".color(:black).background(:yellow) - puts " You are running as user #{current_user.color(:magenta)}, we hope you know what you are doing." - puts " Things may work\/fail for the wrong reasons." - puts " For correct results you should run this as user #{gitlab_user.color(:magenta)}." - puts "" - end - @warned_user_not_gitlab = true - end - end - - # Tries to configure git itself - # - # Returns true if all subcommands were successfull (according to their exit code) - # Returns false if any or all subcommands failed. - def auto_fix_git_config(options) - if !@warned_user_not_gitlab - command_success = options.map do |name, value| - system(*%W(#{Gitlab.config.git.bin_path} config --global #{name} #{value})) - end - - command_success.all? - else - false - end - end - - def all_repos - Gitlab.config.repositories.storages.each do |name, path| - IO.popen(%W(find #{path} -mindepth 2 -maxdepth 2 -type d -name *.git)) do |find| - find.each_line do |path| - yield path.chomp - end - end - end - end - - def repository_storage_paths_args - Gitlab.config.repositories.storages.values - end - - def user_home - Rails.env.test? ? Rails.root.join('tmp/tests') : Gitlab.config.gitlab.user_home - end - - def checkout_or_clone_tag(tag:, repo:, target_dir:) - if Dir.exist?(target_dir) - Dir.chdir(target_dir) do - run_command!(%W[#{Gitlab.config.git.bin_path} fetch --tags --quiet]) - run_command!(%W[#{Gitlab.config.git.bin_path} checkout --quiet #{tag}]) - end - else - run_command!(%W[#{Gitlab.config.git.bin_path} clone -- #{repo} #{target_dir}]) - end - - # Make sure we're on the right tag - Dir.chdir(target_dir) do - # First try to checkout without fetching - # to avoid stalling tests if the Internet is down. - reset_to_tag(tag) - end - end - - def reset_to_tag(tag_wanted) - tag = - begin - run_command!(%W[#{Gitlab.config.git.bin_path} describe -- #{tag_wanted}]) - rescue Gitlab::TaskFailedError - run_command!(%W[#{Gitlab.config.git.bin_path} fetch origin]) - run_command!(%W[#{Gitlab.config.git.bin_path} describe -- origin/#{tag_wanted}]) - end - - if tag - run_command!(%W[#{Gitlab.config.git.bin_path} reset --hard #{tag.strip}]) - else - raise Gitlab::TaskFailedError - end - end -end diff --git a/lib/tasks/gitlab/task_helpers.rb b/lib/tasks/gitlab/task_helpers.rb new file mode 100644 index 00000000000..e128738b5f8 --- /dev/null +++ b/lib/tasks/gitlab/task_helpers.rb @@ -0,0 +1,190 @@ +require 'rainbow/ext/string' + +module Gitlab + TaskFailedError = Class.new(StandardError) + TaskAbortedByUserError = Class.new(StandardError) + + module TaskHelpers + # Ask if the user wants to continue + # + # Returns "yes" the user chose to continue + # Raises Gitlab::TaskAbortedByUserError if the user chose *not* to continue + def ask_to_continue + answer = prompt("Do you want to continue (yes/no)? ".color(:blue), %w{yes no}) + raise Gitlab::TaskAbortedByUserError unless answer == "yes" + end + + # Check which OS is running + # + # It will primarily use lsb_relase to determine the OS. + # It has fallbacks to Debian, SuSE, OS X and systems running systemd. + def os_name + os_name = run_command(%W(lsb_release -irs)) + os_name ||= if File.readable?('/etc/system-release') + File.read('/etc/system-release') + end + os_name ||= if File.readable?('/etc/debian_version') + debian_version = File.read('/etc/debian_version') + "Debian #{debian_version}" + end + os_name ||= if File.readable?('/etc/SuSE-release') + File.read('/etc/SuSE-release') + end + os_name ||= if os_x_version = run_command(%W(sw_vers -productVersion)) + "Mac OS X #{os_x_version}" + end + os_name ||= if File.readable?('/etc/os-release') + File.read('/etc/os-release').match(/PRETTY_NAME=\"(.+)\"/)[1] + end + os_name.try(:squish!) + end + + # Prompt the user to input something + # + # message - the message to display before input + # choices - array of strings of acceptable answers or nil for any answer + # + # Returns the user's answer + def prompt(message, choices = nil) + begin + print(message) + answer = STDIN.gets.chomp + end while choices.present? && !choices.include?(answer) + answer + end + + # Runs the given command and matches the output against the given pattern + # + # Returns nil if nothing matched + # Returns the MatchData if the pattern matched + # + # see also #run_command + # see also String#match + def run_and_match(command, regexp) + run_command(command).try(:match, regexp) + end + + # Runs the given command + # + # Returns '' if the command was not found + # Returns the output of the command otherwise + # + # see also #run_and_match + def run_command(command) + output, _ = Gitlab::Popen.popen(command) + output + rescue Errno::ENOENT + '' # if the command does not exist, return an empty string + end + + # Runs the given command and raises a Gitlab::TaskFailedError exception if + # the command does not exit with 0 + # + # Returns the output of the command otherwise + def run_command!(command) + output, status = Gitlab::Popen.popen(command) + + raise Gitlab::TaskFailedError unless status.zero? + + output + end + + def uid_for(user_name) + run_command(%W(id -u #{user_name})).chomp.to_i + end + + def gid_for(group_name) + begin + Etc.getgrnam(group_name).gid + rescue ArgumentError # no group + "group #{group_name} doesn't exist" + end + end + + def warn_user_is_not_gitlab + unless @warned_user_not_gitlab + gitlab_user = Gitlab.config.gitlab.user + current_user = run_command(%W(whoami)).chomp + unless current_user == gitlab_user + puts " Warning ".color(:black).background(:yellow) + puts " You are running as user #{current_user.color(:magenta)}, we hope you know what you are doing." + puts " Things may work\/fail for the wrong reasons." + puts " For correct results you should run this as user #{gitlab_user.color(:magenta)}." + puts "" + end + @warned_user_not_gitlab = true + end + end + + # Tries to configure git itself + # + # Returns true if all subcommands were successfull (according to their exit code) + # Returns false if any or all subcommands failed. + def auto_fix_git_config(options) + if !@warned_user_not_gitlab + command_success = options.map do |name, value| + system(*%W(#{Gitlab.config.git.bin_path} config --global #{name} #{value})) + end + + command_success.all? + else + false + end + end + + def all_repos + Gitlab.config.repositories.storages.each do |name, path| + IO.popen(%W(find #{path} -mindepth 2 -maxdepth 2 -type d -name *.git)) do |find| + find.each_line do |path| + yield path.chomp + end + end + end + end + + def repository_storage_paths_args + Gitlab.config.repositories.storages.values + end + + def user_home + Rails.env.test? ? Rails.root.join('tmp/tests') : Gitlab.config.gitlab.user_home + end + + def checkout_or_clone_tag(tag:, repo:, target_dir:) + if Dir.exist?(target_dir) + checkout_tag(tag, target_dir) + else + clone_repo(repo, target_dir) + end + + reset_to_tag(tag, target_dir) + end + + def clone_repo(repo, target_dir) + run_command!(%W[#{Gitlab.config.git.bin_path} clone -- #{repo} #{target_dir}]) + end + + def checkout_tag(tag, target_dir) + run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch --tags --quiet]) + run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} checkout --quiet #{tag}]) + end + + def reset_to_tag(tag_wanted, target_dir) + tag = + begin + # First try to checkout without fetching + # to avoid stalling tests if the Internet is down. + run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- #{tag_wanted}]) + rescue Gitlab::TaskFailedError + run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch origin]) + run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- origin/#{tag_wanted}]) + end + + if tag + run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} reset --hard #{tag.strip}]) + else + raise Gitlab::TaskFailedError + end + end + end +end diff --git a/lib/tasks/gitlab/workhorse.rake b/lib/tasks/gitlab/workhorse.rake index 563744dd0c7..46bd0bf2e7b 100644 --- a/lib/tasks/gitlab/workhorse.rake +++ b/lib/tasks/gitlab/workhorse.rake @@ -4,7 +4,7 @@ namespace :gitlab do task :install, [:dir] => :environment do |t, args| warn_user_is_not_gitlab unless args.dir.present? - abort "Please specify the directory where you want to install gitlab-workhorse:\n rake gitlab:workhorse:install[/home/git/gitlab-workhorse]" + abort %(Please specify the directory where you want to install gitlab-workhorse:\n rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]") end tag = "v#{ENV['GITLAB_WORKHORSE_VERSION'] || Gitlab::Workhorse.version}" diff --git a/spec/rake_helper.rb b/spec/rake_helper.rb index 9b5b4bf9fea..298a520f5ca 100644 --- a/spec/rake_helper.rb +++ b/spec/rake_helper.rb @@ -8,7 +8,7 @@ RSpec.configure do |config| config.before(:all) do $stdout = StringIO.new - Rake.application.rake_require 'tasks/gitlab/task_helpers' + Rake.application.rake_require 'tasks/gitlab/helpers' Rake::Task.define_task :environment end diff --git a/spec/support/rake_helpers.rb b/spec/support/rake_helpers.rb index 52d80c69835..4a8158ed79b 100644 --- a/spec/support/rake_helpers.rb +++ b/spec/support/rake_helpers.rb @@ -1,7 +1,7 @@ module RakeHelpers - def run_rake_task(task_name) + def run_rake_task(task_name, *args) Rake::Task[task_name].reenable - Rake.application.invoke_task task_name + Rake.application.invoke_task("#{task_name}[#{args.join(',')}]") end def stub_warn_user_is_not_gitlab diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb index c0847bbd7f1..f40ee862df8 100644 --- a/spec/support/stub_configuration.rb +++ b/spec/support/stub_configuration.rb @@ -17,10 +17,6 @@ module StubConfiguration allow(Gitlab.config.gravatar).to receive_messages(messages) end - def stub_gitlab_workhorse_setting(messages) - allow(Gitlab.config.gitlab_workhorse).to receive_messages(messages) - end - def stub_incoming_email_setting(messages) allow(Gitlab.config.incoming_email).to receive_messages(messages) end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 287d83344db..ecbfc236d3d 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -5,7 +5,7 @@ describe 'gitlab:app namespace rake task' do let(:enable_registry) { true } before :all do - Rake.application.rake_require 'tasks/gitlab/task_helpers' + Rake.application.rake_require 'tasks/gitlab/helpers' Rake.application.rake_require 'tasks/gitlab/backup' Rake.application.rake_require 'tasks/gitlab/shell' Rake.application.rake_require 'tasks/gitlab/db' diff --git a/spec/tasks/gitlab/mail_google_schema_whitelisting.rb b/spec/tasks/gitlab/mail_google_schema_whitelisting.rb index 37feb5e6faf..80fc8c48fed 100644 --- a/spec/tasks/gitlab/mail_google_schema_whitelisting.rb +++ b/spec/tasks/gitlab/mail_google_schema_whitelisting.rb @@ -3,7 +3,7 @@ require 'rake' describe 'gitlab:mail_google_schema_whitelisting rake task' do before :all do - Rake.application.rake_require "tasks/gitlab/task_helpers" + Rake.application.rake_require "tasks/gitlab/helpers" Rake.application.rake_require "tasks/gitlab/mail_google_schema_whitelisting" # empty task as env is already loaded Rake::Task.define_task :environment diff --git a/spec/tasks/gitlab/task_helpers_spec.rb b/spec/tasks/gitlab/task_helpers_spec.rb index dccb3b4cf9a..86e42d845ce 100644 --- a/spec/tasks/gitlab/task_helpers_spec.rb +++ b/spec/tasks/gitlab/task_helpers_spec.rb @@ -1,86 +1,95 @@ require 'spec_helper' -require 'rake' +require 'tasks/gitlab/task_helpers' -describe 'gitlab:workhorse namespace rake task' do - before :all do - Rake.application.rake_require 'tasks/gitlab/task_helpers' +class TestHelpersTest + include Gitlab::TaskHelpers +end - # empty task as env is already loaded - Rake::Task.define_task :environment - end +describe Gitlab::TaskHelpers do + subject { TestHelpersTest.new } + + let(:repo) { 'https://gitlab.com/gitlab-org/gitlab-test.git' } + let(:clone_path) { Rails.root.join('tmp/tests/task_helpers_tests').to_s } + let(:tag) { 'v1.1.0' } describe '#checkout_or_clone_tag' do - let(:repo) { 'https://gitlab.com/gitlab-org/gitlab-test.git' } - let(:clone_path) { Rails.root.join('tmp/tests/task_helpers_tests').to_s } - let(:tag) { 'v1.1.0' } before do - FileUtils.rm_rf(clone_path) - allow_any_instance_of(Object).to receive(:run_command!) - expect_any_instance_of(Object).to receive(:reset_to_tag).with(tag) - end - - after do - FileUtils.rm_rf(clone_path) + allow(subject).to receive(:run_command!) + expect(subject).to receive(:reset_to_tag).with(tag, clone_path) end context 'target_dir does not exist' do it 'clones the repo, retrieve the tag from origin, and checkout the tag' do - expect(Dir).to receive(:chdir).and_call_original - expect_any_instance_of(Object). - to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} clone -- #{repo} #{clone_path}]) { FileUtils.mkdir_p(clone_path) } # Fake the cloning + expect(subject).to receive(:clone_repo).with(repo, clone_path) - checkout_or_clone_tag(tag: tag, repo: repo, target_dir: clone_path) + subject.checkout_or_clone_tag(tag: tag, repo: repo, target_dir: clone_path) end end context 'target_dir exists' do before do - FileUtils.mkdir_p(clone_path) + expect(Dir).to receive(:exist?).and_return(true) end it 'fetch and checkout the tag' do - expect(Dir).to receive(:chdir).twice.and_call_original - expect_any_instance_of(Object). - to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} fetch --tags --quiet]) - expect_any_instance_of(Object). - to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} checkout --quiet #{tag}]) + expect(subject).to receive(:checkout_tag).with(tag, clone_path) - checkout_or_clone_tag(tag: tag, repo: repo, target_dir: clone_path) + subject.checkout_or_clone_tag(tag: tag, repo: repo, target_dir: clone_path) end end end + describe '#clone_repo' do + it 'clones the repo in the target dir' do + expect(subject). + to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} clone -- #{repo} #{clone_path}]) + + subject.clone_repo(repo, clone_path) + end + end + + describe '#checkout_tag' do + it 'clones the repo in the target dir' do + expect(subject). + to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} fetch --tags --quiet]) + expect(subject). + to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} checkout --quiet #{tag}]) + + subject.checkout_tag(tag, clone_path) + end + end + describe '#reset_to_tag' do let(:tag) { 'v1.1.0' } before do - expect_any_instance_of(Object). - to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} reset --hard #{tag}]) + expect(subject). + to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} reset --hard #{tag}]) end context 'when the tag is not checked out locally' do before do - expect_any_instance_of(Object). - to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} describe -- #{tag}]).and_raise(Gitlab::TaskFailedError) + expect(subject). + to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} describe -- #{tag}]).and_raise(Gitlab::TaskFailedError) end it 'fetch origin, ensure the tag exists, and resets --hard to the given tag' do - expect_any_instance_of(Object). - to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} fetch origin]) - expect_any_instance_of(Object). - to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} describe -- origin/#{tag}]).and_return(tag) + expect(subject). + to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} fetch origin]) + expect(subject). + to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} describe -- origin/#{tag}]).and_return(tag) - reset_to_tag(tag) + subject.reset_to_tag(tag, clone_path) end end context 'when the tag is checked out locally' do before do - expect_any_instance_of(Object). - to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} describe -- #{tag}]).and_return(tag) + expect(subject). + to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} describe -- #{tag}]).and_return(tag) end it 'resets --hard to the given tag' do - reset_to_tag(tag) + subject.reset_to_tag(tag, clone_path) end end end diff --git a/spec/tasks/gitlab/users_rake_spec.rb b/spec/tasks/gitlab/users_rake_spec.rb index e6ebef82b78..972670e7f91 100644 --- a/spec/tasks/gitlab/users_rake_spec.rb +++ b/spec/tasks/gitlab/users_rake_spec.rb @@ -5,7 +5,7 @@ describe 'gitlab:users namespace rake task' do let(:enable_registry) { true } before :all do - Rake.application.rake_require 'tasks/gitlab/task_helpers' + Rake.application.rake_require 'tasks/gitlab/helpers' Rake.application.rake_require 'tasks/gitlab/users' # empty task as env is already loaded diff --git a/spec/tasks/gitlab/workhorse_rake_spec.rb b/spec/tasks/gitlab/workhorse_rake_spec.rb index 87bc1b128bf..b695abce091 100644 --- a/spec/tasks/gitlab/workhorse_rake_spec.rb +++ b/spec/tasks/gitlab/workhorse_rake_spec.rb @@ -1,18 +1,8 @@ -require 'spec_helper' -require 'rake' +require 'rake_helper' describe 'gitlab:workhorse namespace rake task' do before :all do - Rake.application.rake_require 'tasks/gitlab/task_helpers' Rake.application.rake_require 'tasks/gitlab/workhorse' - - # empty task as env is already loaded - Rake::Task.define_task :environment - end - - def run_rake_task(task_name, *args) - Rake::Task[task_name].reenable - Rake.application.invoke_task("#{task_name}[#{args.join(',')}]") end describe 'install' do @@ -20,13 +10,13 @@ describe 'gitlab:workhorse namespace rake task' do let(:clone_path) { Rails.root.join('tmp/tests/gitlab-workhorse').to_s } let(:tag) { "v#{File.read(Rails.root.join(Gitlab::Workhorse::VERSION_FILE)).chomp}" } before do - # avoid writing task output to spec progress - allow($stdout).to receive :write allow(ENV).to receive(:[]) end context 'no dir given' do it 'aborts and display a help message' do + # avoid writing task output to spec progress + allow($stderr).to receive :write expect { run_rake_task('gitlab:workhorse:install') }.to raise_error /Please specify the directory where you want to install gitlab-workhorse/ end end -- cgit v1.2.1 From dd5f71138ce98522b1324319fbd60f665b3d1337 Mon Sep 17 00:00:00 2001 From: Robert Schilling <rschilling@student.tugraz.at> Date: Mon, 28 Nov 2016 22:15:12 +0100 Subject: Grapify the files API --- doc/api/repository_files.md | 4 +- lib/api/files.rb | 153 +++++++++++++++++--------------------------- 2 files changed, 62 insertions(+), 95 deletions(-) diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md index 1bc6a24e914..b8c9eb2c9a8 100644 --- a/doc/api/repository_files.md +++ b/doc/api/repository_files.md @@ -60,7 +60,7 @@ Parameters: - `file_path` (required) - Full path to new file. Ex. lib/class.rb - `branch_name` (required) - The name of branch -- `encoding` (optional) - 'text' or 'base64'. Text is default. +- `encoding` (optional) - Change encoding to 'base64'. Default is text. - `author_email` (optional) - Specify the commit author's email address - `author_name` (optional) - Specify the commit author's name - `content` (required) - File content @@ -89,7 +89,7 @@ Parameters: - `file_path` (required) - Full path to file. Ex. lib/class.rb - `branch_name` (required) - The name of branch -- `encoding` (optional) - 'text' or 'base64'. Text is default. +- `encoding` (optional) - Change encoding to 'base64'. Default is text. - `author_email` (optional) - Specify the commit author's email address - `author_name` (optional) - Specify the commit author's name - `content` (required) - New file content diff --git a/lib/api/files.rb b/lib/api/files.rb index 96510e651a3..28f306e45f3 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -23,140 +23,107 @@ module API branch_name: attrs[:branch_name] } end + + params :simple_file_params do + requires :file_path, type: String, desc: 'The path to new file. Ex. lib/class.rb' + requires :branch_name, type: String, desc: 'The name of branch' + requires :commit_message, type: String, desc: 'Commit Message' + optional :author_email, type: String, desc: 'The email of the author' + optional :author_name, type: String, desc: 'The name of the author' + end + + params :extended_file_params do + use :simple_file_params + requires :content, type: String, desc: 'File content' + optional :encoding, type: String, values: %w[base64], desc: 'File encoding' + end end + params do + requires :id, type: String, desc: 'The project ID' + end resource :projects do - # Get file from repository - # File content is Base64 encoded - # - # Parameters: - # file_path (required) - The path to the file. Ex. lib/class.rb - # ref (required) - The name of branch, tag or commit - # - # Example Request: - # GET /projects/:id/repository/files - # - # Example response: - # { - # "file_name": "key.rb", - # "file_path": "app/models/key.rb", - # "size": 1476, - # "encoding": "base64", - # "content": "IyA9PSBTY2hlbWEgSW5mb3...", - # "ref": "master", - # "blob_id": "79f7bbd25901e8334750839545a9bd021f0e4c83", - # "commit_id": "d5a3ff139356ce33e37e73add446f16869741b50", - # "last_commit_id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d", - # } - # + desc 'Get a file from repository' + params do + requires :file_path, type: String, desc: 'The path to the file. Ex. lib/class.rb' + requires :ref, type: String, desc: 'The name of branch, tag, or commit' + end get ":id/repository/files" do authorize! :download_code, user_project - required_attributes! [:file_path, :ref] - attrs = attributes_for_keys [:file_path, :ref] - ref = attrs.delete(:ref) - file_path = attrs.delete(:file_path) - - commit = user_project.commit(ref) - not_found! 'Commit' unless commit + commit = user_project.commit(params[:ref]) + not_found!('Commit') unless commit repo = user_project.repository - blob = repo.blob_at(commit.sha, file_path) + blob = repo.blob_at(commit.sha, params[:file_path]) + not_found!('File') unless blob - if blob - blob.load_all_data!(repo) - status(200) + blob.load_all_data!(repo) + status(200) - { - file_name: blob.name, - file_path: blob.path, - size: blob.size, - encoding: "base64", - content: Base64.strict_encode64(blob.data), - ref: ref, - blob_id: blob.id, - commit_id: commit.id, - last_commit_id: repo.last_commit_for_path(commit.sha, file_path).id - } - else - not_found! 'File' - end + { + file_name: blob.name, + file_path: blob.path, + size: blob.size, + encoding: "base64", + content: Base64.strict_encode64(blob.data), + ref: params[:ref], + blob_id: blob.id, + commit_id: commit.id, + last_commit_id: repo.last_commit_for_path(commit.sha, params[:file_path]).id + } end - # Create new file in repository - # - # Parameters: - # file_path (required) - The path to new file. Ex. lib/class.rb - # branch_name (required) - The name of branch - # content (required) - File content - # commit_message (required) - Commit message - # - # Example Request: - # POST /projects/:id/repository/files - # + desc 'Create new file in repository' + params do + use :extended_file_params + end post ":id/repository/files" do authorize! :push_code, user_project - required_attributes! [:file_path, :branch_name, :content, :commit_message] - attrs = attributes_for_keys [:file_path, :branch_name, :content, :commit_message, :encoding, :author_email, :author_name] - result = ::Files::CreateService.new(user_project, current_user, commit_params(attrs)).execute + file_params = declared_params(include_missing: false) + result = ::Files::CreateService.new(user_project, current_user, commit_params(file_params)).execute if result[:status] == :success status(201) - commit_response(attrs) + commit_response(file_params) else render_api_error!(result[:message], 400) end end - # Update existing file in repository - # - # Parameters: - # file_path (optional) - The path to file. Ex. lib/class.rb - # branch_name (required) - The name of branch - # content (required) - File content - # commit_message (required) - Commit message - # - # Example Request: - # PUT /projects/:id/repository/files - # + desc 'Update existing file in repository' + params do + use :extended_file_params + end put ":id/repository/files" do authorize! :push_code, user_project - required_attributes! [:file_path, :branch_name, :content, :commit_message] - attrs = attributes_for_keys [:file_path, :branch_name, :content, :commit_message, :encoding, :author_email, :author_name] - result = ::Files::UpdateService.new(user_project, current_user, commit_params(attrs)).execute + file_params = declared_params(include_missing: false) + result = ::Files::UpdateService.new(user_project, current_user, commit_params(file_params)).execute if result[:status] == :success status(200) - commit_response(attrs) + commit_response(file_params) else http_status = result[:http_status] || 400 render_api_error!(result[:message], http_status) end end - # Delete existing file in repository - # - # Parameters: - # file_path (optional) - The path to file. Ex. lib/class.rb - # branch_name (required) - The name of branch - # content (required) - File content - # commit_message (required) - Commit message - # - # Example Request: - # DELETE /projects/:id/repository/files - # + desc 'Delete an existing file in repository' + params do + use :simple_file_params + end delete ":id/repository/files" do authorize! :push_code, user_project - required_attributes! [:file_path, :branch_name, :commit_message] - attrs = attributes_for_keys [:file_path, :branch_name, :commit_message, :author_email, :author_name] - result = ::Files::DeleteService.new(user_project, current_user, commit_params(attrs)).execute + file_params = declared_params(include_missing: false) + result = ::Files::DeleteService.new(user_project, current_user, commit_params(file_params)).execute if result[:status] == :success status(200) - commit_response(attrs) + commit_response(file_params) else render_api_error!(result[:message], 400) end -- cgit v1.2.1 From 2ce66c071fc7ab2b8ca881223321a3927ec7d61e Mon Sep 17 00:00:00 2001 From: Robert Schilling <rschilling@student.tugraz.at> Date: Mon, 28 Nov 2016 19:16:15 +0100 Subject: API: Expose branch status --- changelogs/unreleased/api-branch-status.yml | 4 ++++ doc/api/branches.md | 5 +++++ lib/api/entities.rb | 6 +++++- spec/requests/api/branches_spec.rb | 11 +++++++++++ 4 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/api-branch-status.yml diff --git a/changelogs/unreleased/api-branch-status.yml b/changelogs/unreleased/api-branch-status.yml new file mode 100644 index 00000000000..c5763345a22 --- /dev/null +++ b/changelogs/unreleased/api-branch-status.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Expose merge status for branch API' +merge_request: +author: Robert Schilling diff --git a/doc/api/branches.md b/doc/api/branches.md index 07dfa5d4d7f..ffcfea41453 100644 --- a/doc/api/branches.md +++ b/doc/api/branches.md @@ -22,6 +22,7 @@ Example response: [ { "name": "master", + "merged": false, "protected": true, "developers_can_push": false, "developers_can_merge": false, @@ -65,6 +66,7 @@ Example response: ```json { "name": "master", + "merged": false, "protected": true, "developers_can_push": false, "developers_can_merge": false, @@ -123,6 +125,7 @@ Example response: ] }, "name": "master", + "merged": false, "protected": true, "developers_can_push": true, "developers_can_merge": true @@ -166,6 +169,7 @@ Example response: ] }, "name": "master", + "merged": false, "protected": false, "developers_can_push": false, "developers_can_merge": false @@ -206,6 +210,7 @@ Example response: ] }, "name": "newbranch", + "merged": false, "protected": false, "developers_can_push": false, "developers_can_merge": false diff --git a/lib/api/entities.rb b/lib/api/entities.rb index fdb19558c1c..d5dfb8d00be 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -141,8 +141,12 @@ module API options[:project].repository.commit(repo_branch.dereferenced_target) end + expose :merged do |repo_branch, options| + options[:project].repository.merged_to_root_ref?(repo_branch.name) + end + expose :protected do |repo_branch, options| - options[:project].protected_branch? repo_branch.name + options[:project].protected_branch?(repo_branch.name) end expose :developers_can_push do |repo_branch, options| diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index fe6b875b997..28fbae18de1 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -31,11 +31,22 @@ describe API::API, api: true do expect(json_response['name']).to eq(branch_name) expect(json_response['commit']['id']).to eq(branch_sha) + expect(json_response['merged']).to eq(false) expect(json_response['protected']).to eq(false) expect(json_response['developers_can_push']).to eq(false) expect(json_response['developers_can_merge']).to eq(false) end + context 'on a merged branch' do + it "returns the branch information for a single branch" do + get api("/projects/#{project.id}/repository/branches/merge-test", user) + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq('merge-test') + expect(json_response['merged']).to eq(true) + end + end + it "returns a 403 error if guest" do get api("/projects/#{project.id}/repository/branches", user2) expect(response).to have_http_status(403) -- cgit v1.2.1 From edc97c9dc6c3fa43778068f2067cb5911e7ee3f0 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Tue, 29 Nov 2016 17:40:16 +0000 Subject: Use created date from last_deployment Adds test Adds changelog entry --- .../environments/components/environment_item.js.es6 | 10 ++++++++-- changelogs/unreleased/24844-environments-date.yml | 4 ++++ spec/javascripts/environments/environment_item_spec.js.es6 | 14 ++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/24844-environments-date.yml diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 07f49cce3dc..a3ba3c762d7 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -147,6 +147,12 @@ this.model.last_deployment.deployable; }, + canShowDate() { + return this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.created_at; + }, + /** * Human readable date. * @@ -155,7 +161,7 @@ createdDate() { const timeagoInstance = new timeago(); // eslint-disable-line - return timeagoInstance.format(this.model.created_at); + return timeagoInstance.format(this.model.last_deployment.deployable.created_at); }, /** @@ -453,7 +459,7 @@ <td> <span - v-if="!isFolder && model.last_deployment" + v-if="!isFolder && canShowDate" class="environment-created-date-timeago"> {{createdDate}} </span> diff --git a/changelogs/unreleased/24844-environments-date.yml b/changelogs/unreleased/24844-environments-date.yml new file mode 100644 index 00000000000..2bc23d40a68 --- /dev/null +++ b/changelogs/unreleased/24844-environments-date.yml @@ -0,0 +1,4 @@ +--- +title: Fixes Environments displaying incorrect date since 8.14 upgrade +merge_request: +author: diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6 index 14e90a9dd1b..3e2fa730c8d 100644 --- a/spec/javascripts/environments/environment_item_spec.js.es6 +++ b/spec/javascripts/environments/environment_item_spec.js.es6 @@ -1,4 +1,5 @@ //= require vue +//= require timeago //= require environments/components/environment_item describe('Environment item', () => { @@ -109,6 +110,8 @@ describe('Environment item', () => { name: 'deploy', build_path: '/root/ci-folders/builds/1279', retry_path: '/root/ci-folders/builds/1279/retry', + created_at: '2016-11-29T18:11:58.430Z', + updated_at: '2016-11-29T18:11:58.430Z', }, manual_actions: [ { @@ -149,6 +152,17 @@ describe('Environment item', () => { ).toContain('#'); }); + it('should render last deployment date', () => { + const timeagoInstance = new timeago(); // eslint-disable-line + const formatedDate = timeagoInstance.format( + environment.last_deployment.deployable.created_at + ); + + expect( + component.$el.querySelector('.environment-created-date-timeago').textContent + ).toContain(formatedDate); + }); + describe('With user information', () => { it('should render user avatar with link to profile', () => { expect( -- cgit v1.2.1 From 54e628740417a2f7fc0f8d77db095ff31798a335 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Tue, 29 Nov 2016 18:54:34 +0000 Subject: Improvements after review --- .../environments/components/environment_item.js.es6 | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index a3ba3c762d7..c842b9e418f 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -23,6 +23,7 @@ window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; + window.gl.environmentsList.timeagoInstance = new timeago(); // eslint-disable-line gl.environmentsList.EnvironmentItem = Vue.component('environment-item', { @@ -147,10 +148,15 @@ this.model.last_deployment.deployable; }, + /** + * Verifies if the date to be shown is present. + * + * @returns {Boolean|Undefined} + */ canShowDate() { return this.model.last_deployment && this.model.last_deployment.deployable && - this.model.last_deployment.deployable.created_at; + this.model.last_deployment.deployable !== undefined; }, /** @@ -159,9 +165,9 @@ * @returns {String} */ createdDate() { - const timeagoInstance = new timeago(); // eslint-disable-line - - return timeagoInstance.format(this.model.last_deployment.deployable.created_at); + return window.gl.environmentsList.timeagoInstance.format( + this.model.last_deployment.deployable.created_at + ); }, /** -- cgit v1.2.1 From 4681ab1df1c3d05af58335dd065ae1834aab5d35 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Wed, 30 Nov 2016 14:08:51 +0100 Subject: Fix Rubocop offense in merge request specs --- spec/requests/api/merge_requests_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 1e25ae60eef..dce754fa1cb 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -465,7 +465,6 @@ describe API::API, api: true do expect(response).to have_http_status(200) end - it "enables merge when pipeline succeeds if the pipeline is active" do allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline) allow(pipeline).to receive(:active?).and_return(true) -- cgit v1.2.1 From cd5813ee21c3433fecbbe89185976843a77ad04d Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 30 Nov 2016 14:24:12 +0000 Subject: Fix comma-dangle in function's arguments errors --- .../javascripts/environments/components/environment_item.js.es6 | 2 +- spec/javascripts/environments/environment_item_spec.js.es6 | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index c842b9e418f..7ead8a18c2a 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -166,7 +166,7 @@ */ createdDate() { return window.gl.environmentsList.timeagoInstance.format( - this.model.last_deployment.deployable.created_at + this.model.last_deployment.deployable.created_at, ); }, diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6 index 3e2fa730c8d..5d7c6b2411d 100644 --- a/spec/javascripts/environments/environment_item_spec.js.es6 +++ b/spec/javascripts/environments/environment_item_spec.js.es6 @@ -155,11 +155,11 @@ describe('Environment item', () => { it('should render last deployment date', () => { const timeagoInstance = new timeago(); // eslint-disable-line const formatedDate = timeagoInstance.format( - environment.last_deployment.deployable.created_at + environment.last_deployment.deployable.created_at, ); expect( - component.$el.querySelector('.environment-created-date-timeago').textContent + component.$el.querySelector('.environment-created-date-timeago').textContent, ).toContain(formatedDate); }); -- cgit v1.2.1 From c347170853926a4ce0402fe1b647f578b3da620e Mon Sep 17 00:00:00 2001 From: Stan Hu <stanhu@gmail.com> Date: Wed, 30 Nov 2016 07:01:45 -0800 Subject: Revert bump in rufus-scheduler Somehow `bundle install` with an update to Sidekiq-cron caused rufus-scheduler to be bumped, when it doesn't appear absolutely necessary. Closes #25160 --- Gemfile.lock | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1db0e466164..5a14ed6fede 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -614,8 +614,7 @@ GEM rubyntlm (0.5.2) rubypants (0.2.0) rubyzip (1.2.0) - rufus-scheduler (3.3.0) - tzinfo + rufus-scheduler (3.1.10) rugged (0.24.0) safe_yaml (1.0.4) sanitize (2.1.0) -- cgit v1.2.1 From a59e75a17f52d2c71501b0f61686b3a546501d60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Wed, 30 Nov 2016 16:15:02 +0100 Subject: Make the downtime_check task happy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- db/migrate/20130319214458_create_forked_project_links.rb | 2 ++ db/migrate/20130506090604_create_deploy_keys_projects.rb | 2 ++ db/migrate/20130617095603_create_users_groups.rb | 2 ++ db/migrate/20130711063759_create_project_group_links.rb | 2 ++ db/migrate/20131112114325_create_broadcast_messages.rb | 2 ++ db/migrate/20140122112253_create_merge_request_diffs.rb | 2 ++ db/migrate/20140209025651_create_emails.rb | 4 +++- db/migrate/20140625115202_create_users_star_projects.rb | 2 ++ db/migrate/20140729134820_create_labels.rb | 2 ++ db/migrate/20140729140420_create_label_links.rb | 2 ++ db/migrate/20140914113604_add_members_table.rb | 2 ++ db/migrate/20140914173417_remove_old_member_tables.rb | 2 ++ db/migrate/20141118150935_add_audit_event.rb | 2 ++ db/migrate/20141216155758_create_doorkeeper_tables.rb | 2 ++ db/migrate/20150108073740_create_application_settings.rb | 2 ++ db/migrate/20150313012111_create_subscriptions_table.rb | 6 ++++-- db/migrate/20150806104937_create_abuse_reports.rb | 2 ++ db/migrate/20151103134857_create_lfs_objects.rb | 2 ++ db/migrate/20151103134958_create_lfs_objects_projects.rb | 2 ++ db/migrate/20151105094515_create_releases.rb | 2 ++ db/migrate/20160212123307_create_tasks.rb | 2 ++ db/migrate/20160416180807_add_award_emoji.rb | 2 ++ 22 files changed, 47 insertions(+), 3 deletions(-) diff --git a/db/migrate/20130319214458_create_forked_project_links.rb b/db/migrate/20130319214458_create_forked_project_links.rb index 41b0b700a6f..065a5e08243 100644 --- a/db/migrate/20130319214458_create_forked_project_links.rb +++ b/db/migrate/20130319214458_create_forked_project_links.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateForkedProjectLinks < ActiveRecord::Migration + DOWNTIME = false + def change create_table :forked_project_links do |t| t.integer :forked_to_project_id, null: false diff --git a/db/migrate/20130506090604_create_deploy_keys_projects.rb b/db/migrate/20130506090604_create_deploy_keys_projects.rb index f2e416d3b6f..8b9662a27c3 100644 --- a/db/migrate/20130506090604_create_deploy_keys_projects.rb +++ b/db/migrate/20130506090604_create_deploy_keys_projects.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateDeployKeysProjects < ActiveRecord::Migration + DOWNTIME = false + def change create_table :deploy_keys_projects do |t| t.integer :deploy_key_id, null: false diff --git a/db/migrate/20130617095603_create_users_groups.rb b/db/migrate/20130617095603_create_users_groups.rb index cb098aa9bf9..4ba7d0c9461 100644 --- a/db/migrate/20130617095603_create_users_groups.rb +++ b/db/migrate/20130617095603_create_users_groups.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateUsersGroups < ActiveRecord::Migration + DOWNTIME = false + def change create_table :users_groups do |t| t.integer :group_access, null: false diff --git a/db/migrate/20130711063759_create_project_group_links.rb b/db/migrate/20130711063759_create_project_group_links.rb index 8da7ff6f4cd..efccb2aa938 100644 --- a/db/migrate/20130711063759_create_project_group_links.rb +++ b/db/migrate/20130711063759_create_project_group_links.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateProjectGroupLinks < ActiveRecord::Migration + DOWNTIME = false + def change create_table :project_group_links do |t| t.integer :project_id, null: false diff --git a/db/migrate/20131112114325_create_broadcast_messages.rb b/db/migrate/20131112114325_create_broadcast_messages.rb index 4ada40f1b66..ad2549e53af 100644 --- a/db/migrate/20131112114325_create_broadcast_messages.rb +++ b/db/migrate/20131112114325_create_broadcast_messages.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateBroadcastMessages < ActiveRecord::Migration + DOWNTIME = false + def change create_table :broadcast_messages do |t| t.text :message, null: false diff --git a/db/migrate/20140122112253_create_merge_request_diffs.rb b/db/migrate/20140122112253_create_merge_request_diffs.rb index 68448c91529..6c7a92b6950 100644 --- a/db/migrate/20140122112253_create_merge_request_diffs.rb +++ b/db/migrate/20140122112253_create_merge_request_diffs.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateMergeRequestDiffs < ActiveRecord::Migration + DOWNTIME = false + def up create_table :merge_request_diffs do |t| t.string :state, null: false, default: 'collected' diff --git a/db/migrate/20140209025651_create_emails.rb b/db/migrate/20140209025651_create_emails.rb index 48d14682628..51886f8fc89 100644 --- a/db/migrate/20140209025651_create_emails.rb +++ b/db/migrate/20140209025651_create_emails.rb @@ -1,10 +1,12 @@ # rubocop:disable all class CreateEmails < ActiveRecord::Migration + DOWNTIME = false + def change create_table :emails do |t| t.integer :user_id, null: false t.string :email, null: false - + t.timestamps null: true end diff --git a/db/migrate/20140625115202_create_users_star_projects.rb b/db/migrate/20140625115202_create_users_star_projects.rb index c50bc4bd614..d4f3fe5ac62 100644 --- a/db/migrate/20140625115202_create_users_star_projects.rb +++ b/db/migrate/20140625115202_create_users_star_projects.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateUsersStarProjects < ActiveRecord::Migration + DOWNTIME = false + def change create_table :users_star_projects do |t| t.integer :project_id, null: false diff --git a/db/migrate/20140729134820_create_labels.rb b/db/migrate/20140729134820_create_labels.rb index 589aced0d76..66d20e741a6 100644 --- a/db/migrate/20140729134820_create_labels.rb +++ b/db/migrate/20140729134820_create_labels.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateLabels < ActiveRecord::Migration + DOWNTIME = false + def change create_table :labels do |t| t.string :title diff --git a/db/migrate/20140729140420_create_label_links.rb b/db/migrate/20140729140420_create_label_links.rb index abdbaa1bd1a..dacd9f2e4b6 100644 --- a/db/migrate/20140729140420_create_label_links.rb +++ b/db/migrate/20140729140420_create_label_links.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateLabelLinks < ActiveRecord::Migration + DOWNTIME = false + def change create_table :label_links do |t| t.integer :label_id diff --git a/db/migrate/20140914113604_add_members_table.rb b/db/migrate/20140914113604_add_members_table.rb index fb0876dd520..0f76bb0ef79 100644 --- a/db/migrate/20140914113604_add_members_table.rb +++ b/db/migrate/20140914113604_add_members_table.rb @@ -1,5 +1,7 @@ # rubocop:disable all class AddMembersTable < ActiveRecord::Migration + DOWNTIME = false + def change create_table :members do |t| t.integer :access_level, null: false diff --git a/db/migrate/20140914173417_remove_old_member_tables.rb b/db/migrate/20140914173417_remove_old_member_tables.rb index 067dc21ccf1..d2ab326ef1f 100644 --- a/db/migrate/20140914173417_remove_old_member_tables.rb +++ b/db/migrate/20140914173417_remove_old_member_tables.rb @@ -1,5 +1,7 @@ # rubocop:disable all class RemoveOldMemberTables < ActiveRecord::Migration + DOWNTIME = false + def up drop_table :users_groups drop_table :users_projects diff --git a/db/migrate/20141118150935_add_audit_event.rb b/db/migrate/20141118150935_add_audit_event.rb index f0452f46459..52d70b4a0ac 100644 --- a/db/migrate/20141118150935_add_audit_event.rb +++ b/db/migrate/20141118150935_add_audit_event.rb @@ -1,5 +1,7 @@ # rubocop:disable all class AddAuditEvent < ActiveRecord::Migration + DOWNTIME = false + def change create_table :audit_events do |t| t.integer :author_id, null: false diff --git a/db/migrate/20141216155758_create_doorkeeper_tables.rb b/db/migrate/20141216155758_create_doorkeeper_tables.rb index 1c4d32e133c..17e45a77291 100644 --- a/db/migrate/20141216155758_create_doorkeeper_tables.rb +++ b/db/migrate/20141216155758_create_doorkeeper_tables.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateDoorkeeperTables < ActiveRecord::Migration + DOWNTIME = false + def change create_table :oauth_applications do |t| t.string :name, null: false diff --git a/db/migrate/20150108073740_create_application_settings.rb b/db/migrate/20150108073740_create_application_settings.rb index c26a7c39574..0e4c66ca8c0 100644 --- a/db/migrate/20150108073740_create_application_settings.rb +++ b/db/migrate/20150108073740_create_application_settings.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateApplicationSettings < ActiveRecord::Migration + DOWNTIME = false + def change create_table :application_settings do |t| t.integer :default_projects_limit diff --git a/db/migrate/20150313012111_create_subscriptions_table.rb b/db/migrate/20150313012111_create_subscriptions_table.rb index 0977c9adfec..a9a8435330d 100644 --- a/db/migrate/20150313012111_create_subscriptions_table.rb +++ b/db/migrate/20150313012111_create_subscriptions_table.rb @@ -1,15 +1,17 @@ # rubocop:disable all class CreateSubscriptionsTable < ActiveRecord::Migration + DOWNTIME = false + def change create_table :subscriptions do |t| t.integer :user_id t.references :subscribable, polymorphic: true t.boolean :subscribed - + t.timestamps null: true end - add_index :subscriptions, + add_index :subscriptions, [:subscribable_id, :subscribable_type, :user_id], unique: true, name: 'subscriptions_user_id_and_ref_fields' diff --git a/db/migrate/20150806104937_create_abuse_reports.rb b/db/migrate/20150806104937_create_abuse_reports.rb index 9f1512db862..52aed9e1d1d 100644 --- a/db/migrate/20150806104937_create_abuse_reports.rb +++ b/db/migrate/20150806104937_create_abuse_reports.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateAbuseReports < ActiveRecord::Migration + DOWNTIME = false + def change create_table :abuse_reports do |t| t.integer :reporter_id diff --git a/db/migrate/20151103134857_create_lfs_objects.rb b/db/migrate/20151103134857_create_lfs_objects.rb index fadaf637cec..db6fa27199b 100644 --- a/db/migrate/20151103134857_create_lfs_objects.rb +++ b/db/migrate/20151103134857_create_lfs_objects.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateLfsObjects < ActiveRecord::Migration + DOWNTIME = false + def change create_table :lfs_objects do |t| t.string :oid, null: false, unique: true diff --git a/db/migrate/20151103134958_create_lfs_objects_projects.rb b/db/migrate/20151103134958_create_lfs_objects_projects.rb index e830cbe4656..5af1c39fd9c 100644 --- a/db/migrate/20151103134958_create_lfs_objects_projects.rb +++ b/db/migrate/20151103134958_create_lfs_objects_projects.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateLfsObjectsProjects < ActiveRecord::Migration + DOWNTIME = false + def change create_table :lfs_objects_projects do |t| t.integer :lfs_object_id, null: false diff --git a/db/migrate/20151105094515_create_releases.rb b/db/migrate/20151105094515_create_releases.rb index 87f692c64d0..34dd7a10942 100644 --- a/db/migrate/20151105094515_create_releases.rb +++ b/db/migrate/20151105094515_create_releases.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateReleases < ActiveRecord::Migration + DOWNTIME = false + def change create_table :releases do |t| t.string :tag diff --git a/db/migrate/20160212123307_create_tasks.rb b/db/migrate/20160212123307_create_tasks.rb index 8b5b1dd694d..cd3ad0e4cd8 100644 --- a/db/migrate/20160212123307_create_tasks.rb +++ b/db/migrate/20160212123307_create_tasks.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateTasks < ActiveRecord::Migration + DOWNTIME = false + def change create_table :tasks do |t| t.references :user, null: false, index: true diff --git a/db/migrate/20160416180807_add_award_emoji.rb b/db/migrate/20160416180807_add_award_emoji.rb index c0957f028a8..0d252e5044e 100644 --- a/db/migrate/20160416180807_add_award_emoji.rb +++ b/db/migrate/20160416180807_add_award_emoji.rb @@ -1,5 +1,7 @@ # rubocop:disable all class AddAwardEmoji < ActiveRecord::Migration + DOWNTIME = false + def change create_table :award_emoji do |t| t.string :name -- cgit v1.2.1 From c7b7a6b33f20699f293ddeec35ea9f767341e462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Wed, 30 Nov 2016 16:45:09 +0100 Subject: Disable the ee_compat_check task on dev MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We usually use it for security fixes that don't need to be ported to EE and the task seems to hang when run against the security branch. Signed-off-by: Rémy Coutable <remy@rymai.me> --- .gitlab-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3c357c489f8..c7528029c89 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -229,7 +229,6 @@ rake ee_compat_check: <<: *exec only: - branches@gitlab-org/gitlab-ce - - branches@gitlab/gitlabhq except: - master - tags -- cgit v1.2.1 From 728a4b50f8d96085408360a15a0da06f8e06eb46 Mon Sep 17 00:00:00 2001 From: Pedro Moreira da Silva <pedro@gitlab.com> Date: Wed, 30 Nov 2016 16:04:32 +0000 Subject: =?UTF-8?q?Improve=20`issue=20create=20=E2=80=A6`=20slash=20comman?= =?UTF-8?q?d=20with=20user=20input=20keys=20to=20create=20a=20newline=20in?= =?UTF-8?q?=20chat=20clients.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/gitlab/chat_commands/issue_create.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb index 99c1382af44..7522f281500 100644 --- a/lib/gitlab/chat_commands/issue_create.rb +++ b/lib/gitlab/chat_commands/issue_create.rb @@ -8,7 +8,7 @@ module Gitlab end def self.help_message - 'issue create <title>\n<description>' + 'issue create <title>` *`Shift`* + *`Enter`* `<description>' end def self.allowed?(project, user) -- cgit v1.2.1 From e694ab634d57ae89f1d4cc09fc98118277e72ced Mon Sep 17 00:00:00 2001 From: Pedro Moreira da Silva <pedro@gitlab.com> Date: Wed, 30 Nov 2016 16:30:03 +0000 Subject: Update Mattermost slash commands docs to explain how to create a newline and use <kbd> for user input. See HTML5 spec: https://www.w3.org/TR/html5/text-level-semantics.html#the-kbd-element --- doc/project_services/mattermost_slash_commands.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/doc/project_services/mattermost_slash_commands.md b/doc/project_services/mattermost_slash_commands.md index 1507dfa3abd..4c1e00b5317 100644 --- a/doc/project_services/mattermost_slash_commands.md +++ b/doc/project_services/mattermost_slash_commands.md @@ -61,7 +61,7 @@ the administrator console. ### Step 3. Create a new custom slash command in Mattermost -Now that you have enabled the custom slash commands in Mattermost and opened +Now that you have enabled custom slash commands in Mattermost and opened the Mattermost slash commands service in GitLab, it's time to copy these values in a new slash command. @@ -124,20 +124,16 @@ GitLab using the Mattermost commands. ## Available slash commands -The available slash commands so far are: +The available slash commands are: | Command | Description | Example | | ------- | ----------- | ------- | -| `/<trigger> issue create <title>\n<description>` | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | `/trigger issue create We need to change the homepage` | -| `/<trigger> issue show <issue-number>` | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | `/trigger issue show 42` | -| `/<trigger> deploy <environment> to <environment>` | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | `/trigger deploy staging to production` | +| <kbd>/<trigger> issue create <title> **Shift** + **Enter** <description></kbd> | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | <samp>/gitlab issue create We need to change the homepage</samp> | +| <kbd>/<trigger> issue show <issue-number></kbd> | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | <samp>/gitlab issue show 42</samp> | +| <kbd>/<trigger> deploy <environment> to <environment></kbd> | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | <samp>/gitlab deploy staging to production</samp> | -To see a list of available commands that can interact with GitLab, type the -trigger word followed by `help`: - -``` -/my-project help -``` +To see a list of available commands to interact with GitLab, type the +trigger word followed by <kbd>help</kbd>. Example: <samp>/gitlab help</samp> ![Mattermost bot available commands](img/mattermost_bot_available_commands.png) -- cgit v1.2.1 From 43e5009a301d474225bf39e0efc5766b4b6be0c1 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 23 Nov 2016 14:44:05 +0000 Subject: Pipelines tabs --- .../lib/utils/bootstrap_linked_tabs.js.es6 | 110 +++++++++++++++++++++ app/controllers/projects/pipelines_controller.rb | 4 + app/views/projects/pipelines/_with_tabs.html.haml | 23 ++++- config/routes/project.rb | 1 + spec/features/projects/pipelines_spec.rb | 7 ++ 5 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 new file mode 100644 index 00000000000..28239cd66a9 --- /dev/null +++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 @@ -0,0 +1,110 @@ + +/** + * Linked Tabs + * + * Handles persisting and restores the current tab selection and content. + * Reusable component for static content. + * + * ### Example Markup + * + * <ul class="nav-links tab-links"> + * <li class="active"> + * <a data-action="tab1" data-target="#tab1" data-toggle="tab" href="/path/tab1"> + * Tab 1 + * </a> + * </li> + * <li class="groups-tab"> + * <a data-action="tab2" data-target="#tab2" data-toggle="tab" href="/path/tab2"> + * Tab 2 + * </a> + * </li> + * + * + * <div class="tab-content"> + * <div class="tab-pane" id="tab1"> + * Tab 1 Content + * </div> + * <div class="tab-pane" id="tab2"> + * Tab 2 Content + * </div> + * </div> + * + * + * ### How to use + * + * new window.gl.LinkedTabs({ + * action: "#{controller.action_name}", + * defaultAction: 'tab1', + * parentEl: '.tab-links' + * }); + */ + +(() => { + window.gl = window.gl || {}; + + window.gl.LinkedTabs = class LinkedTabs { + /** + * Binds the events and activates de default tab. + * + * @param {Object} options + */ + constructor(options) { + this.options = options || {}; + + this.defaultAction = this.options.defaultAction; + this.action = this.options.action || this.defaultAction; + + this.currentLocation = window.location; + + if (this.action === 'show') { + this.action = this.defaultAction; + } + + // since this is a custom event we need jQuery :( + $(document).on('shown.bs.tab', `${this.options.parentEl} a[data-toggle="tab"]`, evt => this.tabShown(evt)); + + this.activateTab(this.action); + } + + /** + * Handles the `shown.bs.tab` event to set the currect url action. + * + * @param {type} evt + * @return {Function} + */ + tabShown(evt) { + const source = evt.target.getAttribute('href'); + + return this.setCurrentAction(source); + } + + /** + * Updates the URL with the path that matched the given action. + * + * @param {String} source + * @return {String} + */ + setCurrentAction(source) { + const copySource = source; + + copySource.replace(/\/+$/, ''); + + const newState = copySource + this.currentLocation.search + this.currentLocation.hash; + + history.replaceState({ + turbolinks: true, + url: newState, + }, document.title, newState); + return newState; + } + + /** + * Given the current action activates the correct tab. + * http://getbootstrap.com/javascript/#tab-show + * Note: Will trigger `shown.bs.tab` + */ + activateTab() { + return $(`.pipelines-tabs a[data-action='${this.action}']`).tab('show'); + } + }; +})(); diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 533af80aee0..de58f84f105 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -32,6 +32,10 @@ class Projects::PipelinesController < Projects::ApplicationController def show end + def builds + render 'show' + end + def retry pipeline.retry_failed(current_user) diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 718314701f9..49a26b3f374 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -1,14 +1,17 @@ .tabs-holder - %ul.nav-links.no-top.no-bottom - %li.active - = link_to "Pipeline", "#js-tab-pipeline", data: { target: '#js-tab-pipeline', action: 'pipeline', toggle: 'tab' }, class: 'pipeline-tab' + %ul.pipelines-tabs.nav-links.no-top.no-bottom %li - = link_to "#js-tab-builds", data: { target: '#js-tab-builds', action: 'build', toggle: 'tab' }, class: 'builds-tab' do + = link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: { target: 'div#js-tab-pipeline', action: 'pipelines', toggle: 'tab' } do + Pipeline + %li + = link_to builds_namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: {target: 'div#js-tab-builds', action: 'builds', toggle: 'tab' } do Builds %span.badge= pipeline.statuses.count + + .tab-content - #js-tab-pipeline.tab-pane.active + #js-tab-pipeline.tab-pane .build-content.middle-block.pipeline-graph .pipeline-visualization %ul.stage-column-list @@ -49,3 +52,13 @@ %th - pipeline.statuses.relevant.stages.each do |stage| = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage) + + +:javascript + var pipelines_tab; + + pipelines_tab = new window.gl.LinkedTabs({ + action: "#{controller.action_name}", + defaultAction: 'pipelines', + parentEl: '.pipelines-tabs' + }); diff --git a/config/routes/project.rb b/config/routes/project.rb index 1336484a399..0754f0ec3b0 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -129,6 +129,7 @@ constraints(ProjectUrlConstrainer.new) do member do post :cancel post :retry + get :builds end end diff --git a/spec/features/projects/pipelines_spec.rb b/spec/features/projects/pipelines_spec.rb index 10e5466fc85..03e89efb5d2 100644 --- a/spec/features/projects/pipelines_spec.rb +++ b/spec/features/projects/pipelines_spec.rb @@ -178,6 +178,13 @@ describe "Pipelines" do expect(page).to have_link('Play') end + context 'page tabs' do + it 'should have two tabs' do + expect(page).to have_link('Pipeline') + expect(page).to have_link('Builds') + end + end + context 'retrying builds' do it { expect(page).not_to have_content('retried') } -- cgit v1.2.1 From 640062abdbd1892dfcdd493bdab6d6c7d24fb3a6 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 24 Nov 2016 11:28:48 +0000 Subject: Adds tests for tabs in the rspec for pipelines Adds tests for the Linked Tabs class Removes event listener Adds builds --- .../lib/utils/bootstrap_linked_tabs.js.es6 | 7 +-- app/controllers/projects/pipelines_controller.rb | 8 +++- app/views/projects/pipelines/_with_tabs.html.haml | 6 +-- spec/features/projects/pipelines_spec.rb | 6 ++- spec/javascripts/bootstrap_linked_tabs_spec.js.es6 | 55 ++++++++++++++++++++++ spec/javascripts/fixtures/linked_tabs.html.haml | 13 +++++ 6 files changed, 86 insertions(+), 9 deletions(-) create mode 100644 spec/javascripts/bootstrap_linked_tabs_spec.js.es6 create mode 100644 spec/javascripts/fixtures/linked_tabs.html.haml diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 index 28239cd66a9..510ac77009b 100644 --- a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 +++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 @@ -1,4 +1,3 @@ - /** * Linked Tabs * @@ -61,7 +60,9 @@ } // since this is a custom event we need jQuery :( - $(document).on('shown.bs.tab', `${this.options.parentEl} a[data-toggle="tab"]`, evt => this.tabShown(evt)); + $(document) + .off('shown.bs.tab', `${this.options.parentEl} a[data-toggle="tab"]`) + .on('shown.bs.tab', `${this.options.parentEl} a[data-toggle="tab"]`, evt => this.tabShown(evt)); this.activateTab(this.action); } @@ -104,7 +105,7 @@ * Note: Will trigger `shown.bs.tab` */ activateTab() { - return $(`.pipelines-tabs a[data-action='${this.action}']`).tab('show'); + return $(`${this.options.parentEl} a[data-action='${this.action}']`).tab('show'); } }; })(); diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index de58f84f105..85188cfdd4c 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -1,6 +1,6 @@ class Projects::PipelinesController < Projects::ApplicationController before_action :pipeline, except: [:index, :new, :create] - before_action :commit, only: [:show] + before_action :commit, only: [:show, :builds] before_action :authorize_read_pipeline! before_action :authorize_create_pipeline!, only: [:new, :create] before_action :authorize_update_pipeline!, only: [:retry, :cancel] @@ -33,7 +33,11 @@ class Projects::PipelinesController < Projects::ApplicationController end def builds - render 'show' + respond_to do |format| + format.html do + render 'show' + end + end end def retry diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 49a26b3f374..4cc0e116122 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -1,12 +1,12 @@ .tabs-holder %ul.pipelines-tabs.nav-links.no-top.no-bottom %li - = link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: { target: 'div#js-tab-pipeline', action: 'pipelines', toggle: 'tab' } do + = link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: { target: 'div#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do Pipeline %li - = link_to builds_namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: {target: 'div#js-tab-builds', action: 'builds', toggle: 'tab' } do + = link_to builds_namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: {target: 'div#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do Builds - %span.badge= pipeline.statuses.count + %span.badge.js-builds-counter= pipeline.statuses.count diff --git a/spec/features/projects/pipelines_spec.rb b/spec/features/projects/pipelines_spec.rb index 03e89efb5d2..24e266e828f 100644 --- a/spec/features/projects/pipelines_spec.rb +++ b/spec/features/projects/pipelines_spec.rb @@ -179,10 +179,14 @@ describe "Pipelines" do end context 'page tabs' do - it 'should have two tabs' do + it 'shows Pipeline and Builds tabs with link' do expect(page).to have_link('Pipeline') expect(page).to have_link('Builds') end + + it 'shows counter in Builds tab' do + expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s) + end end context 'retrying builds' do diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 new file mode 100644 index 00000000000..9aa3c50611d --- /dev/null +++ b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 @@ -0,0 +1,55 @@ +//= require lib/utils/bootstrap_linked_tabs + +(() => { + describe('Linked Tabs', () => { + fixture.preload('linked_tabs'); + + beforeEach(() => { + fixture.load('linked_tabs'); + }); + + describe('when is initialized', () => { + it('should activate the tab correspondent to the given action', () => { + const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line + action: 'tab1', + defaultAction: 'tab1', + parentEl: '.linked-tabs', + }); + + expect(document.querySelector('#tab1').classList).toContain('active'); + }); + + it('should active the default tab action when the action is show', () => { + const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line + action: 'show', + defaultAction: 'tab1', + parentEl: '.linked-tabs', + }); + + expect(document.querySelector('#tab1').classList).toContain('active'); + }); + }); + + describe('on click', () => { + it('should change the url according to the clicked tab', () => { + const historySpy = spyOn(history, 'replaceState').and.callFake(() => {}); + + const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line + action: 'show', + defaultAction: 'tab1', + parentEl: '.linked-tabs', + }); + + const secondTab = document.querySelector('.linked-tabs li:nth-child(2) a'); + const newState = secondTab.getAttribute('href') + linkedTabs.currentLocation.search + linkedTabs.currentLocation.hash; + + secondTab.click(); + + expect(historySpy).toHaveBeenCalledWith({ + turbolinks: true, + url: newState, + }, document.title, newState); + }); + }); + }); +})(); diff --git a/spec/javascripts/fixtures/linked_tabs.html.haml b/spec/javascripts/fixtures/linked_tabs.html.haml new file mode 100644 index 00000000000..93c0cf97ff0 --- /dev/null +++ b/spec/javascripts/fixtures/linked_tabs.html.haml @@ -0,0 +1,13 @@ +%ul.nav.nav-tabs.linked-tabs + %li + %a{ href: 'foo/bar/1', data: { target: 'div#tab1', action: 'tab1', toggle: 'tab' } } + Tab 1 + %li + %a{ href: 'foo/bar/1/context', data: { target: 'div#tab2', action: 'tab2', toggle: 'tab' } } + Tab 2 + +.tab-content + #tab1.tab-pane + Tab 1 Content + #tab2.tab-pane + Tab 2 Content -- cgit v1.2.1 From da8c2e69876808d991e8c44b0ea86bb1f4c4e5ee Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 24 Nov 2016 15:06:42 +0000 Subject: Adds changelog entry --- changelogs/unreleased/24814-pipeline-tabs.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/24814-pipeline-tabs.yml diff --git a/changelogs/unreleased/24814-pipeline-tabs.yml b/changelogs/unreleased/24814-pipeline-tabs.yml new file mode 100644 index 00000000000..f85e7576905 --- /dev/null +++ b/changelogs/unreleased/24814-pipeline-tabs.yml @@ -0,0 +1,4 @@ +--- +title: Fix Cicking on tabs on pipeline page should set URL +merge_request: 7709 +author: -- cgit v1.2.1 From cc4434a483e75dd83055fe21ef4dedadbfcdc4c7 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 24 Nov 2016 16:43:54 +0000 Subject: Fix after review Adds require statement --- app/assets/javascripts/dispatcher.js.es6 | 12 +++++++++++- app/assets/javascripts/pipelines.js.es6 | 9 ++++++++- app/views/projects/pipelines/_with_tabs.html.haml | 10 ---------- app/views/projects/pipelines/show.html.haml | 2 +- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 16df4b0b005..de205b7905b 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -135,8 +135,18 @@ new TreeView(); } break; + case 'projects:pipelines:builds': case 'projects:pipelines:show': - new gl.Pipelines(); + const controllerAction = document.querySelector('.js-pipeline-container').dataset.controllerAction; + + new gl.Pipelines({ + initTabs: true, + tabsOptions: { + action: controllerAction, + defaultAction: 'pipelines', + parentEl: '.pipelines-tabs', + }, + }); break; case 'groups:activity': new gl.Activities(); diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 index a84db9c0233..97e06bc6d68 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 @@ -1,8 +1,15 @@ +//= require lib/utils/bootstrap_linked_tabs + /* eslint-disable */ ((global) => { class Pipelines { - constructor() { + constructor(options) { + + if (options.initTabs && options.tabsOptions) { + new window.gl.LinkedTabs(options.tabsOptions); + } + this.addMarginToBuildColumns(); } diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 4cc0e116122..d4d2e360f39 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -52,13 +52,3 @@ %th - pipeline.statuses.relevant.stages.each do |stage| = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage) - - -:javascript - var pipelines_tab; - - pipelines_tab = new window.gl.LinkedTabs({ - action: "#{controller.action_name}", - defaultAction: 'pipelines', - parentEl: '.pipelines-tabs' - }); diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml index 8c6652a5f90..371ce3ae599 100644 --- a/app/views/projects/pipelines/show.html.haml +++ b/app/views/projects/pipelines/show.html.haml @@ -2,7 +2,7 @@ - page_title "Pipeline" = render "projects/pipelines/head" -%div{ class: container_class } +%div.js-pipeline-container{ class: container_class, data: {controller_action: "#{controller.action_name}"} } - if @commit = render "projects/pipelines/info" -- cgit v1.2.1 From 58d6120a3b9b5d38566766b7e0154010c6a00bd5 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 25 Nov 2016 11:23:22 +0000 Subject: Adds tests for builds url and tabs behaviour --- app/views/projects/pipelines/_with_tabs.html.haml | 4 +- app/views/projects/pipelines/show.html.haml | 2 +- spec/features/projects/pipelines_spec.rb | 86 +++++++++++++++++++++++ 3 files changed, 89 insertions(+), 3 deletions(-) diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index d4d2e360f39..3464e155a1b 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -1,9 +1,9 @@ .tabs-holder %ul.pipelines-tabs.nav-links.no-top.no-bottom - %li + %li.js-pipeline-tab-link = link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: { target: 'div#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do Pipeline - %li + %li.js-builds-tab-link = link_to builds_namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: {target: 'div#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do Builds %span.badge.js-builds-counter= pipeline.statuses.count diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml index 371ce3ae599..29a41bc664b 100644 --- a/app/views/projects/pipelines/show.html.haml +++ b/app/views/projects/pipelines/show.html.haml @@ -2,7 +2,7 @@ - page_title "Pipeline" = render "projects/pipelines/head" -%div.js-pipeline-container{ class: container_class, data: {controller_action: "#{controller.action_name}"} } +%div.js-pipeline-container{ class: container_class, data: { controller_action: "#{controller.action_name}" } } - if @commit = render "projects/pipelines/info" diff --git a/spec/features/projects/pipelines_spec.rb b/spec/features/projects/pipelines_spec.rb index 24e266e828f..0f4f46d59b5 100644 --- a/spec/features/projects/pipelines_spec.rb +++ b/spec/features/projects/pipelines_spec.rb @@ -178,6 +178,88 @@ describe "Pipelines" do expect(page).to have_link('Play') end + it 'shows Pipeline tab pane as active' do + expect(page).to have_css('#js-tab-pipeline.active') + end + + context 'page tabs' do + it 'shows Pipeline and Builds tabs with link' do + expect(page).to have_link('Pipeline') + expect(page).to have_link('Builds') + end + + it 'shows counter in Builds tab' do + expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s) + end + + it 'shows Pipeline tab as active' do + expect(page).to have_css('li.js-pipeline-tab-link.active') + end + end + + context 'retrying builds' do + it { expect(page).not_to have_content('retried') } + + context 'when retrying' do + before { click_on 'Retry failed' } + + it { expect(page).not_to have_content('Retry failed') } + it { expect(page).to have_selector('.retried') } + end + end + + context 'canceling builds' do + it { expect(page).not_to have_selector('.ci-canceled') } + + context 'when canceling' do + before { click_on 'Cancel running' } + + it { expect(page).not_to have_content('Cancel running') } + it { expect(page).to have_selector('.ci-canceled') } + end + end + + context 'playing manual build' do + before do + within '.pipeline-holder' do + click_link('Play') + end + end + + it { expect(@manual.reload).to be_pending } + end + end + + describe 'GET /:project/pipelines/:id/builds' do + let(:project) { create(:project) } + let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) } + + before do + @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build') + @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test') + @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy') + @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual build') + @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external') + end + + before { visit builds_namespace_project_pipeline_path(project.namespace, project, pipeline)} + + it 'shows a list of builds' do + expect(page).to have_content('Test') + expect(page).to have_content(@success.id) + expect(page).to have_content('Deploy') + expect(page).to have_content(@failed.id) + expect(page).to have_content(@running.id) + expect(page).to have_content(@external.id) + expect(page).to have_content('Retry failed') + expect(page).to have_content('Cancel running') + expect(page).to have_link('Play') + end + + it 'shows Builds tab pane as active' do + expect(page).to have_css('#js-tab-builds.active') + end + context 'page tabs' do it 'shows Pipeline and Builds tabs with link' do expect(page).to have_link('Pipeline') @@ -187,6 +269,10 @@ describe "Pipelines" do it 'shows counter in Builds tab' do expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s) end + + it 'shows Builds tab as active' do + expect(page).to have_css('li.js-builds-tab-link.active') + end end context 'retrying builds' do -- cgit v1.2.1 From cb9cee545a5952ed35ccd95e8cd4abe228f09a7b Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 25 Nov 2016 12:13:12 +0000 Subject: Adds .matches polyfill Moves matches poly to the correct file Divides pipelines index and show tests in order to be able to test with JS --- app/assets/javascripts/extensions/element.js.es6 | 25 +- spec/features/projects/pipelines/pipeline_spec.rb | 170 ++++++++++ spec/features/projects/pipelines/pipelines_spec.rb | 211 ++++++++++++ spec/features/projects/pipelines_spec.rb | 367 --------------------- 4 files changed, 404 insertions(+), 369 deletions(-) create mode 100644 spec/features/projects/pipelines/pipeline_spec.rb create mode 100644 spec/features/projects/pipelines/pipelines_spec.rb delete mode 100644 spec/features/projects/pipelines_spec.rb diff --git a/app/assets/javascripts/extensions/element.js.es6 b/app/assets/javascripts/extensions/element.js.es6 index 6d9b0c4bc3e..0abe8644f30 100644 --- a/app/assets/javascripts/extensions/element.js.es6 +++ b/app/assets/javascripts/extensions/element.js.es6 @@ -1,9 +1,30 @@ /* global Element */ /* eslint-disable consistent-return, max-len */ -Element.prototype.matches = Element.prototype.matches || Element.prototype.msMatchesSelector; - Element.prototype.closest = Element.prototype.closest || function closest(selector, selectedElement = this) { if (!selectedElement) return; return selectedElement.matches(selector) ? selectedElement : Element.prototype.closest(selector, selectedElement.parentElement); }; + +/* eslint-disable */ +/** + * .matches polyfill from mdn + * https://developer.mozilla.org/en-US/docs/Web/API/Element/matches + * + * .matches is used in our code. + * In order to run the tests in Phantomjs we need this polyfill + */ +if (!Element.prototype.matches) { + Element.prototype.matches = + Element.prototype.matchesSelector || + Element.prototype.mozMatchesSelector || + Element.prototype.msMatchesSelector || + Element.prototype.oMatchesSelector || + Element.prototype.webkitMatchesSelector || + function (s) { + var matches = (this.document || this.ownerDocument).querySelectorAll(s), + i = matches.length; + while (--i >= 0 && matches.item(i) !== this) {} + return i > -1; + }; +} diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb new file mode 100644 index 00000000000..201caf3bbd3 --- /dev/null +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -0,0 +1,170 @@ +require 'spec_helper' + +describe "Pipelines", feature: true, js: true do + include GitlabRoutingHelper + + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + + before do + login_as(user) + project.team << [user, :developer] + end + + describe 'GET /:project/pipelines/:id' do + let(:project) { create(:project) } + let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) } + + before do + @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build') + @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test') + @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy') + @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual build') + @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external') + end + + before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) } + + it 'shows a list of builds' do + expect(page).to have_content('Test') + expect(page).to have_content(@success.id) + expect(page).to have_content('Deploy') + expect(page).to have_content(@failed.id) + expect(page).to have_content(@running.id) + expect(page).to have_content(@external.id) + expect(page).to have_content('Retry failed') + expect(page).to have_content('Cancel running') + expect(page).to have_link('Play') + end + + it 'shows Pipeline tab pane as active' do + expect(page).to have_css('#js-tab-pipeline.active') + end + + context 'page tabs' do + it 'shows Pipeline and Builds tabs with link' do + expect(page).to have_link('Pipeline') + expect(page).to have_link('Builds') + end + + it 'shows counter in Builds tab' do + expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s) + end + + it 'shows Pipeline tab as active' do + expect(page).to have_css('.js-pipeline-tab-link.active') + end + end + + context 'retrying builds' do + it { expect(page).not_to have_content('retried') } + + context 'when retrying' do + before { click_on 'Retry failed' } + + it { expect(page).not_to have_content('Retry failed') } + it { expect(page).to have_selector('.retried') } + end + end + + context 'canceling builds' do + it { expect(page).not_to have_selector('.ci-canceled') } + + context 'when canceling' do + before { click_on 'Cancel running' } + + it { expect(page).not_to have_content('Cancel running') } + it { expect(page).to have_selector('.ci-canceled') } + end + end + + context 'playing manual build' do + before do + within '.pipeline-holder' do + click_link('Play') + end + end + + it { expect(@manual.reload).to be_pending } + end + end + + describe 'GET /:project/pipelines/:id/builds' do + let(:project) { create(:project) } + let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) } + + before do + @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build') + @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test') + @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy') + @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual build') + @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external') + end + + before { visit builds_namespace_project_pipeline_path(project.namespace, project, pipeline)} + + it 'shows a list of builds' do + expect(page).to have_content('Test') + expect(page).to have_content(@success.id) + expect(page).to have_content('Deploy') + expect(page).to have_content(@failed.id) + expect(page).to have_content(@running.id) + expect(page).to have_content(@external.id) + expect(page).to have_content('Retry failed') + expect(page).to have_content('Cancel running') + expect(page).to have_link('Play') + end + + it 'shows Builds tab pane as active' do + expect(page).to have_css('#js-tab-builds.active') + end + + context 'page tabs' do + it 'shows Pipeline and Builds tabs with link' do + expect(page).to have_link('Pipeline') + expect(page).to have_link('Builds') + end + + it 'shows counter in Builds tab' do + expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s) + end + + it 'shows Builds tab as active' do + expect(page).to have_css('li.js-builds-tab-link.active') + end + end + + context 'retrying builds' do + it { expect(page).not_to have_content('retried') } + + context 'when retrying' do + before { click_on 'Retry failed' } + + it { expect(page).not_to have_content('Retry failed') } + it { expect(page).to have_selector('.retried') } + end + end + + context 'canceling builds' do + it { expect(page).not_to have_selector('.ci-canceled') } + + context 'when canceling' do + before { click_on 'Cancel running' } + + it { expect(page).not_to have_content('Cancel running') } + it { expect(page).to have_selector('.ci-canceled') } + end + end + + context 'playing manual build' do + before do + within '.pipeline-holder' do + click_link('Play') + end + end + + it { expect(@manual.reload).to be_pending } + end + end + +end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb new file mode 100644 index 00000000000..f3731698a18 --- /dev/null +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -0,0 +1,211 @@ +require 'spec_helper' + +describe "Pipelines" do + include GitlabRoutingHelper + + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + + before do + login_as(user) + project.team << [user, :developer] + end + + describe 'GET /:project/pipelines' do + let!(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running') } + + [:all, :running, :branches].each do |scope| + context "displaying #{scope}" do + let(:project) { create(:project) } + + before { visit namespace_project_pipelines_path(project.namespace, project, scope: scope) } + + it { expect(page).to have_content(pipeline.short_sha) } + end + end + + context 'anonymous access' do + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it { expect(page).to have_http_status(:success) } + end + + context 'cancelable pipeline' do + let!(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') } + + before do + build.run + visit namespace_project_pipelines_path(project.namespace, project) + end + + it { expect(page).to have_link('Cancel') } + it { expect(page).to have_selector('.ci-running') } + + context 'when canceling' do + before { click_link('Cancel') } + + it { expect(page).not_to have_link('Cancel') } + it { expect(page).to have_selector('.ci-canceled') } + end + end + + context 'retryable pipelines' do + let!(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') } + + before do + build.drop + visit namespace_project_pipelines_path(project.namespace, project) + end + + it { expect(page).to have_link('Retry') } + it { expect(page).to have_selector('.ci-failed') } + + context 'when retrying' do + before { click_link('Retry') } + + it { expect(page).not_to have_link('Retry') } + it { expect(page).to have_selector('.ci-running') } + end + end + + context 'with manual actions' do + let!(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'manual build', stage: 'test', commands: 'test') } + + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it { expect(page).to have_link('Manual build') } + + context 'when playing' do + before { click_link('Manual build') } + + it { expect(manual.reload).to be_pending } + end + end + + context 'for generic statuses' do + context 'when running' do + let!(:running) { create(:generic_commit_status, status: 'running', pipeline: pipeline, stage: 'test') } + + before do + visit namespace_project_pipelines_path(project.namespace, project) + end + + it 'is cancelable' do + expect(page).to have_link('Cancel') + end + + it 'has pipeline running' do + expect(page).to have_selector('.ci-running') + end + + context 'when canceling' do + before { click_link('Cancel') } + + it { expect(page).not_to have_link('Cancel') } + it { expect(page).to have_selector('.ci-canceled') } + end + end + + context 'when failed' do + let!(:status) { create(:generic_commit_status, :pending, pipeline: pipeline, stage: 'test') } + + before do + status.drop + visit namespace_project_pipelines_path(project.namespace, project) + end + + it 'is not retryable' do + expect(page).not_to have_link('Retry') + end + + it 'has failed pipeline' do + expect(page).to have_selector('.ci-failed') + end + end + end + + context 'downloadable pipelines' do + context 'with artifacts' do + let!(:with_artifacts) { create(:ci_build, :artifacts, :success, pipeline: pipeline, name: 'rspec tests', stage: 'test') } + + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it { expect(page).to have_selector('.build-artifacts') } + it { expect(page).to have_link(with_artifacts.name) } + end + + context 'with artifacts expired' do + let!(:with_artifacts_expired) { create(:ci_build, :artifacts_expired, :success, pipeline: pipeline, name: 'rspec', stage: 'test') } + + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it { expect(page).not_to have_selector('.build-artifacts') } + end + + context 'without artifacts' do + let!(:without_artifacts) { create(:ci_build, :success, pipeline: pipeline, name: 'rspec', stage: 'test') } + + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it { expect(page).not_to have_selector('.build-artifacts') } + end + end + end + + describe 'POST /:project/pipelines' do + let(:project) { create(:project) } + + before { visit new_namespace_project_pipeline_path(project.namespace, project) } + + context 'for valid commit' do + before { fill_in('pipeline[ref]', with: 'master') } + + context 'with gitlab-ci.yml' do + before { stub_ci_pipeline_to_return_yaml_file } + + it { expect{ click_on 'Create pipeline' }.to change{ Ci::Pipeline.count }.by(1) } + end + + context 'without gitlab-ci.yml' do + before { click_on 'Create pipeline' } + + it { expect(page).to have_content('Missing .gitlab-ci.yml file') } + end + end + + context 'for invalid commit' do + before do + fill_in('pipeline[ref]', with: 'invalid-reference') + click_on 'Create pipeline' + end + + it { expect(page).to have_content('Reference not found') } + end + end + + describe 'Create pipelines', feature: true do + let(:project) { create(:project) } + + before do + visit new_namespace_project_pipeline_path(project.namespace, project) + end + + describe 'new pipeline page' do + it 'has field to add a new pipeline' do + expect(page).to have_field('pipeline[ref]') + expect(page).to have_content('Create for') + end + end + + describe 'find pipelines' do + it 'shows filtered pipelines', js: true do + fill_in('pipeline[ref]', with: 'fix') + find('input#ref').native.send_keys(:keydown) + + within('.ui-autocomplete') do + expect(page).to have_selector('li', text: 'fix') + end + end + end + end +end diff --git a/spec/features/projects/pipelines_spec.rb b/spec/features/projects/pipelines_spec.rb deleted file mode 100644 index 0f4f46d59b5..00000000000 --- a/spec/features/projects/pipelines_spec.rb +++ /dev/null @@ -1,367 +0,0 @@ -require 'spec_helper' - -describe "Pipelines" do - include GitlabRoutingHelper - - let(:project) { create(:empty_project) } - let(:user) { create(:user) } - - before do - login_as(user) - project.team << [user, :developer] - end - - describe 'GET /:project/pipelines' do - let!(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running') } - - [:all, :running, :branches].each do |scope| - context "displaying #{scope}" do - let(:project) { create(:project) } - - before { visit namespace_project_pipelines_path(project.namespace, project, scope: scope) } - - it { expect(page).to have_content(pipeline.short_sha) } - end - end - - context 'anonymous access' do - before { visit namespace_project_pipelines_path(project.namespace, project) } - - it { expect(page).to have_http_status(:success) } - end - - context 'cancelable pipeline' do - let!(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') } - - before do - build.run - visit namespace_project_pipelines_path(project.namespace, project) - end - - it { expect(page).to have_link('Cancel') } - it { expect(page).to have_selector('.ci-running') } - - context 'when canceling' do - before { click_link('Cancel') } - - it { expect(page).not_to have_link('Cancel') } - it { expect(page).to have_selector('.ci-canceled') } - end - end - - context 'retryable pipelines' do - let!(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') } - - before do - build.drop - visit namespace_project_pipelines_path(project.namespace, project) - end - - it { expect(page).to have_link('Retry') } - it { expect(page).to have_selector('.ci-failed') } - - context 'when retrying' do - before { click_link('Retry') } - - it { expect(page).not_to have_link('Retry') } - it { expect(page).to have_selector('.ci-running') } - end - end - - context 'with manual actions' do - let!(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'manual build', stage: 'test', commands: 'test') } - - before { visit namespace_project_pipelines_path(project.namespace, project) } - - it { expect(page).to have_link('Manual build') } - - context 'when playing' do - before { click_link('Manual build') } - - it { expect(manual.reload).to be_pending } - end - end - - context 'for generic statuses' do - context 'when running' do - let!(:running) { create(:generic_commit_status, status: 'running', pipeline: pipeline, stage: 'test') } - - before do - visit namespace_project_pipelines_path(project.namespace, project) - end - - it 'is cancelable' do - expect(page).to have_link('Cancel') - end - - it 'has pipeline running' do - expect(page).to have_selector('.ci-running') - end - - context 'when canceling' do - before { click_link('Cancel') } - - it { expect(page).not_to have_link('Cancel') } - it { expect(page).to have_selector('.ci-canceled') } - end - end - - context 'when failed' do - let!(:status) { create(:generic_commit_status, :pending, pipeline: pipeline, stage: 'test') } - - before do - status.drop - visit namespace_project_pipelines_path(project.namespace, project) - end - - it 'is not retryable' do - expect(page).not_to have_link('Retry') - end - - it 'has failed pipeline' do - expect(page).to have_selector('.ci-failed') - end - end - end - - context 'downloadable pipelines' do - context 'with artifacts' do - let!(:with_artifacts) { create(:ci_build, :artifacts, :success, pipeline: pipeline, name: 'rspec tests', stage: 'test') } - - before { visit namespace_project_pipelines_path(project.namespace, project) } - - it { expect(page).to have_selector('.build-artifacts') } - it { expect(page).to have_link(with_artifacts.name) } - end - - context 'with artifacts expired' do - let!(:with_artifacts_expired) { create(:ci_build, :artifacts_expired, :success, pipeline: pipeline, name: 'rspec', stage: 'test') } - - before { visit namespace_project_pipelines_path(project.namespace, project) } - - it { expect(page).not_to have_selector('.build-artifacts') } - end - - context 'without artifacts' do - let!(:without_artifacts) { create(:ci_build, :success, pipeline: pipeline, name: 'rspec', stage: 'test') } - - before { visit namespace_project_pipelines_path(project.namespace, project) } - - it { expect(page).not_to have_selector('.build-artifacts') } - end - end - end - - describe 'GET /:project/pipelines/:id' do - let(:project) { create(:project) } - let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) } - - before do - @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build') - @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test') - @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy') - @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual build') - @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external') - end - - before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) } - - it 'shows a list of builds' do - expect(page).to have_content('Test') - expect(page).to have_content(@success.id) - expect(page).to have_content('Deploy') - expect(page).to have_content(@failed.id) - expect(page).to have_content(@running.id) - expect(page).to have_content(@external.id) - expect(page).to have_content('Retry failed') - expect(page).to have_content('Cancel running') - expect(page).to have_link('Play') - end - - it 'shows Pipeline tab pane as active' do - expect(page).to have_css('#js-tab-pipeline.active') - end - - context 'page tabs' do - it 'shows Pipeline and Builds tabs with link' do - expect(page).to have_link('Pipeline') - expect(page).to have_link('Builds') - end - - it 'shows counter in Builds tab' do - expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s) - end - - it 'shows Pipeline tab as active' do - expect(page).to have_css('li.js-pipeline-tab-link.active') - end - end - - context 'retrying builds' do - it { expect(page).not_to have_content('retried') } - - context 'when retrying' do - before { click_on 'Retry failed' } - - it { expect(page).not_to have_content('Retry failed') } - it { expect(page).to have_selector('.retried') } - end - end - - context 'canceling builds' do - it { expect(page).not_to have_selector('.ci-canceled') } - - context 'when canceling' do - before { click_on 'Cancel running' } - - it { expect(page).not_to have_content('Cancel running') } - it { expect(page).to have_selector('.ci-canceled') } - end - end - - context 'playing manual build' do - before do - within '.pipeline-holder' do - click_link('Play') - end - end - - it { expect(@manual.reload).to be_pending } - end - end - - describe 'GET /:project/pipelines/:id/builds' do - let(:project) { create(:project) } - let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) } - - before do - @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build') - @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test') - @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy') - @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual build') - @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external') - end - - before { visit builds_namespace_project_pipeline_path(project.namespace, project, pipeline)} - - it 'shows a list of builds' do - expect(page).to have_content('Test') - expect(page).to have_content(@success.id) - expect(page).to have_content('Deploy') - expect(page).to have_content(@failed.id) - expect(page).to have_content(@running.id) - expect(page).to have_content(@external.id) - expect(page).to have_content('Retry failed') - expect(page).to have_content('Cancel running') - expect(page).to have_link('Play') - end - - it 'shows Builds tab pane as active' do - expect(page).to have_css('#js-tab-builds.active') - end - - context 'page tabs' do - it 'shows Pipeline and Builds tabs with link' do - expect(page).to have_link('Pipeline') - expect(page).to have_link('Builds') - end - - it 'shows counter in Builds tab' do - expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s) - end - - it 'shows Builds tab as active' do - expect(page).to have_css('li.js-builds-tab-link.active') - end - end - - context 'retrying builds' do - it { expect(page).not_to have_content('retried') } - - context 'when retrying' do - before { click_on 'Retry failed' } - - it { expect(page).not_to have_content('Retry failed') } - it { expect(page).to have_selector('.retried') } - end - end - - context 'canceling builds' do - it { expect(page).not_to have_selector('.ci-canceled') } - - context 'when canceling' do - before { click_on 'Cancel running' } - - it { expect(page).not_to have_content('Cancel running') } - it { expect(page).to have_selector('.ci-canceled') } - end - end - - context 'playing manual build' do - before do - within '.pipeline-holder' do - click_link('Play') - end - end - - it { expect(@manual.reload).to be_pending } - end - end - - describe 'POST /:project/pipelines' do - let(:project) { create(:project) } - - before { visit new_namespace_project_pipeline_path(project.namespace, project) } - - context 'for valid commit' do - before { fill_in('pipeline[ref]', with: 'master') } - - context 'with gitlab-ci.yml' do - before { stub_ci_pipeline_to_return_yaml_file } - - it { expect{ click_on 'Create pipeline' }.to change{ Ci::Pipeline.count }.by(1) } - end - - context 'without gitlab-ci.yml' do - before { click_on 'Create pipeline' } - - it { expect(page).to have_content('Missing .gitlab-ci.yml file') } - end - end - - context 'for invalid commit' do - before do - fill_in('pipeline[ref]', with: 'invalid-reference') - click_on 'Create pipeline' - end - - it { expect(page).to have_content('Reference not found') } - end - end - - describe 'Create pipelines', feature: true do - let(:project) { create(:project) } - - before do - visit new_namespace_project_pipeline_path(project.namespace, project) - end - - describe 'new pipeline page' do - it 'has field to add a new pipeline' do - expect(page).to have_field('pipeline[ref]') - expect(page).to have_content('Create for') - end - end - - describe 'find pipelines' do - it 'shows filtered pipelines', js: true do - fill_in('pipeline[ref]', with: 'fix') - find('input#ref').native.send_keys(:keydown) - - within('.ui-autocomplete') do - expect(page).to have_selector('li', text: 'fix') - end - end - end - end -end -- cgit v1.2.1 From 6789befbf6b0578718b0e461c60326cd97dc8640 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 25 Nov 2016 14:21:50 +0000 Subject: Fixes after review Fix pipelines tests Fix rubocop --- app/assets/javascripts/extensions/element.js.es6 | 14 +++----------- spec/features/projects/pipelines/pipeline_spec.rb | 22 +++------------------- 2 files changed, 6 insertions(+), 30 deletions(-) diff --git a/app/assets/javascripts/extensions/element.js.es6 b/app/assets/javascripts/extensions/element.js.es6 index 0abe8644f30..328cc57dca0 100644 --- a/app/assets/javascripts/extensions/element.js.es6 +++ b/app/assets/javascripts/extensions/element.js.es6 @@ -1,19 +1,11 @@ /* global Element */ -/* eslint-disable consistent-return, max-len */ +/* eslint-disable consistent-return, max-len, no-empty, no-plusplus, func-names */ Element.prototype.closest = Element.prototype.closest || function closest(selector, selectedElement = this) { if (!selectedElement) return; return selectedElement.matches(selector) ? selectedElement : Element.prototype.closest(selector, selectedElement.parentElement); }; -/* eslint-disable */ -/** - * .matches polyfill from mdn - * https://developer.mozilla.org/en-US/docs/Web/API/Element/matches - * - * .matches is used in our code. - * In order to run the tests in Phantomjs we need this polyfill - */ if (!Element.prototype.matches) { Element.prototype.matches = Element.prototype.matchesSelector || @@ -22,8 +14,8 @@ if (!Element.prototype.matches) { Element.prototype.oMatchesSelector || Element.prototype.webkitMatchesSelector || function (s) { - var matches = (this.document || this.ownerDocument).querySelectorAll(s), - i = matches.length; + const matches = (this.document || this.ownerDocument).querySelectorAll(s); + let i = matches.length; while (--i >= 0 && matches.item(i) !== this) {} return i > -1; }; diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 201caf3bbd3..3350a3aeefc 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -25,16 +25,13 @@ describe "Pipelines", feature: true, js: true do before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) } - it 'shows a list of builds' do + it 'shows the pipeline graph' do + expect(page).to have_selector('.pipeline-visualization') + expect(page).to have_content('Build') expect(page).to have_content('Test') - expect(page).to have_content(@success.id) expect(page).to have_content('Deploy') - expect(page).to have_content(@failed.id) - expect(page).to have_content(@running.id) - expect(page).to have_content(@external.id) expect(page).to have_content('Retry failed') expect(page).to have_content('Cancel running') - expect(page).to have_link('Play') end it 'shows Pipeline tab pane as active' do @@ -63,7 +60,6 @@ describe "Pipelines", feature: true, js: true do before { click_on 'Retry failed' } it { expect(page).not_to have_content('Retry failed') } - it { expect(page).to have_selector('.retried') } end end @@ -74,19 +70,8 @@ describe "Pipelines", feature: true, js: true do before { click_on 'Cancel running' } it { expect(page).not_to have_content('Cancel running') } - it { expect(page).to have_selector('.ci-canceled') } end end - - context 'playing manual build' do - before do - within '.pipeline-holder' do - click_link('Play') - end - end - - it { expect(@manual.reload).to be_pending } - end end describe 'GET /:project/pipelines/:id/builds' do @@ -166,5 +151,4 @@ describe "Pipelines", feature: true, js: true do it { expect(@manual.reload).to be_pending } end end - end -- cgit v1.2.1 From aa2d6eec9e36acdff679d3a5ef17db0780f51447 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Tue, 29 Nov 2016 10:35:53 +0000 Subject: Improvements after review --- app/assets/javascripts/dispatcher.js.es6 | 2 +- app/assets/javascripts/extensions/element.js.es6 | 26 ++++++++++------------ .../lib/utils/bootstrap_linked_tabs.js.es6 | 12 +++++----- app/assets/javascripts/pipelines.js.es6 | 2 +- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index de205b7905b..5fbeb57cc63 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -137,7 +137,7 @@ break; case 'projects:pipelines:builds': case 'projects:pipelines:show': - const controllerAction = document.querySelector('.js-pipeline-container').dataset.controllerAction; + const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; new gl.Pipelines({ initTabs: true, diff --git a/app/assets/javascripts/extensions/element.js.es6 b/app/assets/javascripts/extensions/element.js.es6 index 328cc57dca0..3f12ad9ff9f 100644 --- a/app/assets/javascripts/extensions/element.js.es6 +++ b/app/assets/javascripts/extensions/element.js.es6 @@ -6,17 +6,15 @@ Element.prototype.closest = Element.prototype.closest || function closest(select return selectedElement.matches(selector) ? selectedElement : Element.prototype.closest(selector, selectedElement.parentElement); }; -if (!Element.prototype.matches) { - Element.prototype.matches = - Element.prototype.matchesSelector || - Element.prototype.mozMatchesSelector || - Element.prototype.msMatchesSelector || - Element.prototype.oMatchesSelector || - Element.prototype.webkitMatchesSelector || - function (s) { - const matches = (this.document || this.ownerDocument).querySelectorAll(s); - let i = matches.length; - while (--i >= 0 && matches.item(i) !== this) {} - return i > -1; - }; -} +Element.prototype.matches = Element.prototype.matches || + Element.prototype.matchesSelector || + Element.prototype.mozMatchesSelector || + Element.prototype.msMatchesSelector || + Element.prototype.oMatchesSelector || + Element.prototype.webkitMatchesSelector || + function (s) { + const matches = (this.document || this.ownerDocument).querySelectorAll(s); + let i = matches.length; + while (--i >= 0 && matches.item(i) !== this) {} + return i > -1; + }; diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 index 510ac77009b..e810ee85bd3 100644 --- a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 +++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 @@ -53,16 +53,18 @@ this.defaultAction = this.options.defaultAction; this.action = this.options.action || this.defaultAction; - this.currentLocation = window.location; - if (this.action === 'show') { this.action = this.defaultAction; } + this.currentLocation = window.location; + + const tabSelector = `${this.options.parentEl} a[data-toggle="tab"]`; + // since this is a custom event we need jQuery :( $(document) - .off('shown.bs.tab', `${this.options.parentEl} a[data-toggle="tab"]`) - .on('shown.bs.tab', `${this.options.parentEl} a[data-toggle="tab"]`, evt => this.tabShown(evt)); + .off('shown.bs.tab', tabSelector) + .on('shown.bs.tab', tabSelector, e => this.tabShown(e)); this.activateTab(this.action); } @@ -90,7 +92,7 @@ copySource.replace(/\/+$/, ''); - const newState = copySource + this.currentLocation.search + this.currentLocation.hash; + const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`; history.replaceState({ turbolinks: true, diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 index 97e06bc6d68..72c6c4a1fcd 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 @@ -7,7 +7,7 @@ constructor(options) { if (options.initTabs && options.tabsOptions) { - new window.gl.LinkedTabs(options.tabsOptions); + new global.LinkedTabs(options.tabsOptions); } this.addMarginToBuildColumns(); -- cgit v1.2.1 From 374033fe26013c685157ac0a3cd2a2b40f992ef5 Mon Sep 17 00:00:00 2001 From: Drew Blessing <drew@gitlab.com> Date: Wed, 30 Nov 2016 11:23:04 -0600 Subject: Improve the `Gitlab::OAuth::User` error message The error saving the user is logged to application.log. Previously, the entry had no context and was unusable - 'Error saving user: [Email address already taken]'. Adding the auth hash UID and email makes the error more helpful. --- lib/gitlab/o_auth/user.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index a8b4dc2a83f..96ed20af918 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -39,7 +39,7 @@ module Gitlab log.info "(#{provider}) saving user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}" gl_user rescue ActiveRecord::RecordInvalid => e - log.info "(#{provider}) Error saving user: #{gl_user.errors.full_messages}" + log.info "(#{provider}) Error saving user #{auth_hash.uid} (#{auth_hash.email}): #{gl_user.errors.full_messages}" return self, e.record.errors end -- cgit v1.2.1 From 2cf595dd6c2359d835e22f0948c9b63f3e896565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Wed, 30 Nov 2016 18:26:18 +0100 Subject: Refactor branch chooser in issuable form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- app/views/shared/issuable/_form.html.haml | 25 +----------------- .../shared/issuable/form/_branch_chooser.html.haml | 30 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 24 deletions(-) create mode 100644 app/views/shared/issuable/form/_branch_chooser.html.haml diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 3d515a05d46..2f05093f435 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -40,30 +40,7 @@ title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' } = icon('question-circle') -- if issuable.is_a?(MergeRequest) && !issuable.closed_without_fork? - %hr - - if @merge_request.new_record? - .form-group - = form.label :source_branch, class: 'control-label' - .col-sm-10 - .issuable-form-select-holder - = form.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true }) - .form-group - = form.label :target_branch, class: 'control-label' - .col-sm-10 - .issuable-form-select-holder - = form.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} }) - - if @merge_request.new_record? -   - = link_to 'Change branches', mr_change_branches_path(@merge_request) - - if @merge_request.can_remove_source_branch?(current_user) - .form-group - .col-sm-10.col-sm-offset-2 - .checkbox - = label_tag 'merge_request[force_remove_source_branch]' do - = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil - = check_box_tag 'merge_request[force_remove_source_branch]', '1', @merge_request.force_remove_source_branch? - Remove source branch when merge request is accepted. += render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form - is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?) .row-content-block{class: (is_footer ? "footer-block" : "middle-block")} diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml new file mode 100644 index 00000000000..b757893ea04 --- /dev/null +++ b/app/views/shared/issuable/form/_branch_chooser.html.haml @@ -0,0 +1,30 @@ +- issuable = local_assigns.fetch(:issuable) +- form = local_assigns.fetch(:form) + +- return unless issuable.is_a?(MergeRequest) +- return if issuable.closed_without_fork? + +%hr +- if issuable.new_record? + .form-group + = form.label :source_branch, class: 'control-label' + .col-sm-10 + .issuable-form-select-holder + = form.select(:source_branch, [issuable.source_branch], {}, { class: 'source_branch select2 span2', disabled: true }) +.form-group + = form.label :target_branch, class: 'control-label' + .col-sm-10 + .issuable-form-select-holder + = form.select(:target_branch, issuable.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: issuable.new_record?, data: { placeholder: "Select branch" }}) + - if issuable.new_record? +   + = link_to 'Change branches', mr_change_branches_path(issuable) + +- if issuable.can_remove_source_branch?(current_user) + .form-group + .col-sm-10.col-sm-offset-2 + .checkbox + = label_tag 'merge_request[force_remove_source_branch]' do + = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil + = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch? + Remove source branch when merge request is accepted. -- cgit v1.2.1 From d6b9b21e6db3c32e0f272ab96486876fa8b54d1b Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Tue, 29 Nov 2016 16:59:25 -0200 Subject: Allow access to the wiki with git when repository feature disabled --- app/policies/project_policy.rb | 3 +++ lib/gitlab/git_access.rb | 6 +++++- lib/gitlab/git_access_wiki.rb | 8 ++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 1ee31023e26..8ac4bd9df6d 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -50,6 +50,7 @@ class ProjectPolicy < BasePolicy def reporter_access! can! :download_code + can! :download_wiki_code can! :fork_project can! :create_project_snippet can! :update_issue @@ -187,6 +188,7 @@ class ProjectPolicy < BasePolicy unless project.feature_available?(:wiki, user) || project.has_external_wiki? cannot!(*named_abilities(:wiki)) + cannot!(:download_wiki_code) end unless project.feature_available?(:builds, user) && repository_enabled @@ -226,6 +228,7 @@ class ProjectPolicy < BasePolicy can! :read_commit_status can! :read_container_image can! :download_code + can! :download_wiki_code can! :read_cycle_analytics # NOTE: may be overridden by IssuePolicy diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index bcbf6455998..db07b7c5fcc 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -46,7 +46,7 @@ module Gitlab def download_access_check if user user_download_access_check - elsif deploy_key.nil? && !Guest.can?(:download_code, project) + elsif deploy_key.nil? && !guest_can_downlod_code? raise UnauthorizedError, ERROR_MESSAGES[:download] end end @@ -59,6 +59,10 @@ module Gitlab end end + def guest_can_downlod_code? + Guest.can?(:download_code, project) + end + def user_download_access_check unless user_can_download_code? || build_can_download_code? raise UnauthorizedError, ERROR_MESSAGES[:download] diff --git a/lib/gitlab/git_access_wiki.rb b/lib/gitlab/git_access_wiki.rb index f71d3575909..2c06c4ff1ef 100644 --- a/lib/gitlab/git_access_wiki.rb +++ b/lib/gitlab/git_access_wiki.rb @@ -1,5 +1,13 @@ module Gitlab class GitAccessWiki < GitAccess + def guest_can_downlod_code? + Guest.can?(:download_wiki_code, project) + end + + def user_can_download_code? + authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_wiki_code) + end + def change_access_check(change) if user_access.can_do_action?(:create_wiki) build_status_object(true) -- cgit v1.2.1 From 42c332689deb2fcbb3eb71d5134d583f4518b65f Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Tue, 29 Nov 2016 17:30:14 -0200 Subject: Improve ProjectPolicy spec to check permissions when wiki is disabled --- spec/policies/project_policy_spec.rb | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 96249a7d8c3..b49e4f3a8bc 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -23,7 +23,7 @@ describe ProjectPolicy, models: true do :download_code, :fork_project, :create_project_snippet, :update_issue, :admin_issue, :admin_label, :admin_list, :read_commit_status, :read_build, :read_container_image, :read_pipeline, :read_environment, :read_deployment, - :read_merge_request + :read_merge_request, :download_wiki_code ] end @@ -56,7 +56,8 @@ describe ProjectPolicy, models: true do let(:public_permissions) do [ :download_code, :fork_project, :read_commit_status, :read_pipeline, - :read_container_image, :build_download_code, :build_read_container_image + :read_container_image, :build_download_code, :build_read_container_image, + :download_wiki_code ] end @@ -87,6 +88,15 @@ describe ProjectPolicy, models: true do expect(Ability.allowed?(user, :read_issue, project)).to be_falsy end + it 'does not include the wiki permissions when the feature is disabled' do + project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED) + wiki_permissions = [:read_wiki, :create_wiki, :update_wiki, :admin_wiki, :download_wiki_code] + + permissions = described_class.abilities(owner, project).to_set + + expect(permissions).not_to include(*wiki_permissions) + end + context 'abilities for non-public projects' do let(:project) { create(:empty_project, namespace: owner.namespace) } -- cgit v1.2.1 From e7d794770afeac37b8e2fe53d7f86c9e418e870a Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Tue, 29 Nov 2016 17:31:42 -0200 Subject: Improve Gitlab::GitAccessWiki spec with download access checks --- spec/lib/gitlab/git_access_wiki_spec.rb | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb index 576aa5c366f..578db51631e 100644 --- a/spec/lib/gitlab/git_access_wiki_spec.rb +++ b/spec/lib/gitlab/git_access_wiki_spec.rb @@ -26,4 +26,29 @@ describe Gitlab::GitAccessWiki, lib: true do def changes ['6f6d7e7ed 570e7b2ab refs/heads/master'] end + + describe '#download_access_check' do + subject { access.check('git-upload-pack', '_any') } + + before do + project.team << [user, :developer] + end + + context 'when wiki feature is enabled' do + it 'give access to download wiki code' do + project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::ENABLED) + + expect(subject.allowed?).to be_truthy + end + end + + context 'when wiki feature is disabled' do + it 'does not give access to download wiki code' do + project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED) + + expect(subject.allowed?).to be_falsey + expect(subject.message).to match(/You are not allowed to download code/) + end + end + end end -- cgit v1.2.1 From 28688b5456f40cd45bfbc78e20e9d1d975e4aa60 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Tue, 29 Nov 2016 17:32:15 -0200 Subject: Add CHANGELOG entry --- .../fix-git-access-wiki-when-repository-feature-disabled.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-git-access-wiki-when-repository-feature-disabled.yml diff --git a/changelogs/unreleased/fix-git-access-wiki-when-repository-feature-disabled.yml b/changelogs/unreleased/fix-git-access-wiki-when-repository-feature-disabled.yml new file mode 100644 index 00000000000..82ca6316876 --- /dev/null +++ b/changelogs/unreleased/fix-git-access-wiki-when-repository-feature-disabled.yml @@ -0,0 +1,4 @@ +--- +title: Allow access to the wiki with git when repository feature disabled +merge_request: +author: -- cgit v1.2.1 From 0619fb629dec69f28cb12b6ad896bbb38c841700 Mon Sep 17 00:00:00 2001 From: Matt Lee <mattl@gitlab.com> Date: Wed, 30 Nov 2016 13:41:18 -0500 Subject: WIP: Adds a default commit message when adding a README (#25167) --- app/views/projects/empty.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 7a39064adc5..c0a83091c8c 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -16,7 +16,7 @@ %p Otherwise you can start with adding a = succeed ',' do - = link_to "README", new_readme_path, class: 'underlined-link' + = link_to "README", add_special_file_path(@project, file_name: 'README.md'), class: 'underlined-link' a = succeed ',' do = link_to "LICENSE", add_special_file_path(@project, file_name: 'LICENSE'), class: 'underlined-link' -- cgit v1.2.1 From 66a2e7bdf371a87badea845fcb86b8f60bee4c15 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Wed, 30 Nov 2016 20:40:53 +0100 Subject: Refactor the Git submodules with CI docs [ci skip] --- doc/ci/README.md | 1 + doc/ci/git_submodules.md | 86 ++++++++++++++++ doc/user/permissions.md | 5 +- doc/user/project/new_ci_build_permissions_model.md | 114 ++------------------- 4 files changed, 96 insertions(+), 110 deletions(-) create mode 100644 doc/ci/git_submodules.md diff --git a/doc/ci/README.md b/doc/ci/README.md index 545cc72682d..db236ce2a66 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -21,6 +21,7 @@ - [CI services (linked docker containers)](services/README.md) - [CI/CD pipelines settings](../user/project/pipelines/settings.md) - [Review Apps](review_apps/index.md) +- [Git submodules](git_submodules.md) Using Git submodules in your CI jobs ## Breaking changes diff --git a/doc/ci/git_submodules.md b/doc/ci/git_submodules.md new file mode 100644 index 00000000000..1d782200cca --- /dev/null +++ b/doc/ci/git_submodules.md @@ -0,0 +1,86 @@ +# Using Git submodules with GitLab CI + +> **Notes:** +- GitLab 8.12 introduced a new [CI build permissions model][newperms] and you + are encouraged to upgrade your GitLab instance if you haven't done already. + If you are **not** using GitLab 8.12 or higher, you would need to work your way + around submodules in order to access the sources of e.g., `gitlab.com/group/project` + with the use of [SSH keys](ssh_keys/README.md). +- With GitLab 8.12 onward, your permissions are used to evaluate what a CI build + can access. More information about how this system works can be found in the + [Build permissions model](../user/permissions.md#builds-permissions). +- The HTTP(S) Git protocol [must be enabled][gitpro] in your GitLab instance. + +## Configuring the `.gitmodules` file + +If dealing with [Git submodules][gitscm], your project will probably have a file +named `.gitmodules`. + +Let's consider the following example: + +1. Your project is located at `https://gitlab.com/secret-group/my-project`. +1. To checkout your sources you usually use an SSH address like + `git@gitlab.com:secret-group/my-project.git`. +1. Your project depends on `https://gitlab.com/group/project`, which you want + to include as a submodule. + +If you are using GitLab 8.12+ and your submodule is on the same GitLab server, +you must update your `.gitmodules` file to use **relative URLs**. +Since Git allows the usage of relative URLs for your `.gitmodules` configuration, +this easily allows you to use HTTP(S) for cloning all your CI builds and SSH +for all your local checkouts. The `.gitmodules` would look like: + +```ini +[submodule "project"] + path = project + url = ../../group/project.git +``` + +The above configuration will instruct Git to automatically deduce the URL that +should be used when cloning sources. Whether you use HTTP(S) or SSH, Git will use +that same channel and it will allow to make all your CI builds use HTTP(S) +(because GitLab CI only uses HTTP(S) for cloning your sources), and all your local +clones will continue using SSH. + +For all other submodules not located on the same GitLab server, use the full +HTTP(S) protocol URL: + +```ini +[submodule "project-x"] + path = project-x + url = https://gitserver.com/group/project-x.git +``` + +Once `.gitmodules` is correctly configured, you can move on to +[configuring your `.gitlab-ci.yml`](#using-git-submodules-in-your-ci-jobs). + +## Using Git submodules in your CI jobs + +There are a few steps you need to take in order to make submodules work +correctly with your CI builds: + +1. First, make sure you have used [relative URLs](#configuring-the-gitmodules-file) + for the submodules located in the same GitLab server. +1. Then, use `git submodule sync/update` in `before_script`: + + ```yaml + before_script: + - git submodule sync --recursive + - git submodule update --init --recursive + ``` + + `--recursive` should be used in either both or none (`sync/update`) depending on + whether you have recursive submodules. + +The rationale to set the `sync` and `update` in `before_script` is because of +the way Git submodules work. On a fresh Runner workspace, Git will set the +submodule URL including the token in `.git/config` +(or `.git/modules/<submodule>/config`) based on `.gitmodules` and the current +remote URL. On subsequent builds on the same Runner, `.git/config` is cached +and already contains a full URL for the submodule, corresponding to the previous +build, and to **a token from a previous build**. `sync` allows to force updating +the full URL. + +[gitpro]: ../user/admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols +[gitscm]: https://git-scm.com/book/en/v2/Git-Tools-Submodules "Git submodules documentation" +[newperms]: ../user/project/new_ci_build_permissions_model.md diff --git a/doc/user/permissions.md b/doc/user/permissions.md index cea78864df2..39fe2409a29 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -141,10 +141,9 @@ instance and project. In addition, all admins can use the admin interface under | See events in the system | | | | ✓ | | Admin interface | | | | ✓ | -### Build permissions - -> Changed in GitLab 8.12. +### Builds permissions +>**Note:** GitLab 8.12 has a completely redesigned build permissions system. Read all about the [new model and its implications][new-mod]. diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md index 60b7bec2ba7..4f12acb8398 100644 --- a/doc/user/project/new_ci_build_permissions_model.md +++ b/doc/user/project/new_ci_build_permissions_model.md @@ -34,9 +34,9 @@ as created be the pusher (local push or via the UI) and any build created in thi pipeline will have the permissions of the pusher. This allows us to make it really easy to evaluate the access for all projects -that have Git submodules or are using container images that the pusher would -have access too. **The permission is granted only for time that build is running. -The access is revoked after the build is finished.** +that have [Git submodules][gitsub] or are using container images that the pusher +would have access too. **The permission is granted only for time that build is +running. The access is revoked after the build is finished.** ## Types of users @@ -141,7 +141,7 @@ with GitLab 8.12. With the new build permissions model, there is now an easy way to access all dependent source code in a project. That way, we can: -1. Access a project's Git submodules +1. Access a project's [Git submodules][gitsub] 1. Access private container images 1. Access project's and submodule LFS objects @@ -179,108 +179,8 @@ As a user: ### Git submodules -> -It often happens that while working on one project, you need to use another -project from within it; perhaps it’s a library that a third party developed or -you’re developing a project separately and are using it in multiple parent -projects. -A common issue arises in these scenarios: you want to be able to treat the two -projects as separate yet still be able to use one from within the other. -> -_Excerpt from the [Git website][git-scm] about submodules._ - -If dealing with submodules, your project will probably have a file named -`.gitmodules`. And this is how it usually looks like: - -``` -[submodule "tools"] - path = tools - url = git@gitlab.com/group/tools.git -``` - -> **Note:** -If you are **not** using GitLab 8.12 or higher, you would need to work your way -around this issue in order to access the sources of `gitlab.com/group/tools` -(e.g., use [SSH keys](../ssh_keys/README.md)). -> -With GitLab 8.12 onward, your permissions are used to evaluate what a CI build -can access. More information about how this system works can be found in the -[Build permissions model](../../user/permissions.md#builds-permissions). - -To make use of the new changes, you have to update your `.gitmodules` file to -use a relative URL. - -Let's consider the following example: - -1. Your project is located at `https://gitlab.com/secret-group/my-project`. -1. To checkout your sources you usually use an SSH address like - `git@gitlab.com:secret-group/my-project.git`. -1. Your project depends on `https://gitlab.com/group/tools`. -1. You have the `.gitmodules` file with above content. - -Since Git allows the usage of relative URLs for your `.gitmodules` configuration, -this easily allows you to use HTTP for cloning all your CI builds and SSH -for all your local checkouts. - -For example, if you change the `url` of your `tools` dependency, from -`git@gitlab.com/group/tools.git` to `../../group/tools.git`, this will instruct -Git to automatically deduce the URL that should be used when cloning sources. -Whether you use HTTP or SSH, Git will use that same channel and it will allow -to make all your CI builds use HTTPS (because GitLab CI uses HTTPS for cloning -your sources), and all your local clones will continue using SSH. - -Given the above explanation, your `.gitmodules` file should eventually look -like this: - -``` -[submodule "tools"] - path = tools - url = ../../group/tools.git -``` - -However, you have to explicitly tell GitLab CI to clone your submodules as this -is not done automatically. You can achieve that by adding a `before_script` -section to your `.gitlab-ci.yml`: - -``` -before_script: - - git submodule update --init --recursive - -test: - script: - - run-my-tests -``` - -This will make GitLab CI initialize (fetch) and update (checkout) all your -submodules recursively. - -If Git does not use the newly added relative URLs but still uses your old URLs, -you might need to add `git submodule sync --recursive` to your `.gitlab-ci.yml`, -prior to running `git submodule update --init --recursive`. This transfers the -changes from your `.gitmodules` file into the `.git` folder, which is kept by -runners between runs. - -In case your environment or your Docker image doesn't have Git installed, -you have to either ask your Administrator or install the missing dependency -yourself: - -``` -# Debian / Ubuntu -before_script: - - apt-get update -y - - apt-get install -y git-core - - git submodule update --init --recursive - -# CentOS / RedHat -before_script: - - yum install git - - git submodule update --init --recursive - -# Alpine -before_script: - - apk add -U git - - git submodule update --init --recursive -``` +To properly configure submodules with GitLab CI, read the +[Git submodules documentation][gitsub]. ### Container Registry @@ -310,7 +210,7 @@ test: [build permissions]: ../permissions.md#builds-permissions [comment]: https://gitlab.com/gitlab-org/gitlab-ce/issues/22484#note_16648302 [ext]: ../permissions.md#external-users -[git-scm]: https://git-scm.com/book/en/v2/Git-Tools-Submodules +[gitsub]: ../../ci/git_submodules.md [https]: ../admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols [triggers]: ../../ci/triggers/README.md [update-docs]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update -- cgit v1.2.1 From a167897bed66ed2b9aafad7020d75334e2badf32 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Mon, 14 Nov 2016 15:33:21 -0600 Subject: move wiki navbar content to right sidebar --- app/assets/javascripts/dispatcher.js.es6 | 2 +- app/assets/javascripts/wikis.js | 38 -------------- app/assets/javascripts/wikis.js.es6 | 70 ++++++++++++++++++++++++++ app/assets/stylesheets/framework/sidebar.scss | 2 +- app/assets/stylesheets/pages/wiki.scss | 71 +++++++++++++++++++++++++++ app/helpers/nav_helper.rb | 6 +++ app/views/projects/wikis/_nav.html.haml | 16 ------ app/views/projects/wikis/_sidebar.html.haml | 19 +++++++ app/views/projects/wikis/edit.html.haml | 6 ++- app/views/projects/wikis/git_access.html.haml | 8 ++- app/views/projects/wikis/history.html.haml | 6 ++- app/views/projects/wikis/pages.html.haml | 12 ++++- app/views/projects/wikis/show.html.haml | 6 ++- 13 files changed, 199 insertions(+), 63 deletions(-) delete mode 100644 app/assets/javascripts/wikis.js create mode 100644 app/assets/javascripts/wikis.js.es6 delete mode 100644 app/views/projects/wikis/_nav.html.haml create mode 100644 app/views/projects/wikis/_sidebar.html.haml diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 16df4b0b005..147634f1cc4 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -262,7 +262,7 @@ new NotificationsDropdown(); break; case 'wikis': - new Wikis(); + new gl.Wikis(); shortcut_handler = new ShortcutsNavigation(); new ZenMode(); new GLForm($('.wiki-form')); diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js deleted file mode 100644 index 5dd853389c2..00000000000 --- a/app/assets/javascripts/wikis.js +++ /dev/null @@ -1,38 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, consistent-return, one-var, one-var-declaration-per-line, no-undef, prefer-template, padded-blocks, max-len */ - -/*= require latinise */ - -(function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - this.Wikis = (function() { - function Wikis() { - this.slugify = bind(this.slugify, this); - $('.new-wiki-page').on('submit', (function(_this) { - return function(e) { - var field, path, slug; - $('[data-error~=slug]').addClass('hidden'); - field = $('#new_wiki_path'); - slug = _this.slugify(field.val()); - if (slug.length > 0) { - path = field.attr('data-wikis-path'); - location.href = path + '/' + slug; - return e.preventDefault(); - } - }; - })(this)); - } - - Wikis.prototype.dasherize = function(value) { - return value.replace(/[_\s]+/g, '-'); - }; - - Wikis.prototype.slugify = function(value) { - return this.dasherize(value.trim().toLowerCase().latinise()); - }; - - return Wikis; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js.es6 new file mode 100644 index 00000000000..e246ed6828f --- /dev/null +++ b/app/assets/javascripts/wikis.js.es6 @@ -0,0 +1,70 @@ +/* eslint-disable no-param-reassign */ +/* global Breakpoints */ + +/*= require latinise */ +/*= require breakpoints */ + +((global) => { + const dasherize = str => str.replace(/[_\s]+/g, '-'); + const slugify = str => dasherize(str.trim().toLowerCase().latinise()); + + class Wikis { + constructor() { + this.bp = Breakpoints.get(); + this.sidebarEl = document.querySelector('.js-wiki-sidebar'); + this.sidebarExpanded = false; + + const sidebarToggles = document.querySelectorAll('.js-sidebar-wiki-toggle'); + for (const toggle of sidebarToggles) { + toggle.addEventListener('click', e => this.handleToggleSidebar(e)); + } + + this.newWikiForm = document.querySelector('form.new-wiki-page'); + if (this.newWikiForm) { + this.newWikiForm.addEventListener('submit', e => this.handleNewWikiSubmit(e)); + } + + window.addEventListener('resize', () => this.renderSidebar()); + this.renderSidebar(); + } + + handleNewWikiSubmit(event) { + if (!this.newWikiForm) return; + + const slugInput = this.newWikiForm.querySelector('#new_wiki_path'); + const slug = slugify(slugInput.value); + + if (slug.length > 0) { + const wikisPath = slugInput.getAttribute('data-wikis-path'); + window.location.href = `${wikisPath}/${slug}`; + event.preventDefault(); + } + } + + handleToggleSidebar(event) { + event.preventDefault(); + this.sidebarExpanded = !this.sidebarExpanded; + this.renderSidebar(); + } + + sidebarCanCollapse() { + const bootstrapBreakpoint = this.bp.getBreakpointSize(); + return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm'; + } + + renderSidebar() { + const { classList } = this.sidebarEl; + if (this.sidebarExpanded || !this.sidebarCanCollapse()) { + if (!classList.contains('right-sidebar-expanded')) { + classList.remove('right-sidebar-collapsed'); + classList.add('right-sidebar-expanded'); + } + } else if (classList.contains('right-sidebar-expanded')) { + classList.add('right-sidebar-collapsed'); + classList.remove('right-sidebar-expanded'); + } + } + } + + global.Wikis = Wikis; +})(window.gl || (window.gl = {})); diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 44c445c0543..45602e2d517 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -220,7 +220,7 @@ header.header-sidebar-pinned { padding-right: 0; @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { - &:not(.build-sidebar) { + &:not(.build-sidebar):not(.wiki-sidebar) { padding-right: $sidebar_collapsed_width; } } diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index dfaeba41cf6..b331eec0983 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -4,3 +4,74 @@ margin-right: auto; padding-right: 7px; } + +.top-area { + position: relative; + + &.sub-header-block { + padding-right: 40px; + } + + button.sidebar-toggle { + position: absolute; + right: 0; + top: 11px; + display: block; + } + + @media (min-width: $screen-sm-min) { + padding-right: 40px; + } + + @media (min-width: $screen-md-min) { + &, + &.sub-header-block { + padding-right: 0; + } + + button.sidebar-toggle { + display: none; + } + } +} + +.right-sidebar.wiki-sidebar { + padding: $gl-padding 0; + + &.right-sidebar-collapsed { + display: none; + } + + .blocks-container { + padding: 0 $gl-padding; + } + + .block { + width: 100%; + } + + a { + color: $layout-link-gray; + + &:hover, + &.active { + color: $black; + } + } + + ul.wiki-pages, + ul.wiki-pages li { + list-style: none; + display: block; + padding: 0; + margin: 0; + } + + .wiki-sidebar-header { + padding: 0 $gl-padding $gl-padding; + + .gutter-toggle { + margin-top: 0; + } + } +} diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index df87fac132d..2aeab4b6b62 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -20,6 +20,12 @@ module NavHelper end elsif current_path?('builds#show') "page-gutter build-sidebar right-sidebar-expanded" + elsif current_path?('wikis#show') || + current_path?('wikis#edit') || + current_path?('wikis#history') || + current_path?('wikis#pages') || + current_path?('wikis#git_access') + "page-gutter wiki-sidebar right-sidebar-expanded" end end diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml deleted file mode 100644 index afdef70e1cf..00000000000 --- a/app/views/projects/wikis/_nav.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -= content_for :sub_nav do - .scrolling-tabs-container.sub-nav-scroll - = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs - %ul{ class: (container_class) } - = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do - = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) - - = nav_link(path: 'wikis#pages') do - = link_to 'Pages', namespace_project_wikis_pages_path(@project.namespace, @project) - - = nav_link(path: 'wikis#git_access') do - = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do - Git Access - - = render 'projects/wikis/new' diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml new file mode 100644 index 00000000000..f833b844df4 --- /dev/null +++ b/app/views/projects/wikis/_sidebar.html.haml @@ -0,0 +1,19 @@ +%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar + .block.wiki-sidebar-header.append-bottom-default + %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-wiki-toggle{ href: "#" } + = icon('angle-double-right') + + = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do + = succeed ' ' do + = icon('cloud-download') + Clone repository + + .blocks-container + .block.block-first + %ul.wiki-pages + %li + = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) + %li + = link_to 'Pages', namespace_project_wikis_pages_path(@project.namespace, @project) + += render 'projects/wikis/new' diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 679d6018bef..7483af54d76 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -1,9 +1,11 @@ - @no_container = true - page_title "Edit", @page.title.capitalize, "Wiki" -= render 'nav' %div{ class: container_class } .top-area + %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } + = icon('angle-double-left') + .nav-text %strong - if @page.persisted? @@ -21,3 +23,5 @@ New Page = render 'form' + += render 'sidebar' diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index b8811a28dd6..871a67b87cd 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -1,9 +1,11 @@ - @no_container = true - page_title "Git Access", "Wiki" -= render 'nav' %div{ class: container_class } - .sub-header-block + .top-area.sub-header-block + %button.btn.btn-default.visible-xs.visible-sm.pull-right.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } + = icon('angle-double-left') + %span.oneline Git access for %strong= @project_wiki.path_with_namespace @@ -32,3 +34,5 @@ >> Thin web server (v1.5.0 codename Knife) >> Maximum connections set to 1024 >> Listening on 0.0.0.0:4567, CTRL+C to stop + += render 'sidebar' diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index 4c0b14e2c42..58ef26d215a 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -1,7 +1,9 @@ - page_title "History", @page.title.capitalize, "Wiki" -= render 'nav' + %div{ class: container_class } .top-area + %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } + = icon('angle-double-left') .nav-text %strong = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page) @@ -35,3 +37,5 @@ %td %strong = @page.page.wiki.page(@page.page.name, commit.id).try(:format) + += render 'sidebar' diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index 9c10acd4cb6..e843b1fb3d3 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -1,9 +1,15 @@ - @no_container = true - page_title "Pages", "Wiki" -= render 'nav' - %div{ class: container_class } + .top-area + %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } + = icon('angle-double-left') + + .nav-text + %strong + Wiki Pages + %ul.content-list - @wiki_pages.each do |wiki_page| %li @@ -12,3 +18,5 @@ .pull-right %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)} = paginate @wiki_pages, theme: 'gitlab' + += render 'sidebar' diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 5cebb538cf5..f331201b52e 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -1,9 +1,11 @@ - @no_container = true - page_title @page.title.capitalize, "Wiki" -= render 'nav' %div{ class: container_class } .top-area + %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } + = icon('angle-double-left') + .nav-text %strong= @page.title.capitalize @@ -24,3 +26,5 @@ .wiki = preserve do = render_wiki_content(@page) + += render 'sidebar' -- cgit v1.2.1 From 48f4f76c16352278600174c13744b3ce8c026f1a Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 15 Nov 2016 01:44:43 -0600 Subject: add active states to sidebar nav --- app/assets/stylesheets/pages/wiki.scss | 9 ++++++++- app/views/projects/wikis/_sidebar.html.haml | 8 +++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index b331eec0983..78edff35aa7 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -59,14 +59,21 @@ } } + .active > a { + color: $black; + } + ul.wiki-pages, ul.wiki-pages li { list-style: none; - display: block; padding: 0; margin: 0; } + ul.wiki-pages li { + margin: 5px 0 10px; + } + .wiki-sidebar-header { padding: 0 $gl-padding $gl-padding; diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml index f833b844df4..4e0fe46f8fc 100644 --- a/app/views/projects/wikis/_sidebar.html.haml +++ b/app/views/projects/wikis/_sidebar.html.haml @@ -3,7 +3,8 @@ %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-wiki-toggle{ href: "#" } = icon('angle-double-right') - = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do + - git_access_url = namespace_project_wikis_git_access_path(@project.namespace, @project) + = link_to git_access_url, class: active_nav_link?(path: 'wikis#git_access') ? 'active' : '' do = succeed ' ' do = icon('cloud-download') Clone repository @@ -11,9 +12,10 @@ .blocks-container .block.block-first %ul.wiki-pages - %li + %li{ class: params[:id] == 'home' ? 'active' : '' } = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) - %li + + %li{ class: active_nav_link?(path: 'wikis#pages') ? 'active' : '' } = link_to 'Pages', namespace_project_wikis_pages_path(@project.namespace, @project) = render 'projects/wikis/new' -- cgit v1.2.1 From 9fa6512501f03f844cb0a9393de78555e963afb4 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 15 Nov 2016 01:45:03 -0600 Subject: update design of wiki git access page --- app/assets/stylesheets/pages/wiki.scss | 40 ++++++++++++++++++----- app/views/projects/wikis/git_access.html.haml | 46 +++++++++++++-------------- 2 files changed, 54 insertions(+), 32 deletions(-) diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index 78edff35aa7..ab9c2e2e3e1 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -8,10 +8,6 @@ .top-area { position: relative; - &.sub-header-block { - padding-right: 40px; - } - button.sidebar-toggle { position: absolute; right: 0; @@ -24,10 +20,7 @@ } @media (min-width: $screen-md-min) { - &, - &.sub-header-block { - padding-right: 0; - } + padding-right: 0; button.sidebar-toggle { display: none; @@ -35,6 +28,37 @@ } } +.wiki-page-header { + @extend .top-area; + + .git-access-header { + padding: 16px 0 11px; + line-height: 28px; + font-size: 18px; + } + + .git-clone-holder { + width: 480px; + padding-bottom: 40px; + } + + @media (max-width: $screen-xs-max) { + .git-clone-holder { + width: 100%; + } + } +} + +.wiki-git-access { + margin: $gl-padding 0; + + h3 { + font-size: 22px; + font-weight: normal; + margin-top: 1.4em; + } +} + .right-sidebar.wiki-sidebar { padding: $gl-padding 0; diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index 871a67b87cd..c148f65eb3e 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -2,37 +2,35 @@ - page_title "Git Access", "Wiki" %div{ class: container_class } - .top-area.sub-header-block + .wiki-page-header %button.btn.btn-default.visible-xs.visible-sm.pull-right.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') - %span.oneline - Git access for + .git-access-header + Clone repository %strong= @project_wiki.path_with_namespace - .pull-right - = render "shared/clone_panel", project: @project_wiki + = render "shared/clone_panel", project: @project_wiki - .prepend-top-default - %fieldset - %legend Install Gollum: - %pre.dark - :preserve - gem install gollum + .wiki-git-access + %h3 Install Gollum + %pre.dark + :preserve + gem install gollum - %legend Clone Your Wiki: - %pre.dark - :preserve - git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')} - cd #{h @project_wiki.path} + %h3 Clone your wiki + %pre.dark + :preserve + git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')} + cd #{h @project_wiki.path} - %legend Start Gollum And Edit Locally: - %pre.dark - :preserve - gollum - == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin - >> Thin web server (v1.5.0 codename Knife) - >> Maximum connections set to 1024 - >> Listening on 0.0.0.0:4567, CTRL+C to stop + %h3 Start Gollum and edit locally + %pre.dark + :preserve + gollum + == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin + >> Thin web server (v1.5.0 codename Knife) + >> Maximum connections set to 1024 + >> Listening on 0.0.0.0:4567, CTRL+C to stop = render 'sidebar' -- cgit v1.2.1 From 8e23d7ad84db730413b1df66117775b241dfe38c Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 15 Nov 2016 02:04:39 -0600 Subject: allow wiki sidebar to scroll on overflow --- app/assets/javascripts/wikis.js.es6 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js.es6 index e246ed6828f..b5626f5bd81 100644 --- a/app/assets/javascripts/wikis.js.es6 +++ b/app/assets/javascripts/wikis.js.es6 @@ -3,6 +3,7 @@ /*= require latinise */ /*= require breakpoints */ +/*= require jquery.nicescroll */ ((global) => { const dasherize = str => str.replace(/[_\s]+/g, '-'); @@ -13,6 +14,7 @@ this.bp = Breakpoints.get(); this.sidebarEl = document.querySelector('.js-wiki-sidebar'); this.sidebarExpanded = false; + $(this.sidebarEl).niceScroll(); const sidebarToggles = document.querySelectorAll('.js-sidebar-wiki-toggle'); for (const toggle of sidebarToggles) { -- cgit v1.2.1 From a1518055723459cf042327e1f9a4cf5004a0d301 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 15 Nov 2016 02:19:08 -0600 Subject: add wiki page list to sidebar --- app/views/projects/wikis/_sidebar.html.haml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml index 4e0fe46f8fc..85604c69865 100644 --- a/app/views/projects/wikis/_sidebar.html.haml +++ b/app/views/projects/wikis/_sidebar.html.haml @@ -12,10 +12,9 @@ .blocks-container .block.block-first %ul.wiki-pages - %li{ class: params[:id] == 'home' ? 'active' : '' } - = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) - - %li{ class: active_nav_link?(path: 'wikis#pages') ? 'active' : '' } - = link_to 'Pages', namespace_project_wikis_pages_path(@project.namespace, @project) + - @project_wiki.pages.each do |wiki_page| + %li{ class: params[:id] == wiki_page.slug ? 'active' : '' } + = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_page) do + = wiki_page.title.capitalize = render 'projects/wikis/new' -- cgit v1.2.1 From 952bdfae52b5843e3265ad672750765a57c5bc9b Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 15 Nov 2016 03:17:44 -0600 Subject: update wiki pages with new design --- app/assets/stylesheets/pages/wiki.scss | 53 +++++++++++++++++++----------- app/views/projects/wikis/_form.html.haml | 3 -- app/views/projects/wikis/edit.html.haml | 26 ++++++++++----- app/views/projects/wikis/history.html.haml | 11 ++++--- app/views/projects/wikis/pages.html.haml | 4 +-- app/views/projects/wikis/show.html.haml | 11 ++++--- 6 files changed, 64 insertions(+), 44 deletions(-) diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index ab9c2e2e3e1..216b8ab3c7f 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -5,31 +5,27 @@ padding-right: 7px; } -.top-area { +.wiki-page-header { + @extend .top-area; position: relative; - button.sidebar-toggle { - position: absolute; - right: 0; - top: 11px; - display: block; - } - - @media (min-width: $screen-sm-min) { - padding-right: 40px; + .wiki-page-title { + margin: 0; + font-size: 22px; } - @media (min-width: $screen-md-min) { - padding-right: 0; + .wiki-last-edit-by { + color: $gl-gray-light; - button.sidebar-toggle { - display: none; + strong { + color: $gl-text-color; } } -} -.wiki-page-header { - @extend .top-area; + .light { + font-weight: normal; + color: $gl-gray-light; + } .git-access-header { padding: 16px 0 11px; @@ -38,13 +34,30 @@ } .git-clone-holder { - width: 480px; + width: 100%; padding-bottom: 40px; } - @media (max-width: $screen-xs-max) { + button.sidebar-toggle { + position: absolute; + right: 0; + top: 11px; + display: block; + } + + @media (min-width: $screen-sm-min) { + padding-right: 40px; + .git-clone-holder { - width: 100%; + width: 480px; + } + } + + @media (min-width: $screen-md-min) { + padding-right: 0; + + button.sidebar-toggle { + display: none; } } } diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 4e41a15d9f4..c52527332bc 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -34,9 +34,6 @@ - if @page && @page.persisted? = f.submit 'Save changes', class: "btn-save btn" .pull-right - - if can?(current_user, :admin_wiki, @project) - = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-danger btn-grouped" do - Delete = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-cancel btn-grouped" - else = f.submit 'Create page', class: "btn-create btn" diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 7483af54d76..23117128bbb 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -2,25 +2,33 @@ - page_title "Edit", @page.title.capitalize, "Wiki" %div{ class: container_class } - .top-area + .wiki-page-header %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') .nav-text - %strong + %h2.wiki-page-title - if @page.persisted? = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page) - else = @page.title.capitalize - %span.light - · - Edit Page + %span.light + · + - if @page.persisted? + Edit Page + - else + Create Page .nav-controls - - if !(@page && @page.persisted?) - - if can?(current_user, :create_wiki, @project) - = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do - New Page + - if can?(current_user, :create_wiki, @project) + = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do + New Page + - if @page.persisted? + = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do + Page History + - if can?(current_user, :admin_wiki, @project) + = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-danger" do + Delete = render 'form' diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index 58ef26d215a..5f9c3067cc1 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -1,15 +1,16 @@ - page_title "History", @page.title.capitalize, "Wiki" %div{ class: container_class } - .top-area + .wiki-page-header %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') + .nav-text - %strong + %h2.wiki-page-title = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page) - %span.light - · - History + %span.light + · + History .table-holder %table.table diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index e843b1fb3d3..d89f860d9a8 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -2,12 +2,12 @@ - page_title "Pages", "Wiki" %div{ class: container_class } - .top-area + .wiki-page-header %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') .nav-text - %strong + %h2.wiki-page-title Wiki Pages %ul.content-list diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index f331201b52e..28c1c5ab7d1 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -2,16 +2,18 @@ - page_title @page.title.capitalize, "Wiki" %div{ class: container_class } - .top-area + .wiki-page-header %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') .nav-text - %strong= @page.title.capitalize + %h2.wiki-page-title= @page.title.capitalize %span.wiki-last-edit-by - · - last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)} + Last edited by + %strong + #{@page.commit.author.name} + #{time_ago_with_tooltip(@page.commit.authored_date)} .nav-controls = render 'main_links' @@ -21,7 +23,6 @@ This is an old version of this page. You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", namespace_project_wiki_history_path(@project.namespace, @project, @page)}. - .wiki-holder.prepend-top-default.append-bottom-default .wiki = preserve do -- cgit v1.2.1 From e069875e80117020eea5bfe1b06f1a5bf868ffc9 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 15 Nov 2016 11:32:57 -0600 Subject: fix tests broken by new wiki page design --- app/assets/javascripts/wikis.js.es6 | 4 ++-- features/project/wiki.feature | 5 ----- features/steps/project/source/markdown_render.rb | 6 +++--- features/steps/project/wiki.rb | 16 ++++++---------- .../projects/wiki/user_creates_wiki_page_spec.rb | 12 ++++++------ .../projects/wiki/user_updates_wiki_page_spec.rb | 4 ++-- 6 files changed, 19 insertions(+), 28 deletions(-) diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js.es6 index b5626f5bd81..8710ca973c4 100644 --- a/app/assets/javascripts/wikis.js.es6 +++ b/app/assets/javascripts/wikis.js.es6 @@ -17,8 +17,8 @@ $(this.sidebarEl).niceScroll(); const sidebarToggles = document.querySelectorAll('.js-sidebar-wiki-toggle'); - for (const toggle of sidebarToggles) { - toggle.addEventListener('click', e => this.handleToggleSidebar(e)); + for (let i = 0; i < sidebarToggles.length; i += 1) { + sidebarToggles[i].addEventListener('click', e => this.handleToggleSidebar(e)); } this.newWikiForm = document.querySelector('form.new-wiki-page'); diff --git a/features/project/wiki.feature b/features/project/wiki.feature index 63ce3ccb536..a04228de03b 100644 --- a/features/project/wiki.feature +++ b/features/project/wiki.feature @@ -49,7 +49,6 @@ Feature: Project Wiki Scenario: View all pages Given I have an existing wiki page And I browse to that Wiki page - And I click on the "Pages" button Then I should see the existing page in the pages list Scenario: File exists in wiki repo @@ -72,13 +71,11 @@ Feature: Project Wiki @javascript Scenario: New Wiki page that has a path Given I create a New page with paths - And I click on the "Pages" button Then I should see non-escaped link in the pages list @javascript Scenario: Edit Wiki page that has a path Given I create a New page with paths - And I click on the "Pages" button And I edit the Wiki page with a path Then I should see a non-escaped path And I should see the Editing page @@ -88,7 +85,6 @@ Feature: Project Wiki @javascript Scenario: View the page history of a Wiki page that has a path Given I create a New page with paths - And I click on the "Pages" button And I view the page history of a Wiki page that has a path Then I should see a non-escaped path And I should see the page history @@ -96,7 +92,6 @@ Feature: Project Wiki @javascript Scenario: View an old page version of a Wiki page Given I create a New page with paths - And I click on the "Pages" button And I edit the Wiki page with a path Then I should see a non-escaped path And I should see the Editing page diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb index 2134dae168a..dee6a8a5558 100644 --- a/features/steps/project/source/markdown_render.rb +++ b/features/steps/project/source/markdown_render.rb @@ -241,7 +241,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps page.within(:css, ".nav-text") do expect(page).to have_content "Test" - expect(page).to have_content "Edit Page" + expect(page).to have_content "Create Page" end end @@ -258,7 +258,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "api") page.within(:css, ".nav-text") do - expect(page).to have_content "Edit" + expect(page).to have_content "Create" expect(page).to have_content "Api" end end @@ -271,7 +271,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "raketasks") page.within(:css, ".nav-text") do - expect(page).to have_content "Edit" + expect(page).to have_content "Create" expect(page).to have_content "Rake" end end diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb index 07a955b1a14..4cb0a21fbb4 100644 --- a/features/steps/project/wiki.rb +++ b/features/steps/project/wiki.rb @@ -29,7 +29,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps expect(page).to have_content "link test" click_link "link test" - expect(page).to have_content "Edit Page" + expect(page).to have_content "Create Page" end step 'I have an existing Wiki page' do @@ -80,13 +80,9 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps expect(page).to have_content "Page was successfully deleted" end - step 'I click on the "Pages" button' do - click_on "Pages" - end - step 'I should see the existing page in the pages list' do expect(page).to have_content current_user.name - expect(page).to have_content @page.title + expect(find('.wiki-pages')).to have_content @page.title.capitalize end step 'I have an existing Wiki page with images linked on page' do @@ -125,7 +121,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps step 'I should see the new wiki page form' do expect(current_path).to match('wikis/image.jpg') expect(page).to have_content('New Wiki Page') - expect(page).to have_content('Edit Page') + expect(page).to have_content('Create Page') end step 'I create a New page with paths' do @@ -142,8 +138,8 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I edit the Wiki page with a path' do - expect(page).to have_content('three') - click_on 'three' + expect(find('.wiki-pages')).to have_content('Three') + click_on 'Three' expect(find('.nav-text')).to have_content('Three') click_on 'Edit' end @@ -157,7 +153,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I view the page history of a Wiki page that has a path' do - click_on 'three' + click_on 'Three' click_on 'Page History' end diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb index 7afd83b7250..fff8b9f3447 100644 --- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb @@ -20,7 +20,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do click_button 'Create page' expect(page).to have_content('Home') - expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content("Last edited by #{user.name}") expect(page).to have_content('My awesome wiki!') end end @@ -41,7 +41,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do click_button 'Create page' expect(page).to have_content('Foo') - expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content("Last edited by #{user.name}") expect(page).to have_content('My awesome wiki!') end @@ -55,7 +55,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do click_button 'Create page' expect(page).to have_content('Spaces in the name') - expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content("Last edited by #{user.name}") expect(page).to have_content('My awesome wiki!') end @@ -69,7 +69,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do click_button 'Create page' expect(page).to have_content('Hyphens in the name') - expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content("Last edited by #{user.name}") expect(page).to have_content('My awesome wiki!') end end @@ -85,7 +85,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do click_button 'Create page' expect(page).to have_content('Home') - expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content("Last edited by #{user.name}") expect(page).to have_content('My awesome wiki!') end end @@ -105,7 +105,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do click_button 'Create page' expect(page).to have_content('Foo') - expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content("Last edited by #{user.name}") expect(page).to have_content('My awesome wiki!') end end diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb index ef82d2375dd..f842d14fa96 100644 --- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb @@ -22,7 +22,7 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do click_button 'Save changes' expect(page).to have_content('Home') - expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content("Last edited by #{user.name}") expect(page).to have_content('My awesome wiki!') end end @@ -37,7 +37,7 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do click_button 'Save changes' expect(page).to have_content('Home') - expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content("Last edited by #{user.name}") expect(page).to have_content('My awesome wiki!') end end -- cgit v1.2.1 From b2137632203cda91b79763c33ac80c51e88ed010 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 16 Nov 2016 11:17:25 -0600 Subject: prevent sidebar toggle from overlapping git access header on mobile --- app/assets/stylesheets/pages/wiki.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index 216b8ab3c7f..28b725be3d9 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -28,7 +28,7 @@ } .git-access-header { - padding: 16px 0 11px; + padding: 16px 40px 11px 0; line-height: 28px; font-size: 18px; } -- cgit v1.2.1 From f0d20b09dc6604d4ea11a39a7d7492ad10574094 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 16 Nov 2016 12:10:16 -0600 Subject: use wiki pages index for sidebar overflow and limit sidebar list to 15 pages --- app/assets/javascripts/wikis.js.es6 | 1 + app/controllers/projects/wikis_controller.rb | 2 ++ app/helpers/nav_helper.rb | 1 - app/views/projects/wikis/_sidebar.html.haml | 5 ++++- app/views/projects/wikis/pages.html.haml | 9 +++++---- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js.es6 index 8710ca973c4..fa6d0e1f56c 100644 --- a/app/assets/javascripts/wikis.js.es6 +++ b/app/assets/javascripts/wikis.js.es6 @@ -55,6 +55,7 @@ } renderSidebar() { + if (!this.sidebarEl) return; const { classList } = this.sidebarEl; if (this.sidebarExpanded || !this.sidebarCanCollapse()) { if (!classList.contains('right-sidebar-expanded')) { diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 177ccf5eec9..c3353446fd1 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -115,6 +115,8 @@ class Projects::WikisController < Projects::ApplicationController # Call #wiki to make sure the Wiki Repo is initialized @project_wiki.wiki + + @sidebar_wiki_pages = @project_wiki.pages.first(15) rescue ProjectWiki::CouldNotCreateWikiError flash[:notice] = "Could not create Wiki Repository at this time. Please try again later." redirect_to project_path(@project) diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 2aeab4b6b62..a3331dc80cb 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -23,7 +23,6 @@ module NavHelper elsif current_path?('wikis#show') || current_path?('wikis#edit') || current_path?('wikis#history') || - current_path?('wikis#pages') || current_path?('wikis#git_access') "page-gutter wiki-sidebar right-sidebar-expanded" end diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml index 85604c69865..0e64d1c6e8a 100644 --- a/app/views/projects/wikis/_sidebar.html.haml +++ b/app/views/projects/wikis/_sidebar.html.haml @@ -12,9 +12,12 @@ .blocks-container .block.block-first %ul.wiki-pages - - @project_wiki.pages.each do |wiki_page| + - @sidebar_wiki_pages.each do |wiki_page| %li{ class: params[:id] == wiki_page.slug ? 'active' : '' } = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_page) do = wiki_page.title.capitalize + .block + = link_to namespace_project_wiki_pages_path(@project.namespace, @project), class: 'btn btn-block' do + More Pages = render 'projects/wikis/new' diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index d89f860d9a8..e1eaffc6884 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -3,13 +3,16 @@ %div{ class: container_class } .wiki-page-header - %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } - = icon('angle-double-left') .nav-text %h2.wiki-page-title Wiki Pages + .nav-controls + = link_to namespace_project_wikis_git_access_path(@project.namespace, @project), class: 'btn' do + = icon('cloud-download') + Clone repository + %ul.content-list - @wiki_pages.each do |wiki_page| %li @@ -18,5 +21,3 @@ .pull-right %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)} = paginate @wiki_pages, theme: 'gitlab' - -= render 'sidebar' -- cgit v1.2.1 From 5499f1e66b5c5fa5645856ff9415158a0a3ab781 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 16 Nov 2016 15:07:44 -0600 Subject: add CHANGELOG entry for !7429 --- changelogs/unreleased/18546-update-wiki-page-design.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/18546-update-wiki-page-design.yml diff --git a/changelogs/unreleased/18546-update-wiki-page-design.yml b/changelogs/unreleased/18546-update-wiki-page-design.yml new file mode 100644 index 00000000000..c76e17340f2 --- /dev/null +++ b/changelogs/unreleased/18546-update-wiki-page-design.yml @@ -0,0 +1,4 @@ +--- +title: Update wiki page design +merge_request: 7429 +author: -- cgit v1.2.1 From d5b4b026339fcc3f0deecb4d713b3066c8c69c71 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 29 Nov 2016 14:21:28 -0600 Subject: echo changes in 6683fdcf for wiki pages path --- app/views/projects/wikis/_sidebar.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml index 0e64d1c6e8a..cad9c15a49e 100644 --- a/app/views/projects/wikis/_sidebar.html.haml +++ b/app/views/projects/wikis/_sidebar.html.haml @@ -17,7 +17,7 @@ = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_page) do = wiki_page.title.capitalize .block - = link_to namespace_project_wiki_pages_path(@project.namespace, @project), class: 'btn btn-block' do + = link_to namespace_project_wikis_pages_path(@project.namespace, @project), class: 'btn btn-block' do More Pages = render 'projects/wikis/new' -- cgit v1.2.1 From 9918be8fc885600d21ae6a5020e6288e8e341f21 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 29 Nov 2016 19:33:29 -0600 Subject: prevent nav control buttons from wrapping awkwardly when they overflow --- app/assets/stylesheets/pages/wiki.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index 28b725be3d9..df16ad1600c 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -51,6 +51,12 @@ .git-clone-holder { width: 480px; } + + .nav-controls { + width: auto; + min-width: 50%; + white-space: nowrap; + } } @media (min-width: $screen-md-min) { -- cgit v1.2.1 From bf587cb5c0bbc8d99809d7195a17f96d6c3a1a80 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 29 Nov 2016 19:46:40 -0600 Subject: add special class in cases where sidebar toggle should exist in the margin --- app/assets/stylesheets/pages/wiki.scss | 8 ++++++-- app/views/projects/wikis/edit.html.haml | 2 +- app/views/projects/wikis/git_access.html.haml | 2 +- app/views/projects/wikis/history.html.haml | 2 +- app/views/projects/wikis/show.html.haml | 2 +- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index df16ad1600c..b9f81533150 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -46,7 +46,9 @@ } @media (min-width: $screen-sm-min) { - padding-right: 40px; + &.has-sidebar-toggle { + padding-right: 40px; + } .git-clone-holder { width: 480px; @@ -60,7 +62,9 @@ } @media (min-width: $screen-md-min) { - padding-right: 0; + &.has-sidebar-toggle { + padding-right: 0; + } button.sidebar-toggle { display: none; diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 23117128bbb..8cf018da1b7 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -2,7 +2,7 @@ - page_title "Edit", @page.title.capitalize, "Wiki" %div{ class: container_class } - .wiki-page-header + .wiki-page-header.has-sidebar-toggle %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index c148f65eb3e..e25d6a48573 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -2,7 +2,7 @@ - page_title "Git Access", "Wiki" %div{ class: container_class } - .wiki-page-header + .wiki-page-header.has-sidebar-toggle %button.btn.btn-default.visible-xs.visible-sm.pull-right.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index 5f9c3067cc1..dd7213622c1 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -1,7 +1,7 @@ - page_title "History", @page.title.capitalize, "Wiki" %div{ class: container_class } - .wiki-page-header + .wiki-page-header.has-sidebar-toggle %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 28c1c5ab7d1..1b6dceee241 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -2,7 +2,7 @@ - page_title @page.title.capitalize, "Wiki" %div{ class: container_class } - .wiki-page-header + .wiki-page-header.has-sidebar-toggle %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') -- cgit v1.2.1 From d42e7f23b2cde8991ab8f617335abc19eec4e906 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 29 Nov 2016 19:58:03 -0600 Subject: fix media query off-by-1 pixel error --- app/assets/stylesheets/framework/nav.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 1839ffa0976..98f72e58c23 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -123,7 +123,7 @@ line-height: 28px; /* Small devices (phones, tablets, 768px and lower) */ - @media (max-width: $screen-sm-min) { + @media (max-width: $screen-xs-max) { width: 100%; } } -- cgit v1.2.1 From 3833a28c3aa7a7ae8c1a353e2c6d4338f50b5fb1 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 30 Nov 2016 13:32:26 -0600 Subject: rename event variable to just e --- app/assets/javascripts/wikis.js.es6 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js.es6 index fa6d0e1f56c..ecff5fd5bf4 100644 --- a/app/assets/javascripts/wikis.js.es6 +++ b/app/assets/javascripts/wikis.js.es6 @@ -30,7 +30,7 @@ this.renderSidebar(); } - handleNewWikiSubmit(event) { + handleNewWikiSubmit(e) { if (!this.newWikiForm) return; const slugInput = this.newWikiForm.querySelector('#new_wiki_path'); @@ -39,12 +39,12 @@ if (slug.length > 0) { const wikisPath = slugInput.getAttribute('data-wikis-path'); window.location.href = `${wikisPath}/${slug}`; - event.preventDefault(); + e.preventDefault(); } } - handleToggleSidebar(event) { - event.preventDefault(); + handleToggleSidebar(e) { + e.preventDefault(); this.sidebarExpanded = !this.sidebarExpanded; this.renderSidebar(); } -- cgit v1.2.1 From ffd28232610b87b3c738357655b95feedb3479d0 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Mon, 21 Nov 2016 12:18:49 -0600 Subject: remove duplicate functionality (bad merge conflict resolution?) --- app/assets/javascripts/application.js | 31 +++++------------------- app/assets/javascripts/lib/utils/common_utils.js | 16 +++++++++--- 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 76f3c6506ed..ec6d7cad3ab 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -57,32 +57,13 @@ (function () { document.addEventListener('page:fetch', gl.utils.cleanupBeforeFetch); - window.addEventListener('hashchange', gl.utils.shiftWindow); - - // automatically adjust scroll position for hash urls taking the height of the navbar into account - // https://github.com/twitter/bootstrap/issues/1768 - window.adjustScroll = function() { - var navbar = document.querySelector('.navbar-gitlab'); - var subnav = document.querySelector('.layout-nav'); - var fixedTabs = document.querySelector('.js-tabs-affix'); - - adjustment = 0; - if (navbar) adjustment -= navbar.offsetHeight; - if (subnav) adjustment -= subnav.offsetHeight; - if (fixedTabs) adjustment -= fixedTabs.offsetHeight; - - return scrollBy(0, adjustment); - }; - - window.addEventListener("hashchange", adjustScroll); - - window.onload = function () { - // Scroll the window to avoid the topnav bar - // https://github.com/twitter/bootstrap/issues/1768 - if (location.hash) { - return setTimeout(adjustScroll, 100); + window.addEventListener('hashchange', gl.utils.handleLocationHash); + window.addEventListener('load', function onLoad() { + window.removeEventListener('load', onLoad, false); + if (window.location.hash) { + setTimeout(gl.utils.handleLocationHash, 100); } - }; + }, false); $(function () { var $body = $('body'); diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index d83c41fae9d..b6b3bd18877 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -94,10 +94,20 @@ return $(document).off('scroll'); }; - w.gl.utils.shiftWindow = function() { - return w.scrollBy(0, -100); - }; + // automatically adjust scroll position for hash urls taking the height of the navbar into account + // https://github.com/twitter/bootstrap/issues/1768 + w.gl.utils.handleLocationHash = function() { + var navbar = document.querySelector('.navbar-gitlab'); + var subnav = document.querySelector('.layout-nav'); + var fixedTabs = document.querySelector('.js-tabs-affix'); + + var adjustment = 0; + if (navbar) adjustment -= navbar.offsetHeight; + if (subnav) adjustment -= subnav.offsetHeight; + if (fixedTabs) adjustment -= fixedTabs.offsetHeight; + window.scrollBy(0, adjustment); + }; gl.utils.updateTooltipTitle = function($tooltipEl, newTitle) { return $tooltipEl.tooltip('destroy').attr('title', newTitle).tooltip('fixTitle'); -- cgit v1.2.1 From ff2026f40ec0bc162dc4281b067ed4716b2ad248 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Mon, 21 Nov 2016 12:02:19 -0600 Subject: add transparent namespace to all user-generated anchors in GitLab flavored markdown --- app/assets/javascripts/lib/utils/common_utils.js | 21 ++++++++++++++++++--- lib/banzai/filter/table_of_contents_filter.rb | 8 +++++--- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index b6b3bd18877..09b0a5eb9a5 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-param-reassign, no-else-return, quotes, object-shorthand, comma-dangle, camelcase, one-var, vars-on-top, one-var-declaration-per-line, no-return-assign, consistent-return, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-param-reassign, no-else-return, quotes, object-shorthand, comma-dangle, camelcase, one-var, vars-on-top, one-var-declaration-per-line, no-return-assign, consistent-return, padded-blocks, max-len, prefer-template */ (function() { (function(w) { var base; @@ -97,6 +97,9 @@ // automatically adjust scroll position for hash urls taking the height of the navbar into account // https://github.com/twitter/bootstrap/issues/1768 w.gl.utils.handleLocationHash = function() { + var hash = w.gl.utils.getLocationHash(); + if (!hash) return; + var navbar = document.querySelector('.navbar-gitlab'); var subnav = document.querySelector('.layout-nav'); var fixedTabs = document.querySelector('.js-tabs-affix'); @@ -104,9 +107,21 @@ var adjustment = 0; if (navbar) adjustment -= navbar.offsetHeight; if (subnav) adjustment -= subnav.offsetHeight; - if (fixedTabs) adjustment -= fixedTabs.offsetHeight; - window.scrollBy(0, adjustment); + // scroll to user-generated markdown anchor if we cannot find a match + if (document.getElementById(hash) === null) { + var target = document.getElementById('user-content_' + hash); + if (target && target.scrollIntoView) { + target.scrollIntoView(true); + window.scrollBy(0, adjustment); + } + } else { + // only adjust for fixedTabs when not targeting user-generated content + if (fixedTabs) { + adjustment -= fixedTabs.offsetHeight; + } + window.scrollBy(0, adjustment); + } }; gl.utils.updateTooltipTitle = function($tooltipEl, newTitle) { diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb index a4eda6fdf76..80669953723 100644 --- a/lib/banzai/filter/table_of_contents_filter.rb +++ b/lib/banzai/filter/table_of_contents_filter.rb @@ -35,9 +35,11 @@ module Banzai headers[id] += 1 if header_content = node.children.first + # namespace detection will be automatically handled via javascript (see issue #22781) + namespace = "user-content_" href = "#{id}#{uniq}" push_toc(href, text) - header_content.add_previous_sibling(anchor_tag(href)) + header_content.add_previous_sibling(anchor_tag("#{namespace}#{href}", href)) end end @@ -48,8 +50,8 @@ module Banzai private - def anchor_tag(href) - %Q{<a id="#{href}" class="anchor" href="##{href}" aria-hidden="true"></a>} + def anchor_tag(id, href) + %Q{<a id="#{id}" class="anchor" href="##{href}" aria-hidden="true"></a>} end def push_toc(href, text) -- cgit v1.2.1 From 85422ea412ce1c3ea75562dfb008b6cdfaec8858 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Mon, 21 Nov 2016 13:27:11 -0600 Subject: add CHANGELOG entry for !7631 --- changelogs/unreleased/22781-user-generated-permalinks.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/22781-user-generated-permalinks.yml diff --git a/changelogs/unreleased/22781-user-generated-permalinks.yml b/changelogs/unreleased/22781-user-generated-permalinks.yml new file mode 100644 index 00000000000..e46739e48e3 --- /dev/null +++ b/changelogs/unreleased/22781-user-generated-permalinks.yml @@ -0,0 +1,4 @@ +--- +title: Prevent DOM ID collisions resulting from user-generated content anchors +merge_request: 7631 +author: -- cgit v1.2.1 From 94ae12f6979db948687464e39f76a39fa5f43bae Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Mon, 21 Nov 2016 13:41:15 -0600 Subject: remove id collision caveat from documentation --- doc/development/gotchas.md | 48 ---------------------------------------------- 1 file changed, 48 deletions(-) diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md index 7bfc9cb361f..0f78e8238af 100644 --- a/doc/development/gotchas.md +++ b/doc/development/gotchas.md @@ -141,51 +141,3 @@ in an initializer._ ### Further reading - Stack Overflow: [Why you should not write inline JavaScript](http://programmers.stackexchange.com/questions/86589/why-should-i-avoid-inline-scripting) - -## ID-based CSS selectors need to be a bit more specific - -Normally, because HTML `id` attributes need to be unique to the page, it's -perfectly fine to write some JavaScript like the following: - -```javascript -$('#js-my-selector').hide(); -``` - -However, there's a feature of GitLab's Markdown processing that [automatically -adds anchors to header elements][ToC Processing], with the `id` attribute being -automatically generated based on the content of the header. - -Unfortunately, this feature makes it possible for user-generated content to -create a header element with the same `id` attribute we're using in our -selector, potentially breaking the JavaScript behavior. A user could break the -above example with the following Markdown: - -```markdown -## JS My Selector -``` - -Which gets converted to the following HTML: - -```html -<h2> - <a id="js-my-selector" class="anchor" href="#js-my-selector" aria-hidden="true"></a> - JS My Selector -</h2> -``` - -[ToC Processing]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-4-stable/lib/banzai/filter/table_of_contents_filter.rb#L31-37 - -### Solution - -The current recommended fix for this is to make our selectors slightly more -specific: - -```javascript -$('div#js-my-selector').hide(); -``` - -### Further reading - -- Issue: [Merge request ToC anchor conflicts with tabs](https://gitlab.com/gitlab-org/gitlab-ce/issues/3908) -- Merge Request: [Make tab target selectors less naive](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2023) -- Merge Request: [Make cross-project reference's clipboard target less naive](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2024) -- cgit v1.2.1 From 567b8a99fe0e454a6d2c0b79cde86ee2dea806f3 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Mon, 21 Nov 2016 13:48:29 -0600 Subject: update gitlab flavored markdown tests to reflect namespaced ids --- features/steps/shared/markdown.rb | 2 +- .../banzai/filter/table_of_contents_filter_spec.rb | 21 +++++++++++++-------- spec/support/matchers/markdown_matchers.rb | 6 +++--- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/features/steps/shared/markdown.rb b/features/steps/shared/markdown.rb index 56b36f7c46c..d355913a7ce 100644 --- a/features/steps/shared/markdown.rb +++ b/features/steps/shared/markdown.rb @@ -2,7 +2,7 @@ module SharedMarkdown include Spinach::DSL def header_should_have_correct_id_and_link(level, text, id, parent = ".wiki") - node = find("#{parent} h#{level} a##{id}") + node = find("#{parent} h#{level} a#user-content_#{id}") expect(node[:href]).to eq "##{id}" # Work around a weird Capybara behavior where calling `parent` on a node diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb index 356dd01a03a..e551a7d0ca5 100644 --- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb +++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb @@ -22,7 +22,7 @@ describe Banzai::Filter::TableOfContentsFilter, lib: true do html = header(i, "Header #{i}") doc = filter(html) - expect(doc.css("h#{i} a").first.attr('id')).to eq "header-#{i}" + expect(doc.css("h#{i} a").first.attr('id')).to eq "user-content_header-#{i}" end end @@ -32,7 +32,12 @@ describe Banzai::Filter::TableOfContentsFilter, lib: true do expect(doc.css('h1 a').first.attr('class')).to eq 'anchor' end - it 'links to the id' do + it 'has a namespaced id' do + doc = filter(header(1, 'Header')) + expect(doc.css('h1 a').first.attr('id')).to eq 'user-content_header' + end + + it 'links to the non-namespaced id' do doc = filter(header(1, 'Header')) expect(doc.css('h1 a').first.attr('href')).to eq '#header' end @@ -40,29 +45,29 @@ describe Banzai::Filter::TableOfContentsFilter, lib: true do describe 'generated IDs' do it 'translates spaces to dashes' do doc = filter(header(1, 'This header has spaces in it')) - expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-has-spaces-in-it' + expect(doc.css('h1 a').first.attr('href')).to eq '#this-header-has-spaces-in-it' end it 'squeezes multiple spaces and dashes' do doc = filter(header(1, 'This---header is poorly-formatted')) - expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-poorly-formatted' + expect(doc.css('h1 a').first.attr('href')).to eq '#this-header-is-poorly-formatted' end it 'removes punctuation' do doc = filter(header(1, "This, header! is, filled. with @ punctuation?")) - expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-filled-with-punctuation' + expect(doc.css('h1 a').first.attr('href')).to eq '#this-header-is-filled-with-punctuation' end it 'appends a unique number to duplicates' do doc = filter(header(1, 'One') + header(2, 'One')) - expect(doc.css('h1 a').first.attr('id')).to eq 'one' - expect(doc.css('h2 a').first.attr('id')).to eq 'one-1' + expect(doc.css('h1 a').first.attr('href')).to eq '#one' + expect(doc.css('h2 a').first.attr('href')).to eq '#one-1' end it 'supports Unicode' do doc = filter(header(1, '한글')) - expect(doc.css('h1 a').first.attr('id')).to eq '한글' + expect(doc.css('h1 a').first.attr('id')).to eq 'user-content_한글' expect(doc.css('h1 a').first.attr('href')).to eq '#한글' end end diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index 8c98b1f988c..a44bd2601c1 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -38,9 +38,9 @@ module MarkdownMatchers set_default_markdown_messages match do |actual| - expect(actual).to have_selector('h1 a#gitlab-markdown') - expect(actual).to have_selector('h2 a#markdown') - expect(actual).to have_selector('h3 a#autolinkfilter') + expect(actual).to have_selector('h1 a#user-content_gitlab-markdown') + expect(actual).to have_selector('h2 a#user-content_markdown') + expect(actual).to have_selector('h3 a#user-content_autolinkfilter') end end -- cgit v1.2.1 From 118ab549147684d86af39fd9229789e6b15d09a7 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 22 Nov 2016 16:11:12 -0600 Subject: prevent anchor tag outline on :focus or :target pseudo-class --- app/assets/stylesheets/framework/typography.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 070e42d63d2..aa604b1cd19 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -182,6 +182,7 @@ left: -16px; position: absolute; text-decoration: none; + outline: none; &::after { content: image-url('icon_anchor.svg'); -- cgit v1.2.1 From 19f174bc68e0dc17e0298d8903bc855868334776 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 22 Nov 2016 17:23:34 -0600 Subject: remove setTimeout wrapper for location hash correction --- app/assets/javascripts/application.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index ec6d7cad3ab..cfab4721f4b 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -60,9 +60,7 @@ window.addEventListener('hashchange', gl.utils.handleLocationHash); window.addEventListener('load', function onLoad() { window.removeEventListener('load', onLoad, false); - if (window.location.hash) { - setTimeout(gl.utils.handleLocationHash, 100); - } + gl.utils.handleLocationHash(); }, false); $(function () { -- cgit v1.2.1 From 131a04d7962b01daa58b8e5efe3f1359a3e73fe1 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 29 Nov 2016 13:07:38 -0600 Subject: remove underscore from user-content id namespace --- app/assets/javascripts/lib/utils/common_utils.js | 2 +- features/steps/shared/markdown.rb | 2 +- lib/banzai/filter/table_of_contents_filter.rb | 2 +- spec/lib/banzai/filter/table_of_contents_filter_spec.rb | 6 +++--- spec/support/matchers/markdown_matchers.rb | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 09b0a5eb9a5..c5846068b07 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -110,7 +110,7 @@ // scroll to user-generated markdown anchor if we cannot find a match if (document.getElementById(hash) === null) { - var target = document.getElementById('user-content_' + hash); + var target = document.getElementById('user-content-' + hash); if (target && target.scrollIntoView) { target.scrollIntoView(true); window.scrollBy(0, adjustment); diff --git a/features/steps/shared/markdown.rb b/features/steps/shared/markdown.rb index d355913a7ce..a036d9b884f 100644 --- a/features/steps/shared/markdown.rb +++ b/features/steps/shared/markdown.rb @@ -2,7 +2,7 @@ module SharedMarkdown include Spinach::DSL def header_should_have_correct_id_and_link(level, text, id, parent = ".wiki") - node = find("#{parent} h#{level} a#user-content_#{id}") + node = find("#{parent} h#{level} a#user-content-#{id}") expect(node[:href]).to eq "##{id}" # Work around a weird Capybara behavior where calling `parent` on a node diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb index 80669953723..8e7084f2543 100644 --- a/lib/banzai/filter/table_of_contents_filter.rb +++ b/lib/banzai/filter/table_of_contents_filter.rb @@ -36,7 +36,7 @@ module Banzai if header_content = node.children.first # namespace detection will be automatically handled via javascript (see issue #22781) - namespace = "user-content_" + namespace = "user-content-" href = "#{id}#{uniq}" push_toc(href, text) header_content.add_previous_sibling(anchor_tag("#{namespace}#{href}", href)) diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb index e551a7d0ca5..70b31f3a880 100644 --- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb +++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb @@ -22,7 +22,7 @@ describe Banzai::Filter::TableOfContentsFilter, lib: true do html = header(i, "Header #{i}") doc = filter(html) - expect(doc.css("h#{i} a").first.attr('id')).to eq "user-content_header-#{i}" + expect(doc.css("h#{i} a").first.attr('id')).to eq "user-content-header-#{i}" end end @@ -34,7 +34,7 @@ describe Banzai::Filter::TableOfContentsFilter, lib: true do it 'has a namespaced id' do doc = filter(header(1, 'Header')) - expect(doc.css('h1 a').first.attr('id')).to eq 'user-content_header' + expect(doc.css('h1 a').first.attr('id')).to eq 'user-content-header' end it 'links to the non-namespaced id' do @@ -67,7 +67,7 @@ describe Banzai::Filter::TableOfContentsFilter, lib: true do it 'supports Unicode' do doc = filter(header(1, '한글')) - expect(doc.css('h1 a').first.attr('id')).to eq 'user-content_한글' + expect(doc.css('h1 a').first.attr('id')).to eq 'user-content-한글' expect(doc.css('h1 a').first.attr('href')).to eq '#한글' end end diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index a44bd2601c1..97b8b342eb2 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -38,9 +38,9 @@ module MarkdownMatchers set_default_markdown_messages match do |actual| - expect(actual).to have_selector('h1 a#user-content_gitlab-markdown') - expect(actual).to have_selector('h2 a#user-content_markdown') - expect(actual).to have_selector('h3 a#user-content_autolinkfilter') + expect(actual).to have_selector('h1 a#user-content-gitlab-markdown') + expect(actual).to have_selector('h2 a#user-content-markdown') + expect(actual).to have_selector('h3 a#user-content-autolinkfilter') end end -- cgit v1.2.1 From 9ce28e7913928f93ed2790c434ad888e751e0257 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra <dimitriehoekstra@gmail.com> Date: Wed, 9 Nov 2016 23:27:16 +0100 Subject: Remove the help text under the sidebar subscribe button and style it inline --- app/views/shared/issuable/_sidebar.html.haml | 9 ++------- ...-merge-request-sidebar-subscribe-button-style-improvement.yml | 4 ++++ 2 files changed, 6 insertions(+), 7 deletions(-) create mode 100644 changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 02427650219..eac83f5d83a 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -144,16 +144,11 @@ .block.light.subscription{data: {url: toggle_subscription_path(issuable)}} .sidebar-collapsed-icon = icon('rss') - .title.hide-collapsed + %span.issuable-header-text.hide-collapsed.pull-left Notifications - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed' - %button.btn.btn-block.btn-default.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } + %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } %span= subscribed ? 'Unsubscribe' : 'Subscribe' - .subscription-status.hide-collapsed{data: {status: subscribtion_status}} - .unsubscribed{class: ( 'hidden' if subscribed )} - You're not receiving notifications from this thread. - .subscribed{class: ( 'hidden' unless subscribed )} - You're receiving notifications because you're subscribed to this thread. - project_ref = cross_project_reference(@project, issuable) .block.project-reference diff --git a/changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml b/changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml new file mode 100644 index 00000000000..2227c81bd34 --- /dev/null +++ b/changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml @@ -0,0 +1,4 @@ +--- +title: Remove the help text under the sidebar subscribe button and style it inline +merge_request: 7389 +author: -- cgit v1.2.1 From d795a70d2da02110fe517dc8d1e79d5986ac2946 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Fri, 18 Nov 2016 14:52:20 -0600 Subject: rename subscription.js to subscription.js.es6 --- app/assets/javascripts/subscription.js | 52 ------------------------------ app/assets/javascripts/subscription.js.es6 | 52 ++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 52 deletions(-) delete mode 100644 app/assets/javascripts/subscription.js create mode 100644 app/assets/javascripts/subscription.js.es6 diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js deleted file mode 100644 index 6d75688deeb..00000000000 --- a/app/assets/javascripts/subscription.js +++ /dev/null @@ -1,52 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, vars-on-top, no-unused-vars, one-var, one-var-declaration-per-line, camelcase, consistent-return, no-undef, padded-blocks, max-len */ -(function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - this.Subscription = (function() { - function Subscription(container) { - this.toggleSubscription = bind(this.toggleSubscription, this); - var $container; - this.$container = $(container); - this.url = this.$container.attr('data-url'); - this.subscribe_button = this.$container.find('.js-subscribe-button'); - this.subscription_status = this.$container.find('.subscription-status'); - this.subscribe_button.unbind('click').click(this.toggleSubscription); - } - - Subscription.prototype.toggleSubscription = function(event) { - var action, btn, current_status; - btn = $(event.currentTarget); - action = btn.find('span').text(); - current_status = this.subscription_status.attr('data-status'); - btn.addClass('disabled'); - - if ($('html').hasClass('issue-boards-page')) { - this.url = this.$container.attr('data-url'); - } - - return $.post(this.url, (function(_this) { - return function() { - var status; - btn.removeClass('disabled'); - - if ($('html').hasClass('issue-boards-page')) { - Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'subscribed', !gl.issueBoards.BoardsStore.detail.issue.subscribed); - } else { - status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed'; - _this.subscription_status.attr('data-status', status); - action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe'; - btn.find('span').text(action); - _this.subscription_status.find('>div').toggleClass('hidden'); - if (btn.attr('data-original-title')) { - return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle'); - } - } - }; - })(this)); - }; - - return Subscription; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/subscription.js.es6 b/app/assets/javascripts/subscription.js.es6 new file mode 100644 index 00000000000..6d75688deeb --- /dev/null +++ b/app/assets/javascripts/subscription.js.es6 @@ -0,0 +1,52 @@ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, vars-on-top, no-unused-vars, one-var, one-var-declaration-per-line, camelcase, consistent-return, no-undef, padded-blocks, max-len */ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.Subscription = (function() { + function Subscription(container) { + this.toggleSubscription = bind(this.toggleSubscription, this); + var $container; + this.$container = $(container); + this.url = this.$container.attr('data-url'); + this.subscribe_button = this.$container.find('.js-subscribe-button'); + this.subscription_status = this.$container.find('.subscription-status'); + this.subscribe_button.unbind('click').click(this.toggleSubscription); + } + + Subscription.prototype.toggleSubscription = function(event) { + var action, btn, current_status; + btn = $(event.currentTarget); + action = btn.find('span').text(); + current_status = this.subscription_status.attr('data-status'); + btn.addClass('disabled'); + + if ($('html').hasClass('issue-boards-page')) { + this.url = this.$container.attr('data-url'); + } + + return $.post(this.url, (function(_this) { + return function() { + var status; + btn.removeClass('disabled'); + + if ($('html').hasClass('issue-boards-page')) { + Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'subscribed', !gl.issueBoards.BoardsStore.detail.issue.subscribed); + } else { + status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed'; + _this.subscription_status.attr('data-status', status); + action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe'; + btn.find('span').text(action); + _this.subscription_status.find('>div').toggleClass('hidden'); + if (btn.attr('data-original-title')) { + return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle'); + } + } + }; + })(this)); + }; + + return Subscription; + + })(); + +}).call(this); -- cgit v1.2.1 From e0e5ea0e19d2527c9787eb7c23001fca8e7f58a8 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 17 Nov 2016 09:48:45 -0600 Subject: rewrite subscription javascript to accomodate new design --- .../boards/components/board_sidebar.js.es6 | 2 +- app/assets/javascripts/subscription.js.es6 | 98 ++++++++++++---------- app/assets/stylesheets/pages/boards.scss | 1 - .../components/sidebar/_notifications.html.haml | 12 +-- app/views/shared/issuable/_sidebar.html.haml | 2 +- spec/features/boards/sidebar_spec.rb | 4 +- 6 files changed, 60 insertions(+), 59 deletions(-) diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6 index d5cb6164e0b..1644a772737 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js.es6 +++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6 @@ -47,7 +47,7 @@ new gl.DueDateSelectors(); new LabelsSelect(); new Sidebar(); - new Subscription('.subscription'); + gl.Subscription.bindAll('.subscription'); } }); })(); diff --git a/app/assets/javascripts/subscription.js.es6 b/app/assets/javascripts/subscription.js.es6 index 6d75688deeb..58b380e0f2e 100644 --- a/app/assets/javascripts/subscription.js.es6 +++ b/app/assets/javascripts/subscription.js.es6 @@ -1,52 +1,58 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, vars-on-top, no-unused-vars, one-var, one-var-declaration-per-line, camelcase, consistent-return, no-undef, padded-blocks, max-len */ -(function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - this.Subscription = (function() { - function Subscription(container) { - this.toggleSubscription = bind(this.toggleSubscription, this); - var $container; - this.$container = $(container); - this.url = this.$container.attr('data-url'); - this.subscribe_button = this.$container.find('.js-subscribe-button'); - this.subscription_status = this.$container.find('.subscription-status'); - this.subscribe_button.unbind('click').click(this.toggleSubscription); +/* global Vue */ + +((global) => { + class Subscription { + constructor(containerSelector) { + this.containerElm = (typeof containerSelector === 'string') + ? document.querySelector(containerSelector) + : containerSelector; + + const subscribeButton = this.containerElm.querySelector('.js-subscribe-button'); + if (subscribeButton) { + // remove class so we don't bind twice + subscribeButton.classList.remove('js-subscribe-button'); + subscribeButton.addEventListener('click', this.toggleSubscription.bind(this)); + } } - Subscription.prototype.toggleSubscription = function(event) { - var action, btn, current_status; - btn = $(event.currentTarget); - action = btn.find('span').text(); - current_status = this.subscription_status.attr('data-status'); - btn.addClass('disabled'); - - if ($('html').hasClass('issue-boards-page')) { - this.url = this.$container.attr('data-url'); + toggleSubscription(event) { + const button = event.currentTarget; + const buttonSpan = button.querySelector('span'); + if (!buttonSpan || button.classList.contains('disabled')) { + return; } - - return $.post(this.url, (function(_this) { - return function() { - var status; - btn.removeClass('disabled'); - - if ($('html').hasClass('issue-boards-page')) { - Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'subscribed', !gl.issueBoards.BoardsStore.detail.issue.subscribed); - } else { - status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed'; - _this.subscription_status.attr('data-status', status); - action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe'; - btn.find('span').text(action); - _this.subscription_status.find('>div').toggleClass('hidden'); - if (btn.attr('data-original-title')) { - return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle'); - } + button.classList.add('disabled'); + + const isSubscribed = buttonSpan.innerHTML.trim() !== 'Subscribe'; + const toggleActionUrl = this.containerElm.getAttribute('data-url'); + + $.post(toggleActionUrl, () => { + button.classList.remove('disabled'); + + // hack to allow this to work with the issue boards Vue object + if (document.querySelector('html').classList.contains('issue-boards-page')) { + Vue.set( + gl.issueBoards.BoardsStore.detail.issue, + 'subscribed', + !gl.issueBoards.BoardsStore.detail.issue.subscribed + ); + } else { + const newToggleText = isSubscribed ? 'Subscribe' : 'Unsubscribe'; + buttonSpan.innerHTML = newToggleText; + + if (button.getAttribute('data-original-title')) { + button.setAttribute('data-original-title', newToggleText); + $(button).tooltip('hide').tooltip('fixTitle'); } - }; - })(this)); - }; - - return Subscription; + } + }); + } - })(); + static bindAll(selector) { + [].forEach.call(document.querySelectorAll(selector), elm => new Subscription(elm)); + } + } -}).call(this); + // eslint-disable-next-line no-param-reassign + global.Subscription = Subscription; +})(window.gl || (window.gl = {})); diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 4327f8bf640..82f36f24867 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -325,7 +325,6 @@ } .issuable-header-text { - width: 100%; padding-right: 35px; > strong { diff --git a/app/views/projects/boards/components/sidebar/_notifications.html.haml b/app/views/projects/boards/components/sidebar/_notifications.html.haml index 21c9563e9db..a08c7f2af09 100644 --- a/app/views/projects/boards/components/sidebar/_notifications.html.haml +++ b/app/views/projects/boards/components/sidebar/_notifications.html.haml @@ -1,11 +1,7 @@ - if current_user .block.light.subscription{ ":data-url" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '/toggle_subscription'" } - .title + %span.issuable-header-text.hide-collapsed.pull-left Notifications - %button.btn.btn-block.btn-default.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } - {{ issue.subscribed ? 'Unsubscribe' : 'Subscribe' }} - .subscription-status{ ":data-status" => "issue.subscribed ? 'subscribed' : 'unsubscribed'" } - .unsubscribed{ "v-show" => "!issue.subscribed" } - You're not receiving notifications from this thread. - .subscribed{ "v-show" => "issue.subscribed" } - You're receiving notifications because you're subscribed to this thread. + %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } + %span + {{issue.subscribed ? 'Unsubscribe' : 'Subscribe'}} diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index eac83f5d83a..958f8413e1d 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -165,6 +165,6 @@ new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}'); new LabelsSelect(); new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}'); - new Subscription('.subscription') + gl.Subscription.bindAll('.subscription'); new gl.DueDateSelectors(); sidebar = new Sidebar(); diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index f160052a844..c16aafa1470 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -304,8 +304,8 @@ describe 'Issue Boards', feature: true, js: true do page.within('.subscription') do click_button 'Subscribe' - - expect(page).to have_content("You're receiving notifications because you're subscribed to this thread.") + wait_for_ajax + expect(page).to have_content("Unsubscribe") end end end -- cgit v1.2.1 From d23a888ba3cc31a1f6f69d70a090acd3625b186b Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 22 Nov 2016 15:47:11 -0600 Subject: move eslint rules to top of script --- app/assets/javascripts/subscription.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/subscription.js.es6 b/app/assets/javascripts/subscription.js.es6 index 58b380e0f2e..f043e3e6850 100644 --- a/app/assets/javascripts/subscription.js.es6 +++ b/app/assets/javascripts/subscription.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable no-param-reassign */ /* global Vue */ ((global) => { @@ -53,6 +54,5 @@ } } - // eslint-disable-next-line no-param-reassign global.Subscription = Subscription; })(window.gl || (window.gl = {})); -- cgit v1.2.1 From 85a82ed4d6ef12100a7e4df19e6b6506a92e2ee9 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 22 Nov 2016 15:56:52 -0600 Subject: use less error-prone lowercase comparison for isSubscribed --- app/assets/javascripts/subscription.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/subscription.js.es6 b/app/assets/javascripts/subscription.js.es6 index f043e3e6850..ae596916c9d 100644 --- a/app/assets/javascripts/subscription.js.es6 +++ b/app/assets/javascripts/subscription.js.es6 @@ -24,7 +24,7 @@ } button.classList.add('disabled'); - const isSubscribed = buttonSpan.innerHTML.trim() !== 'Subscribe'; + const isSubscribed = buttonSpan.innerHTML.trim().toLowerCase() !== 'subscribe'; const toggleActionUrl = this.containerElm.getAttribute('data-url'); $.post(toggleActionUrl, () => { -- cgit v1.2.1 From ee99de6e61a02a61bef8fb0f85a114d2a28c5425 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 30 Nov 2016 17:20:06 -0600 Subject: add comma to satisfy eslint comma-dangle --- app/assets/javascripts/subscription.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/subscription.js.es6 b/app/assets/javascripts/subscription.js.es6 index ae596916c9d..8ca3281d824 100644 --- a/app/assets/javascripts/subscription.js.es6 +++ b/app/assets/javascripts/subscription.js.es6 @@ -35,7 +35,7 @@ Vue.set( gl.issueBoards.BoardsStore.detail.issue, 'subscribed', - !gl.issueBoards.BoardsStore.detail.issue.subscribed + !gl.issueBoards.BoardsStore.detail.issue.subscribed, ); } else { const newToggleText = isSubscribed ? 'Subscribe' : 'Unsubscribe'; -- cgit v1.2.1 From 31356c203d6333158872ecad98d1cc071631a559 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 30 Nov 2016 17:21:28 -0600 Subject: satisfy eslint no-param-reassign and remove rule exception --- app/assets/javascripts/subscription.js.es6 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/subscription.js.es6 b/app/assets/javascripts/subscription.js.es6 index 8ca3281d824..73d484f85c7 100644 --- a/app/assets/javascripts/subscription.js.es6 +++ b/app/assets/javascripts/subscription.js.es6 @@ -1,7 +1,6 @@ -/* eslint-disable no-param-reassign */ /* global Vue */ -((global) => { +(() => { class Subscription { constructor(containerSelector) { this.containerElm = (typeof containerSelector === 'string') @@ -54,5 +53,6 @@ } } - global.Subscription = Subscription; -})(window.gl || (window.gl = {})); + window.gl = window.gl || {}; + window.gl.Subscription = Subscription; +})(); -- cgit v1.2.1 From d2ac77177dccfdd079605c4fe1c4cfe75a64653d Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 30 Nov 2016 17:43:21 -0600 Subject: remove tooltip logic --- app/assets/javascripts/subscription.js.es6 | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/app/assets/javascripts/subscription.js.es6 b/app/assets/javascripts/subscription.js.es6 index 73d484f85c7..d5876ee3944 100644 --- a/app/assets/javascripts/subscription.js.es6 +++ b/app/assets/javascripts/subscription.js.es6 @@ -37,13 +37,7 @@ !gl.issueBoards.BoardsStore.detail.issue.subscribed, ); } else { - const newToggleText = isSubscribed ? 'Subscribe' : 'Unsubscribe'; - buttonSpan.innerHTML = newToggleText; - - if (button.getAttribute('data-original-title')) { - button.setAttribute('data-original-title', newToggleText); - $(button).tooltip('hide').tooltip('fixTitle'); - } + buttonSpan.innerHTML = isSubscribed ? 'Subscribe' : 'Unsubscribe'; } }); } -- cgit v1.2.1 From 9abcb71c9eec09d578a563dd80e3b430864faa54 Mon Sep 17 00:00:00 2001 From: Nick Thomas <nick@gitlab.com> Date: Wed, 30 Nov 2016 23:01:55 +0000 Subject: Rename a label to fix an intermittently-failing spec --- spec/finders/labels_finder_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb index 9085cc8debf..1724cdba830 100644 --- a/spec/finders/labels_finder_spec.rb +++ b/spec/finders/labels_finder_spec.rb @@ -17,7 +17,7 @@ describe LabelsFinder do let!(:project_label_4) { create(:label, project: project_4, title: 'Label 4') } let!(:project_label_5) { create(:label, project: project_5, title: 'Label 5') } - let!(:group_label_1) { create(:group_label, group: group_1, title: 'Label 1') } + let!(:group_label_1) { create(:group_label, group: group_1, title: 'Label 1 (group)') } let!(:group_label_2) { create(:group_label, group: group_1, title: 'Group Label 2') } let!(:group_label_3) { create(:group_label, group: group_2, title: 'Group Label 3') } -- cgit v1.2.1 From 8ca040d8ae66be461f61f52673a87022e6c84204 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Thu, 1 Dec 2016 00:03:12 -0200 Subject: Fix branch validation for GitHub PR where repo/fork was renamed/deleted --- lib/gitlab/github_import/branch_formatter.rb | 2 +- spec/lib/gitlab/github_import/branch_formatter_spec.rb | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb index 4750675ae9d..0a8d05b5fe1 100644 --- a/lib/gitlab/github_import/branch_formatter.rb +++ b/lib/gitlab/github_import/branch_formatter.rb @@ -8,7 +8,7 @@ module Gitlab end def valid? - repo.present? + sha.present? && ref.present? end private diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb index e5300dbba1e..462caa5b5fe 100644 --- a/spec/lib/gitlab/github_import/branch_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb @@ -49,14 +49,20 @@ describe Gitlab::GithubImport::BranchFormatter, lib: true do end describe '#valid?' do - it 'returns true when raw repo is present' do + it 'returns true when raw sha and ref are present' do branch = described_class.new(project, double(raw)) expect(branch.valid?).to eq true end - it 'returns false when raw repo is blank' do - branch = described_class.new(project, double(raw.merge(repo: nil))) + it 'returns false when raw sha is blank' do + branch = described_class.new(project, double(raw.merge(sha: nil))) + + expect(branch.valid?).to eq false + end + + it 'returns false when raw ref is blank' do + branch = described_class.new(project, double(raw.merge(ref: nil))) expect(branch.valid?).to eq false end -- cgit v1.2.1 From 20720d6aac157207b7ce31edf1ab7a1cd2be6853 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Thu, 1 Dec 2016 00:03:41 -0200 Subject: Add CHANGELOG entry --- changelogs/unreleased/fix-github-branch-formatter.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-github-branch-formatter.yml diff --git a/changelogs/unreleased/fix-github-branch-formatter.yml b/changelogs/unreleased/fix-github-branch-formatter.yml new file mode 100644 index 00000000000..c8698f507de --- /dev/null +++ b/changelogs/unreleased/fix-github-branch-formatter.yml @@ -0,0 +1,4 @@ +--- +title: Fix branch validation for GitHub PR where repo/fork was renamed/deleted +merge_request: +author: -- cgit v1.2.1 From 819f459b69935f75cbe423884149564cf6cea001 Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Wed, 30 Nov 2016 16:29:27 +0800 Subject: Only include EmailHelpers in mailer specs and specs using them --- spec/models/ci/pipeline_spec.rb | 2 ++ spec/models/project_services/pipeline_email_service_spec.rb | 2 ++ spec/requests/api/issues_spec.rb | 1 + spec/services/issues/update_service_spec.rb | 2 ++ spec/services/merge_requests/update_service_spec.rb | 2 ++ spec/services/notification_service_spec.rb | 2 ++ spec/spec_helper.rb | 2 +- spec/workers/build_email_worker_spec.rb | 1 + spec/workers/emails_on_push_worker_spec.rb | 1 + spec/workers/pipeline_notification_worker_spec.rb | 2 ++ 10 files changed, 16 insertions(+), 1 deletion(-) diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 3b12f16b4db..0d2b4920835 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Ci::Pipeline, models: true do + include EmailHelpers + let(:project) { FactoryGirl.create :empty_project } let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, status: 'created', project: project } diff --git a/spec/models/project_services/pipeline_email_service_spec.rb b/spec/models/project_services/pipeline_email_service_spec.rb index 4f56bceda44..7c8824485f5 100644 --- a/spec/models/project_services/pipeline_email_service_spec.rb +++ b/spec/models/project_services/pipeline_email_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe PipelinesEmailService do + include EmailHelpers + let(:pipeline) do create(:ci_pipeline, project: project, sha: project.commit('master').sha) end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index ae7994af981..9b52463ba29 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe API::API, api: true do include ApiHelpers + include EmailHelpers let(:user) { create(:user) } let(:user2) { create(:user) } diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 4c878d748c0..500d224ff98 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' describe Issues::UpdateService, services: true do + include EmailHelpers + let(:user) { create(:user) } let(:user2) { create(:user) } let(:user3) { create(:user) } diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 0bd6db1810a..790ef765f3a 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe MergeRequests::UpdateService, services: true do + include EmailHelpers + let(:project) { create(:project) } let(:user) { create(:user) } let(:user2) { create(:user) } diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 08ae61708a5..f3e80ac22a0 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe NotificationService, services: true do + include EmailHelpers + let(:notification) { NotificationService.new } around(:each) do |example| diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index bead1a006d1..ef33c473d38 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -32,7 +32,7 @@ RSpec.configure do |config| config.include LoginHelpers, type: :feature config.include SearchHelpers, type: :feature config.include StubConfiguration - config.include EmailHelpers + config.include EmailHelpers, type: :mailer config.include TestEnv config.include ActiveJob::TestHelper config.include ActiveSupport::Testing::TimeHelpers diff --git a/spec/workers/build_email_worker_spec.rb b/spec/workers/build_email_worker_spec.rb index a1aa336361a..542e674c150 100644 --- a/spec/workers/build_email_worker_spec.rb +++ b/spec/workers/build_email_worker_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe BuildEmailWorker do + include EmailHelpers include RepoHelpers let(:build) { create(:ci_build) } diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb index fc652f6f4c3..f27e413f7b8 100644 --- a/spec/workers/emails_on_push_worker_spec.rb +++ b/spec/workers/emails_on_push_worker_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe EmailsOnPushWorker do include RepoHelpers + include EmailHelpers include EmailSpec::Matchers let(:project) { create(:project) } diff --git a/spec/workers/pipeline_notification_worker_spec.rb b/spec/workers/pipeline_notification_worker_spec.rb index d487a719680..739f9b63967 100644 --- a/spec/workers/pipeline_notification_worker_spec.rb +++ b/spec/workers/pipeline_notification_worker_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe PipelineNotificationWorker do + include EmailHelpers + let(:pipeline) do create(:ci_pipeline, project: project, -- cgit v1.2.1 From 410cb1bf18edf17357babf6ae38e77012e187818 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 30 Nov 2016 23:27:58 -0600 Subject: fix incorrect max-width media queries --- app/assets/stylesheets/framework/calendar.scss | 2 +- app/assets/stylesheets/framework/flash.scss | 2 +- app/assets/stylesheets/framework/forms.scss | 2 +- app/assets/stylesheets/framework/header.scss | 2 +- app/assets/stylesheets/framework/nav.scss | 2 +- app/assets/stylesheets/pages/cycle_analytics.scss | 8 ++++---- app/assets/stylesheets/pages/environments.scss | 2 +- app/assets/stylesheets/pages/groups.scss | 6 +++--- app/assets/stylesheets/pages/milestone.scss | 2 +- app/assets/stylesheets/pages/note_form.scss | 2 +- app/assets/stylesheets/pages/notes.scss | 2 +- app/assets/stylesheets/pages/profile.scss | 4 ++-- app/assets/stylesheets/pages/projects.scss | 2 +- app/assets/stylesheets/pages/stat_graph.scss | 2 +- 14 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index 8642b7530e2..81852158b94 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -2,7 +2,7 @@ padding-left: 0; padding-right: 0; - @media (min-width: $screen-sm-min) and (max-width: $screen-lg-min) { + @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) { overflow-x: scroll; } } diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index a9006de6d3e..eadb9409fee 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -38,7 +38,7 @@ } } -@media (max-width: $screen-md-min) { +@media (max-width: $screen-sm-max) { ul.notes { .flash-container.timeline-content { margin-left: 0; diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index e83a1f7ad68..a01899ccbd2 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -98,7 +98,7 @@ label { } } - @media(max-width: $screen-sm-min) { + @media(max-width: $screen-xs-max) { padding: 0 $gl-padding; .control-label, diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 16ecf466931..f9bcbbf2ca5 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -228,7 +228,7 @@ header { } .page-sidebar-pinned.right-sidebar-expanded { - @media (max-width: $screen-lg-min) { + @media (max-width: $screen-md-max) { .header-content .title { width: 300px; } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 1839ffa0976..98f72e58c23 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -123,7 +123,7 @@ line-height: 28px; /* Small devices (phones, tablets, 768px and lower) */ - @media (max-width: $screen-sm-min) { + @media (max-width: $screen-xs-max) { width: 100%; } } diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 498a8f68e49..0b4930ec98f 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -53,7 +53,7 @@ border-bottom: none; position: relative; - @media (max-width: $screen-sm-min) { + @media (max-width: $screen-xs-max) { padding: 6px 0 24px; } } @@ -61,7 +61,7 @@ .column { text-align: center; - @media (max-width: $screen-sm-min) { + @media (max-width: $screen-xs-max) { padding: 15px 0; } @@ -78,7 +78,7 @@ } &:last-child { - @media (max-width: $screen-sm-min) { + @media (max-width: $screen-xs-max) { text-align: center; } } @@ -156,7 +156,7 @@ } .inner-content { - @media (max-width: $screen-sm-min) { + @media (max-width: $screen-xs-max) { padding: 0 28px; text-align: center; } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 4b382e8adaf..de3d2ba549f 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -8,7 +8,7 @@ font-size: 34px; } -@media (max-width: $screen-sm-min) { +@media (max-width: $screen-xs-max) { .environments-container { width: 100%; overflow: auto; diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 57d028cec8c..a9af7af59e2 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -49,14 +49,14 @@ padding: 50px 100px; overflow: hidden; - @media (max-width: $screen-md-min) { + @media (max-width: $screen-sm-max) { padding: 50px 0; } svg { float: right; - @media (max-width: $screen-md-min) { + @media (max-width: $screen-sm-max) { float: none; display: block; width: 250px; @@ -71,7 +71,7 @@ width: 460px; margin-top: 120px; - @media (max-width: $screen-md-min) { + @media (max-width: $screen-sm-max) { float: none; margin-top: 60px; width: auto; diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 8843d1463db..dfc6079bd15 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -123,7 +123,7 @@ padding: 20px 0; } -@media (max-width: $screen-sm-min) { +@media (max-width: $screen-xs-max) { .milestone-actions { @include clearfix(); padding-top: $gl-vert-padding; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 16ddef481bd..65775c45e5b 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -111,7 +111,7 @@ text-align: center; font-size: 13px; - @media (max-width: $screen-md-min) { + @media (max-width: $screen-sm-max) { // On smaller devices the warning becomes the fourth item in the list, // rather than centering, and grows to span the full width of the // comment area. diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 15ec8be831e..56a798a7b6d 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -253,7 +253,7 @@ ul.notes { } .page-sidebar-pinned.right-sidebar-expanded { - @media (max-width: $screen-lg-min) { + @media (max-width: $screen-md-max) { .note-header { .note-headline-light { display: block; diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 6fab97a71aa..f8677f93fe0 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -180,7 +180,7 @@ .modal-dialog { width: 380px; - @media (max-width: $screen-sm-min) { + @media (max-width: $screen-xs-max) { width: auto; } @@ -261,4 +261,4 @@ table.u2f-registrations { td:not(:last-child) { border-right: solid 1px transparent; } -} \ No newline at end of file +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 0562ee7b178..1cf7587dbb4 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -189,7 +189,7 @@ } .download-button { - @media (max-width: $screen-lg-min) { + @media (max-width: $screen-md-max) { margin-left: 0; } } diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss index 69288b31cc4..779db77da60 100644 --- a/app/assets/stylesheets/pages/stat_graph.scss +++ b/app/assets/stylesheets/pages/stat_graph.scss @@ -37,7 +37,7 @@ @include make-md-column(6); margin-top: 10px; - @media (max-width: $screen-sm-min) { + @media (max-width: $screen-xs-max) { width: 100%; } } -- cgit v1.2.1 From cc1c9f8bedc102033492abfb3846e01da142f288 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 30 Nov 2016 23:42:16 -0600 Subject: fix incorrect min-width media queries --- app/assets/stylesheets/framework/pagination.scss | 4 ++-- app/assets/stylesheets/pages/tree.scss | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss index cb2c351c368..b37c1d0d670 100644 --- a/app/assets/stylesheets/framework/pagination.scss +++ b/app/assets/stylesheets/framework/pagination.scss @@ -43,7 +43,7 @@ /** * Small screen pagination */ -@media (max-width: $screen-xs) { +@media (max-width: $screen-xs-min) { .gl-pagination { .pagination li a { padding: 6px 10px; @@ -62,7 +62,7 @@ /** * Medium screen pagination */ -@media (min-width: $screen-xs) and (max-width: $screen-md-max) { +@media (min-width: $screen-xs-min) and (max-width: $screen-md-max) { .gl-pagination { .page { display: none; diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 2b836fa1f4a..20ad63be045 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -31,7 +31,7 @@ .last-commit { @include str-truncated(506px); - @media (min-width: $screen-sm-max) and (max-width: $screen-md-max) { + @media (min-width: $screen-md-min) and (max-width: $screen-md-max) { @include str-truncated(450px); } -- cgit v1.2.1 From eed2de205852afbc4ff56eaaff7e9299f7928317 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 1 Dec 2016 00:11:21 -0600 Subject: use HTMLElement.dataset over getAttribute since all browsers GitLab supports implement it --- app/assets/javascripts/subscription.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/subscription.js.es6 b/app/assets/javascripts/subscription.js.es6 index d5876ee3944..02be594135d 100644 --- a/app/assets/javascripts/subscription.js.es6 +++ b/app/assets/javascripts/subscription.js.es6 @@ -24,7 +24,7 @@ button.classList.add('disabled'); const isSubscribed = buttonSpan.innerHTML.trim().toLowerCase() !== 'subscribe'; - const toggleActionUrl = this.containerElm.getAttribute('data-url'); + const toggleActionUrl = this.containerElm.dataset.url; $.post(toggleActionUrl, () => { button.classList.remove('disabled'); -- cgit v1.2.1 From 1f6ec183154369f8a11d63713970cded0b1474fa Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 1 Dec 2016 00:14:12 -0600 Subject: remove selector string option from Subscription constructor --- app/assets/javascripts/subscription.js.es6 | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/subscription.js.es6 b/app/assets/javascripts/subscription.js.es6 index 02be594135d..62d1604fe9e 100644 --- a/app/assets/javascripts/subscription.js.es6 +++ b/app/assets/javascripts/subscription.js.es6 @@ -2,12 +2,10 @@ (() => { class Subscription { - constructor(containerSelector) { - this.containerElm = (typeof containerSelector === 'string') - ? document.querySelector(containerSelector) - : containerSelector; + constructor(containerElm) { + this.containerElm = containerElm; - const subscribeButton = this.containerElm.querySelector('.js-subscribe-button'); + const subscribeButton = containerElm.querySelector('.js-subscribe-button'); if (subscribeButton) { // remove class so we don't bind twice subscribeButton.classList.remove('js-subscribe-button'); -- cgit v1.2.1 From 5747b0d3ed2a658a5a452e29aefba1aea5debc04 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Thu, 1 Dec 2016 16:17:20 +0800 Subject: Make deleting with optimistic locking respect NULL For now deleting with optimistic locking is broken when lock_version is still NULL, because Rails would try to delete with `lock_version = 0` while in the database the column is still `NULL`. The monkey patches would force Rails just pass whatever in the column, and stop Rails from casting `NULL` into `0` when the value is read from database. Closes #24766 --- config/initializers/ar_monkey_patch.rb | 18 ++++++++++++++++ spec/services/projects/destroy_service_spec.rb | 30 ++++++++++++++++++++------ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/config/initializers/ar_monkey_patch.rb b/config/initializers/ar_monkey_patch.rb index 0da584626ee..5506873344f 100644 --- a/config/initializers/ar_monkey_patch.rb +++ b/config/initializers/ar_monkey_patch.rb @@ -52,6 +52,24 @@ module ActiveRecord raise end end + + # This is patched because we need it to query `lock_version IS NULL` + # rather than `lock_version = 0` whenever lock_version is NULL. + def relation_for_destroy + return super unless locking_enabled? + + column_name = self.class.locking_column + table_name = self.class.quoted_table_name + super.where("#{table_name}.#{column_name}" => self[column_name]) + end + end + + # This is patched because we want `lock_version` default to `NULL` + # rather than `0` + class LockingType < SimpleDelegator + def type_cast_from_database(value) + super + end end end end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 7dcd03496bb..90771825f5c 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -7,15 +7,21 @@ describe Projects::DestroyService, services: true do let!(:remove_path) { path.sub(/\.git\Z/, "+#{project.id}+deleted.git") } let!(:async) { false } # execute or async_execute + shared_examples 'deleting the project' do + it 'deletes the project' do + expect(Project.all).not_to include(project) + expect(Dir.exist?(path)).to be_falsey + expect(Dir.exist?(remove_path)).to be_falsey + end + end + context 'Sidekiq inline' do before do # Run sidekiq immediatly to check that renamed repository will be removed Sidekiq::Testing.inline! { destroy_project(project, user, {}) } end - it { expect(Project.all).not_to include(project) } - it { expect(Dir.exist?(path)).to be_falsey } - it { expect(Dir.exist?(remove_path)).to be_falsey } + it_behaves_like 'deleting the project' end context 'Sidekiq fake' do @@ -38,11 +44,21 @@ describe Projects::DestroyService, services: true do Sidekiq::Testing.inline! { destroy_project(project, user, {}) } end - it 'deletes the project' do - expect(Project.all).not_to include(project) - expect(Dir.exist?(path)).to be_falsey - expect(Dir.exist?(remove_path)).to be_falsey + it_behaves_like 'deleting the project' + end + + context 'delete with pipeline' do # which has optimistic locking + let!(:pipeline) { create(:ci_pipeline, project: project) } + + before do + expect(project).to receive(:destroy!).and_call_original + + perform_enqueued_jobs do + destroy_project(project, user, {}) + end end + + it_behaves_like 'deleting the project' end context 'container registry' do -- cgit v1.2.1 From 1105597303496e60308f93441b591a6f7dfadc74 Mon Sep 17 00:00:00 2001 From: Adam Niedzielski <adamsunday@gmail.com> Date: Thu, 1 Dec 2016 09:45:06 +0100 Subject: Refactor JiraService by moving code out of JiraService#execute method The implicit interface of project services states that the "execute" method is meant to be called when project hooks are executed. Currently JiraService does not support any project events even though JiraService#supported_events says that "commit" and "merge_request" are supported. They are only used to render correct options in JIRA configuration screen, but they are not supported. Because of that, this commit makes "execute" method a no-op. --- app/models/project_services/jira_service.rb | 57 +++++++++++----------- app/services/issues/close_service.rb | 2 +- changelogs/unreleased/clean-up-jira-service.yml | 4 ++ spec/models/project_services/jira_service_spec.rb | 40 +++++++++------ spec/services/merge_requests/merge_service_spec.rb | 2 +- 5 files changed, 61 insertions(+), 44 deletions(-) create mode 100644 changelogs/unreleased/clean-up-jira-service.yml diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 70bbbbcda85..894315a8593 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -9,6 +9,9 @@ class JiraService < IssueTrackerService before_update :reset_password + # This is confusing, but JiraService does not really support these events. + # The values here are required to display correct options in the service + # configuration screen. def supported_events %w(commit merge_request) end @@ -105,18 +108,29 @@ class JiraService < IssueTrackerService "#{url}/secure/CreateIssue.jspa" end - def execute(push, issue = nil) - if issue.nil? - # No specific issue, that means - # we just want to test settings - test_settings - else - jira_issue = jira_request { client.Issue.find(issue.iid) } + def execute(push) + # This method is a no-op, because currently JiraService does not + # support any events. + end - return false unless jira_issue.present? + def close_issue(entity, external_issue) + issue = jira_request { client.Issue.find(external_issue.iid) } - close_issue(push, jira_issue) - end + return if issue.nil? || issue.resolution.present? || !jira_issue_transition_id.present? + + commit_id = if entity.is_a?(Commit) + entity.id + elsif entity.is_a?(MergeRequest) + entity.diff_head_sha + end + + commit_url = build_entity_url(:commit, commit_id) + + # Depending on the JIRA project's workflow, a comment during transition + # may or may not be allowed. Refresh the issue after transition and check + # if it is closed, so we don't have one comment for every commit. + issue = jira_request { client.Issue.find(issue.key) } if transition_issue(issue) + add_issue_solved_comment(issue, commit_id, commit_url) if issue.resolution end def create_cross_reference_note(mentioned, noteable, author) @@ -156,6 +170,11 @@ class JiraService < IssueTrackerService "Please fill in Password and Username." end + def test(_) + result = test_settings + { success: result.present?, result: result } + end + def can_test? username.present? && password.present? end @@ -182,24 +201,6 @@ class JiraService < IssueTrackerService end end - def close_issue(entity, issue) - return if issue.nil? || issue.resolution.present? || !jira_issue_transition_id.present? - - commit_id = if entity.is_a?(Commit) - entity.id - elsif entity.is_a?(MergeRequest) - entity.diff_head_sha - end - - commit_url = build_entity_url(:commit, commit_id) - - # Depending on the JIRA project's workflow, a comment during transition - # may or may not be allowed. Refresh the issue after transition and check - # if it is closed, so we don't have one comment for every commit. - issue = jira_request { client.Issue.find(issue.key) } if transition_issue(issue) - add_issue_solved_comment(issue, commit_id, commit_url) if issue.resolution - end - def transition_issue(issue) issue.transitions.build.save(transition: { id: jira_issue_transition_id }) end diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb index ab4c51386a4..f1030912c68 100644 --- a/app/services/issues/close_service.rb +++ b/app/services/issues/close_service.rb @@ -17,7 +17,7 @@ module Issues # allowed to close the given issue. def close_issue(issue, commit: nil, notifications: true, system_note: true) if project.jira_tracker? && project.jira_service.active - project.jira_service.execute(commit, issue) + project.jira_service.close_issue(commit, issue) todo_service.close_issue(issue, current_user) return issue end diff --git a/changelogs/unreleased/clean-up-jira-service.yml b/changelogs/unreleased/clean-up-jira-service.yml new file mode 100644 index 00000000000..a309cb57532 --- /dev/null +++ b/changelogs/unreleased/clean-up-jira-service.yml @@ -0,0 +1,4 @@ +--- +title: Refactor JiraService by moving code out of JiraService#execute method +merge_request: 7756 +author: diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index f5da967cd14..862e3a72a73 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -68,7 +68,7 @@ describe JiraService, models: true do end end - describe "Execute" do + describe '#close_issue' do let(:custom_base_url) { 'http://custom_url' } let(:user) { create(:user) } let(:project) { create(:project) } @@ -101,12 +101,10 @@ describe JiraService, models: true do @jira_service.save project_issues_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123' - @project_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/project/GitLabProject' @transitions_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions' @comment_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/comment' @remote_link_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/remotelink' - WebMock.stub_request(:get, @project_url) WebMock.stub_request(:get, project_issues_url) WebMock.stub_request(:post, @transitions_url) WebMock.stub_request(:post, @comment_url) @@ -114,7 +112,7 @@ describe JiraService, models: true do end it "calls JIRA API" do - @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) + @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) expect(WebMock).to have_requested(:post, @comment_url).with( body: /Issue solved with/ @@ -124,7 +122,7 @@ describe JiraService, models: true do # Check https://developer.atlassian.com/jiradev/jira-platform/guides/other/guide-jira-remote-issue-links/fields-in-remote-issue-links # for more information it "creates Remote Link reference in JIRA for comment" do - @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) + @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) # Creates comment expect(WebMock).to have_requested(:post, @comment_url) @@ -146,7 +144,7 @@ describe JiraService, models: true do it "does not send comment or remote links to issues already closed" do allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(true) - @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) + @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) expect(WebMock).not_to have_requested(:post, @comment_url) expect(WebMock).not_to have_requested(:post, @remote_link_url) @@ -155,7 +153,7 @@ describe JiraService, models: true do it "references the GitLab commit/merge request" do stub_config_setting(base_url: custom_base_url) - @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) + @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) expect(WebMock).to have_requested(:post, @comment_url).with( body: /#{custom_base_url}\/#{project.path_with_namespace}\/commit\/#{merge_request.diff_head_sha}/ @@ -170,7 +168,7 @@ describe JiraService, models: true do { script_name: '/gitlab' } end - @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) + @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) expect(WebMock).to have_requested(:post, @comment_url).with( body: /#{Gitlab.config.gitlab.url}\/#{project.path_with_namespace}\/commit\/#{merge_request.diff_head_sha}/ @@ -178,19 +176,33 @@ describe JiraService, models: true do end it "calls the api with jira_issue_transition_id" do - @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) + @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) expect(WebMock).to have_requested(:post, @transitions_url).with( body: /custom-id/ ).once end + end - context "when testing" do - it "tries to get jira project" do - @jira_service.execute(nil) + describe '#test_settings' do + let(:jira_service) do + described_class.new( + url: 'http://jira.example.com', + username: 'gitlab_jira_username', + password: 'gitlab_jira_password', + project_key: 'GitLabProject' + ) + end + let(:project_url) { 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/project/GitLabProject' } - expect(WebMock).to have_requested(:get, @project_url) - end + before do + WebMock.stub_request(:get, project_url) + end + + it 'tries to get JIRA project' do + jira_service.test_settings + + expect(WebMock).to have_requested(:get, project_url) end end diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index dff1781d2aa..5a89acc96a4 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -75,7 +75,7 @@ describe MergeRequests::MergeService, services: true do commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}") allow(merge_request).to receive(:commits).and_return([commit]) - expect_any_instance_of(JiraService).to receive(:close_issue).with(merge_request, an_instance_of(JIRA::Resource::Issue)).once + expect_any_instance_of(JiraService).to receive(:close_issue).with(merge_request, jira_issue).once service.execute(merge_request) end -- cgit v1.2.1 From 7839aa55f57e5eb22141ed068cf43a29aac847f6 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Thu, 1 Dec 2016 17:17:04 +0800 Subject: Use Arel to avoid MySQL triple quoting --- config/initializers/ar_monkey_patch.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config/initializers/ar_monkey_patch.rb b/config/initializers/ar_monkey_patch.rb index 5506873344f..6979f4641b0 100644 --- a/config/initializers/ar_monkey_patch.rb +++ b/config/initializers/ar_monkey_patch.rb @@ -59,8 +59,7 @@ module ActiveRecord return super unless locking_enabled? column_name = self.class.locking_column - table_name = self.class.quoted_table_name - super.where("#{table_name}.#{column_name}" => self[column_name]) + super.where(self.class.arel_table[column_name].eq(self[column_name])) end end -- cgit v1.2.1 From 85c4aa4a6725407a90f8b313488584f193e1b55a Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Thu, 1 Dec 2016 09:34:15 +0100 Subject: Copy-edit text about right balance in code reviews [ci skip] --- doc/development/code_review.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 709cefcb5c6..52ee633c43a 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -76,25 +76,25 @@ experience, refactors the existing code). Then: ## The right balance -One of the most difficult things during the code review is finding the right +One of the most difficult things during code review is finding the right balance in how deep the reviewer can interfere with the code created by a reviewee. -- Learning how to find the right balance takes time, that is why we have +- Learning how to find the right balance takes time; that is why we have minibosses that become merge request endbosses after some time spent on reviewing merge requests. - Finding bugs and improving code style is important, but thinking about good design is important as well. Building abstractions and good design is what - makes it possible to hide complexity and is a leverage for the future work. -- Asking reviewee to change the design sometimes means the complete rewrite of - the contributed code. It is usually a good idea to ask other merge request - endboss before doing it, but have the courage to do it when you believe it is - important. + makes it possible to hide complexity and makes future changes easier. +- Asking the reviewee to change the design sometimes means the complete rewrite + of the contributed code. It's usually a good idea to ask another merge + request endboss before doing it, but have the courage to do it when you + believe it is important. - There is a difference in doing things right and doing things right now. Ideally, we should do the former, but in the real world we need the latter as - well. The good example is a security fix which should be released as soon as - possible. Asking reviewee to do the major refactoring in the merge request - that is an urgent fix should be avoided. + well. A good example is a security fix which should be released as soon as + possible. Asking the reviewee to do the major refactoring in the merge + request that is an urgent fix should be avoided. - Doing things well today is usually better than doing something perfectly tomorrow. Shipping a kludge today is usually worse than doing something well tomorrow. When you are not able to find the right balance, ask other people -- cgit v1.2.1 From 79d99d470faf5cf088a0b76ae6bb2ec280c4c4a8 Mon Sep 17 00:00:00 2001 From: Robert Schilling <rschilling@student.tugraz.at> Date: Wed, 30 Nov 2016 18:02:58 +0100 Subject: API: Expose committer details for a commit --- changelogs/unreleased/api-expose-commiter-details.yml | 4 ++++ doc/api/commits.md | 8 ++++++++ lib/api/entities.rb | 1 + spec/requests/api/commits_spec.rb | 9 +++++++-- 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/api-expose-commiter-details.yml diff --git a/changelogs/unreleased/api-expose-commiter-details.yml b/changelogs/unreleased/api-expose-commiter-details.yml new file mode 100644 index 00000000000..5ee34adc5c9 --- /dev/null +++ b/changelogs/unreleased/api-expose-commiter-details.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Expose committer details for commits' +merge_request: +author: Robert Schilling diff --git a/doc/api/commits.md b/doc/api/commits.md index e1ed99d98d3..0170af00e0e 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -29,6 +29,8 @@ Example response: "title": "Replace sanitize with escape once", "author_name": "Dmitriy Zaporozhets", "author_email": "dzaporozhets@sphereconsultinginc.com", + "committer_name": "Administrator", + "committer_email": "admin@example.com", "created_at": "2012-09-20T11:50:22+03:00", "message": "Replace sanitize with escape once", "allow_failure": false @@ -39,6 +41,8 @@ Example response: "title": "Sanitize for network graph", "author_name": "randx", "author_email": "dmitriy.zaporozhets@gmail.com", + "committer_name": "Dmitriy", + "committer_email": "dmitriy.zaporozhets@gmail.com", "created_at": "2012-09-20T09:06:12+03:00", "message": "Sanitize for network graph", "allow_failure": false @@ -115,6 +119,8 @@ Example response: "title": "some commit message", "author_name": "Dmitriy Zaporozhets", "author_email": "dzaporozhets@sphereconsultinginc.com", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dzaporozhets@sphereconsultinginc.com", "created_at": "2016-09-20T09:26:24.000-07:00", "message": "some commit message", "parent_ids": [ @@ -159,6 +165,8 @@ Example response: "title": "Sanitize for network graph", "author_name": "randx", "author_email": "dmitriy.zaporozhets@gmail.com", + "committer_name": "Dmitriy", + "committer_email": "dmitriy.zaporozhets@gmail.com", "created_at": "2012-09-20T09:06:12+03:00", "message": "Sanitize for network graph", "committed_date": "2012-09-20T09:06:12+03:00", diff --git a/lib/api/entities.rb b/lib/api/entities.rb index d5dfb8d00be..899d68bc6c7 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -174,6 +174,7 @@ module API class RepoCommit < Grape::Entity expose :id, :short_id, :title, :author_name, :author_email, :created_at + expose :committer_name, :committer_email expose :safe_message, as: :message end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index a6e8550fac3..314a157b7a0 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -18,11 +18,14 @@ describe API::API, api: true do before { project.team << [user2, :reporter] } it "returns project commits" do + commit = project.repository.commit get api("/projects/#{project.id}/repository/commits", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(200) expect(json_response).to be_an Array - expect(json_response.first['id']).to eq(project.repository.commit.id) + expect(json_response.first['id']).to eq(commit.id) + expect(json_response.first['committer_name']).to eq(commit.committer_name) + expect(json_response.first['committer_email']).to eq(commit.committer_email) end end @@ -134,6 +137,8 @@ describe API::API, api: true do expect(response).to have_http_status(201) expect(json_response['title']).to eq(message) + expect(json_response['committer_name']).to eq(user.name) + expect(json_response['committer_email']).to eq(user.email) end it 'returns a 400 bad request if file exists' do -- cgit v1.2.1 From dbce8432eff53f2b7051be996f8ac3b304e73e0a Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <jacob@gitlab.com> Date: Thu, 1 Dec 2016 11:31:12 +0100 Subject: Use gitlab-workhorse 1.1.0 --- GITLAB_WORKHORSE_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 7dea76edb3d..9084fa2f716 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -1.0.1 +1.1.0 -- cgit v1.2.1 From 41a0dd5c052d8db8286d028a5bca56da65108ed6 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Thu, 1 Dec 2016 12:13:48 +0100 Subject: Fix wrong link in builds artifacts admin docs [ci skip] --- doc/administration/build_artifacts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/administration/build_artifacts.md b/doc/administration/build_artifacts.md index 64353f7282b..3ba8387c7f0 100644 --- a/doc/administration/build_artifacts.md +++ b/doc/administration/build_artifacts.md @@ -84,7 +84,7 @@ _The artifacts are stored by default in ## Set the maximum file size of the artifacts Provided the artifacts are enabled, you can change the maximum file size of the -artifacts through the [Admin area settings](../user/admin_area/settings/continuous_integration#maximum-artifacts-size). +artifacts through the [Admin area settings](../user/admin_area/settings/continuous_integration.md#maximum-artifacts-size). [reconfigure gitlab]: restart_gitlab.md "How to restart GitLab" [restart gitlab]: restart_gitlab.md "How to restart GitLab" -- cgit v1.2.1 From cb6f8cdfc23a18daf33288c95ae8badec63f53ad Mon Sep 17 00:00:00 2001 From: Adam Niedzielski <adamsunday@gmail.com> Date: Thu, 1 Dec 2016 12:17:30 +0100 Subject: Replace references to MergeRequestDiff#commits with st_commits when we care only about the number of commits We do not have to instantiate all objects in this case. --- .../projects/merge_requests_controller.rb | 4 +-- app/models/ci/build.rb | 2 +- app/models/merge_request.rb | 17 +++++++---- app/models/merge_request_diff.rb | 10 +++---- app/services/merge_requests/refresh_service.rb | 4 +-- .../merge_requests/show/_versions.html.haml | 2 +- .../projects/merge_requests/widget/_open.html.haml | 2 +- .../unreleased/use-st-commits-where-possible.yml | 5 ++++ spec/models/build_spec.rb | 4 +-- spec/models/merge_request_diff_spec.rb | 34 +++++++++++----------- spec/models/merge_request_spec.rb | 33 ++++++++++++++++----- 11 files changed, 73 insertions(+), 44 deletions(-) create mode 100644 changelogs/unreleased/use-st-commits-where-possible.yml diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index f47df8b623b..d2cef52842c 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -492,7 +492,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def validates_merge_request # Show git not found page # if there is no saved commits between source & target branch - if @merge_request.commits.blank? + if @merge_request.has_no_commits? # and if target branch doesn't exist return invalid_mr unless @merge_request.target_branch_exists? end @@ -500,7 +500,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def define_show_vars @noteable = @merge_request - @commits_count = @merge_request.commits.count + @commits_count = @merge_request.commits_count if @merge_request.locked_long_ago? @merge_request.unlock_mr diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index e7d33bd26db..88c46076df6 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -203,7 +203,7 @@ module Ci .reorder(iid: :asc) merge_requests.find do |merge_request| - merge_request.commits.any? { |ci| ci.id == pipeline.sha } + merge_request.commits_sha.include?(pipeline.sha) end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 64990f8134e..bfb016df46d 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -22,7 +22,8 @@ class MergeRequest < ActiveRecord::Base after_create :ensure_merge_request_diff, unless: :importing? after_update :reload_diff_if_branch_changed - delegate :commits, :real_size, to: :merge_request_diff, prefix: nil + delegate :commits, :real_size, :commits_sha, :commits_count, + 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 @@ -662,7 +663,7 @@ class MergeRequest < ActiveRecord::Base end def broken? - self.commits.blank? || branch_missing? || cannot_be_merged? + has_no_commits? || branch_missing? || cannot_be_merged? end def can_be_merged_by?(user) @@ -770,10 +771,6 @@ class MergeRequest < ActiveRecord::Base diverged_commits_count > 0 end - def commits_sha - commits.map(&:sha) - end - def head_pipeline return unless diff_head_sha && source_project @@ -871,4 +868,12 @@ class MergeRequest < ActiveRecord::Base @conflicts_can_be_resolved_in_ui = false end end + + def has_commits? + commits_count > 0 + end + + def has_no_commits? + !has_commits? + end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 58a24eb84cb..b8f36a2c958 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -127,11 +127,7 @@ class MergeRequestDiff < ActiveRecord::Base end def commits_sha - if @commits - commits.map(&:sha) - else - st_commits.map { |commit| commit[:id] } - end + st_commits.map { |commit| commit[:id] } end def diff_refs @@ -176,6 +172,10 @@ class MergeRequestDiff < ActiveRecord::Base CompareService.new.execute(project, head_commit_sha, project, sha, straight: straight) end + def commits_count + st_commits.count + end + private # Old GitLab implementations may have generated diffs as ["--broken-diff"]. diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 22596b4014a..e4056306bc4 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -63,7 +63,7 @@ module MergeRequests if merge_request.source_branch == @branch_name || force_push? merge_request.reload_diff else - mr_commit_ids = merge_request.commits.map(&:id) + mr_commit_ids = merge_request.commits_sha push_commit_ids = @commits.map(&:id) matches = mr_commit_ids & push_commit_ids merge_request.reload_diff if matches.any? @@ -123,7 +123,7 @@ module MergeRequests return unless @commits.present? merge_requests_for_source_branch.each do |merge_request| - mr_commit_ids = Set.new(merge_request.commits.map(&:id)) + mr_commit_ids = Set.new(merge_request.commits_sha) new_commits, existing_commits = @commits.partition do |commit| mr_commit_ids.include?(commit.id) diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml index eab48b78cb3..5cc92595fe0 100644 --- a/app/views/projects/merge_requests/show/_versions.html.haml +++ b/app/views/projects/merge_requests/show/_versions.html.haml @@ -27,7 +27,7 @@ version #{version_index(merge_request_diff)} .monospace #{short_sha(merge_request_diff.head_commit_sha)} %small - #{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)}, + #{number_with_delimiter(merge_request_diff.commits_count)} #{'commit'.pluralize(merge_request_diff.commits_count)}, = time_ago_with_tooltip(merge_request_diff.created_at) - if @merge_request_diff.base_commit_sha diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index 20c93930abc..eee711dc5af 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -11,7 +11,7 @@ = render 'projects/merge_requests/widget/open/archived' - elsif @merge_request.branch_missing? = render 'projects/merge_requests/widget/open/missing_branch' - - elsif @merge_request.commits.blank? + - elsif @merge_request.has_no_commits? = render 'projects/merge_requests/widget/open/nothing' - elsif @merge_request.unchecked? = render 'projects/merge_requests/widget/open/check' diff --git a/changelogs/unreleased/use-st-commits-where-possible.yml b/changelogs/unreleased/use-st-commits-where-possible.yml new file mode 100644 index 00000000000..e4395461560 --- /dev/null +++ b/changelogs/unreleased/use-st-commits-where-possible.yml @@ -0,0 +1,5 @@ +--- +title: Replace references to MergeRequestDiff#commits with st_commits when we care + only about the number of commits +merge_request: 7668 +author: diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index ef07f2275b1..d4970e38f7c 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -730,8 +730,8 @@ describe Ci::Build, models: true do pipeline2 = create(:ci_pipeline, project: project) @build2 = create(:ci_build, pipeline: pipeline2) - commits = [double(id: pipeline.sha), double(id: pipeline2.sha)] - allow(@merge_request).to receive(:commits).and_return(commits) + allow(@merge_request).to receive(:commits_sha). + and_return([pipeline.sha, pipeline2.sha]) allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request]) end diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index e5007424041..eb876d105da 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -77,24 +77,13 @@ describe MergeRequestDiff, models: true do end describe '#commits_sha' do - shared_examples 'returning all commits SHA' do - it 'returns all commits SHA' do - commits_sha = subject.commits_sha + it 'returns all commits SHA using serialized commits' do + subject.st_commits = [ + { id: 'sha1' }, + { id: 'sha2' } + ] - expect(commits_sha).to eq(subject.commits.map(&:sha)) - end - end - - context 'when commits were loaded' do - before do - subject.commits - end - - it_behaves_like 'returning all commits SHA' - end - - context 'when commits were not loaded' do - it_behaves_like 'returning all commits SHA' + expect(subject.commits_sha).to eq(['sha1', 'sha2']) end end @@ -113,4 +102,15 @@ describe MergeRequestDiff, models: true do expect(diffs.size).to eq(3) end end + + describe '#commits_count' do + it 'returns number of commits using serialized commits' do + subject.st_commits = [ + { id: 'sha1' }, + { id: 'sha2' } + ] + + expect(subject.commits_count).to eq 2 + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 26034cb1c7b..ec22ef93465 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -557,16 +557,13 @@ describe MergeRequest, models: true do end describe '#commits_sha' do - let(:commit0) { double('commit0', sha: 'sha1') } - let(:commit1) { double('commit1', sha: 'sha2') } - let(:commit2) { double('commit2', sha: 'sha3') } - before do - allow(subject.merge_request_diff).to receive(:commits).and_return([commit0, commit1, commit2]) + allow(subject.merge_request_diff).to receive(:commits_sha). + and_return(['sha1']) end - it 'returns sha of commits' do - expect(subject.commits_sha).to contain_exactly('sha1', 'sha2', 'sha3') + it 'delegates to merge request diff' do + expect(subject.commits_sha).to eq ['sha1'] end end @@ -1440,4 +1437,26 @@ describe MergeRequest, models: true do end end end + + describe '#has_commits?' do + before do + allow(subject.merge_request_diff).to receive(:commits_count). + and_return(2) + end + + it 'returns true when merge request diff has commits' do + expect(subject.has_commits?).to be_truthy + end + end + + describe '#has_no_commits?' do + before do + allow(subject.merge_request_diff).to receive(:commits_count). + and_return(0) + end + + it 'returns true when merge request diff has 0 commits' do + expect(subject.has_no_commits?).to be_truthy + end + end end -- cgit v1.2.1 From dd7f0fb687b2507c0f45b772bc2a4465650e5a7b Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Thu, 1 Dec 2016 12:35:24 +0100 Subject: Clarify that a commit SHA doesn't work for CI triggers [ci skip] --- doc/ci/triggers/README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md index cf7c55f75f2..efca05af7b8 100644 --- a/doc/ci/triggers/README.md +++ b/doc/ci/triggers/README.md @@ -6,7 +6,7 @@ GitLab 8.12 has a completely redesigned build permissions system. Read all about the [new model and its implications](../../user/project/new_ci_build_permissions_model.md#build-triggers). -Triggers can be used to force a rebuild of a specific branch, tag or commit, +Triggers can be used to force a rebuild of a specific `ref` (branch or tag) with an API call. ## Add a trigger @@ -29,6 +29,10 @@ irreversible. ## Trigger a build +> **Note**: +Valid refs are only the branches and tags. If you pass a commit SHA as a ref, +it will not trigger a build. + To trigger a build you need to send a `POST` request to GitLab's API endpoint: ``` @@ -36,8 +40,8 @@ POST /projects/:id/trigger/builds ``` The required parameters are the trigger's `token` and the Git `ref` on which -the trigger will be performed. Valid refs are the branch, the tag or the commit -SHA. The `:id` of a project can be found by [querying the API](../../api/projects.md) +the trigger will be performed. Valid refs are the branch and the tag. The `:id` +of a project can be found by [querying the API](../../api/projects.md) or by visiting the **Triggers** page which provides self-explanatory examples. When a rebuild is triggered, the information is exposed in GitLab's UI under -- cgit v1.2.1 From 532f8cbd38d61ba73886ea3ed0dbce1864819bec Mon Sep 17 00:00:00 2001 From: Andrew Smith <espadav8@gmail.com> Date: Wed, 30 Nov 2016 22:26:22 +1000 Subject: If SSH prototol is disabled don't say the user requires SSH keys --- app/models/user.rb | 2 +- ...ide-prompt-to-add-ssh-key-if-ssh-protocol-is-disabled.yml | 4 ++++ spec/models/user_spec.rb | 12 ++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/24921-hide-prompt-to-add-ssh-key-if-ssh-protocol-is-disabled.yml diff --git a/app/models/user.rb b/app/models/user.rb index b54ce14f0bf..b9bb4a9e3f7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -512,7 +512,7 @@ class User < ActiveRecord::Base end def require_ssh_key? - keys.count == 0 + keys.count == 0 && Gitlab::ProtocolAccess.allowed?('ssh') end def require_password? diff --git a/changelogs/unreleased/24921-hide-prompt-to-add-ssh-key-if-ssh-protocol-is-disabled.yml b/changelogs/unreleased/24921-hide-prompt-to-add-ssh-key-if-ssh-protocol-is-disabled.yml new file mode 100644 index 00000000000..4d4019e770e --- /dev/null +++ b/changelogs/unreleased/24921-hide-prompt-to-add-ssh-key-if-ssh-protocol-is-disabled.yml @@ -0,0 +1,4 @@ +--- +title: Don't display prompt to add SSH keys if SSH protocol is disabled +merge_request: 7840 +author: Andrew Smith (EspadaV8) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 14c891994d0..475f4419d58 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -575,6 +575,18 @@ describe User, models: true do end end end + + context 'when current_application_settings.enabled_git_access_protocol does not contain SSH' do + before do + stub_application_setting(enabled_git_access_protocol: 'HTTP') + end + + it "doesn't require user to have SSH key" do + user = build(:user) + + expect(user.require_ssh_key?).to be_falsey + end + end end describe '.find_by_any_email' do -- cgit v1.2.1 From bc8b6024b1a993f4f2ba673e542a02395ba691a0 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Thu, 1 Dec 2016 13:19:00 +0100 Subject: Small refactor of webhooks docs [ci skip] --- doc/web_hooks/ssl.png | Bin 23191 -> 27799 bytes doc/web_hooks/web_hooks.md | 81 +++++++++++++++++++++++++-------------------- 2 files changed, 46 insertions(+), 35 deletions(-) diff --git a/doc/web_hooks/ssl.png b/doc/web_hooks/ssl.png index a552888ed96..21ddec4ebdf 100644 Binary files a/doc/web_hooks/ssl.png and b/doc/web_hooks/ssl.png differ diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index cd37189fdd2..1659dd1f6cb 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -1,17 +1,21 @@ # Webhooks -_**Note:** -Starting from GitLab 8.5:_ +>**Note:** +Starting from GitLab 8.5: +- the `repository` key is deprecated in favor of the `project` key +- the `project.ssh_url` key is deprecated in favor of the `project.git_ssh_url` key +- the `project.http_url` key is deprecated in favor of the `project.git_http_url` key -- _the `repository` key is deprecated in favor of the `project` key_ -- _the `project.ssh_url` key is deprecated in favor of the `project.git_ssh_url` key_ -- _the `project.http_url` key is deprecated in favor of the `project.git_http_url` key_ +Project webhooks allow you to trigger a URL if for example new code is pushed or +a new issue is created. You can configure webhooks to listen for specific events +like pushes, issues or merge requests. GitLab will send a POST request with data +to the webhook URL. -Project webhooks allow you to trigger an URL if new code is pushed or a new issue is created. +Webhooks can be used to update an external issue tracker, trigger CI builds, +update a backup mirror, or even deploy to your production server. -You can configure webhooks to listen for specific events like pushes, issues or merge requests. GitLab will send a POST request with data to the webhook URL. - -Webhooks can be used to update an external issue tracker, trigger CI builds, update a backup mirror, or even deploy to your production server. +Navigate to the webhooks page by choosing **Webhooks** from your project's +settings which can be found under the wheel icon in the upper right corner. ## Webhook endpoint tips @@ -26,21 +30,27 @@ GitLab webhooks keep in mind the following things: you are writing a low-level hook this is important to remember. - GitLab ignores the HTTP status code returned by your endpoint. -## Secret Token +## Secret token -If you specify a secret token, it will be sent with the hook request in the `X-Gitlab-Token` HTTP header. Your webhook endpoint can check that to verify that the request is legitimate. +If you specify a secret token, it will be sent with the hook request in the +`X-Gitlab-Token` HTTP header. Your webhook endpoint can check that to verify +that the request is legitimate. -## SSL Verification +## SSL verification By default, the SSL certificate of the webhook endpoint is verified based on -an internal list of Certificate Authorities, -which means the certificate cannot be self-signed. +an internal list of Certificate Authorities, which means the certificate cannot +be self-signed. You can turn this off in the webhook settings in your GitLab projects. ![SSL Verification](ssl.png) -## Push events +## Events + +Below are described the supported events. + +### Push events Triggered when you push to the repository except when pushing tags. @@ -121,7 +131,7 @@ X-Gitlab-Event: Push Hook } ``` -## Tag events +### Tag events Triggered when you create (or delete) tags to the repository. @@ -174,7 +184,7 @@ X-Gitlab-Event: Tag Push Hook } ``` -## Issues events +### Issues events Triggered when a new issue is created or an existing issue was updated/closed/reopened. @@ -240,7 +250,7 @@ X-Gitlab-Event: Issue Hook } } ``` -## Comment events +### Comment events Triggered when a new comment is made on commits, merge requests, issues, and code snippets. The note data will be stored in `object_attributes` (e.g. `note`, `noteable_type`). The @@ -253,7 +263,7 @@ Valid target types: 3. `issue` 4. `snippet` -### Comment on commit +#### Comment on commit **Request header**: @@ -332,7 +342,7 @@ X-Gitlab-Event: Note Hook } ``` -### Comment on merge request +#### Comment on merge request **Request header**: @@ -459,7 +469,7 @@ X-Gitlab-Event: Note Hook } ``` -### Comment on issue +#### Comment on issue **Request header**: @@ -534,7 +544,7 @@ X-Gitlab-Event: Note Hook } ``` -### Comment on code snippet +#### Comment on code snippet **Request header**: @@ -607,7 +617,7 @@ X-Gitlab-Event: Note Hook } ``` -## Merge request events +### Merge request events Triggered when a new merge request is created, an existing merge request was updated/merged/closed or a commit is added in the source branch. @@ -699,7 +709,7 @@ X-Gitlab-Event: Merge Request Hook } ``` -## Wiki Page events +### Wiki Page events Triggered when a wiki page is created or edited. @@ -737,9 +747,9 @@ X-Gitlab-Event: Wiki Page Hook }, "wiki": { "web_url": "http://example.com/root/awesome-project/wikis/home", - "git_ssh_url": "git@example.com:root/awesome-project.wiki.git", - "git_http_url": "http://example.com/root/awesome-project.wiki.git", - "path_with_namespace": "root/awesome-project.wiki", + "git_ssh_url": "git@example.com:root/awesome-project.wiki.git", + "git_http_url": "http://example.com/root/awesome-project.wiki.git", + "path_with_namespace": "root/awesome-project.wiki", "default_branch": "master" }, "object_attributes": { @@ -754,7 +764,7 @@ X-Gitlab-Event: Wiki Page Hook } ``` -## Pipeline events +### Pipeline events Triggered on status change of Pipeline. @@ -922,8 +932,7 @@ X-Gitlab-Event: Pipeline Hook } ``` - -## Build events +### Build events Triggered on status change of a Build. @@ -935,7 +944,7 @@ X-Gitlab-Event: Build Hook **Request Body**: -``` +```json { "object_kind": "build", "ref": "gitlab-script-trigger", @@ -980,12 +989,13 @@ X-Gitlab-Event: Build Hook } ``` -#### Example webhook receiver +## Example webhook receiver If you want to see GitLab's webhooks in action for testing purposes you can use -a simple echo script running in a console session. +a simple echo script running in a console session. For the following script to +work you need to have Ruby installed. -Save the following file as `print_http_body.rb`. +Save the following file as `print_http_body.rb`: ```ruby require 'webrick' @@ -1005,7 +1015,8 @@ Pick an unused port (e.g. 8000) and start the script: `ruby print_http_body.rb 8000`. Then add your server as a webhook receiver in GitLab as `http://my.host:8000/`. -When you press 'Test Hook' in GitLab, you should see something like this in the console. +When you press 'Test Hook' in GitLab, you should see something like this in the +console: ``` {"before":"077a85dd266e6f3573ef7e9ef8ce3343ad659c4e","after":"95cd4a99e93bc4bbabacfa2cd10e6725b1403c60",<SNIP>} -- cgit v1.2.1 From 79b5bfc113973e8564f7d78f5e76347693245900 Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Fri, 25 Nov 2016 13:41:13 +0000 Subject: Save a query on issue and MR lists `any?` on an AR relation performs a `SELECT COUNT`, which we don't need. 1. We are very likely to have issues or MRs, so the `SELECT COUNT` is often unnecessary. 2. Even where there are no items returned, the overhead of the `SELECT *` instead of `SELECT COUNT` is relatively small. Calling `to_a` on the relation lets us use `Enumerable#any?`, which will return immediately if there are objects returned. --- app/views/shared/_issues.html.haml | 2 +- app/views/shared/_merge_requests.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml index baa6d5f8206..26b349e8a62 100644 --- a/app/views/shared/_issues.html.haml +++ b/app/views/shared/_issues.html.haml @@ -1,4 +1,4 @@ -- if @issues.reorder(nil).any? +- if @issues.to_a.any? - @issues.group_by(&:project).each do |group| .panel.panel-default.panel-small - project = group[0] diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml index ca3178395c1..2f3605b4d27 100644 --- a/app/views/shared/_merge_requests.html.haml +++ b/app/views/shared/_merge_requests.html.haml @@ -1,4 +1,4 @@ -- if @merge_requests.reorder(nil).any? +- if @merge_requests.to_a.any? - @merge_requests.group_by(&:target_project).each do |group| .panel.panel-default.panel-small - project = group[0] -- cgit v1.2.1 From 8ee072808651b21535ba4444e3a9e2361c860c49 Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Fri, 25 Nov 2016 13:57:56 +0000 Subject: Count all issuable states at once Instead of doing n queries for n states, do one query to get all the counts grouped by state, and figure out what the count is for each state is from that. We can still cache the individual counts (it can't hurt), but this will help with initial load. Note that the `opened` scope on `Issuable` includes the `opened` and `reopened` states, which is why there's a special case. --- app/finders/issuable_finder.rb | 26 ++++++++++++++++++++++++++ app/helpers/issuables_helper.rb | 9 +++------ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 9a74e36870b..001c83ccb4b 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -49,6 +49,32 @@ class IssuableFinder execute.find_by(*params) end + # We often get counts for each state by running a query per state, and + # counting those results. This is typically slower than running one query + # (even if that query is slower than any of the individual state queries) and + # grouping and counting within that query. + # + def count_by_state + count_params = params.merge(state: nil, sort: nil) + labels_count = label_names.any? ? label_names.count : 1 + finder = self.class.new(current_user, count_params) + counts = Hash.new(0) + + # Searching by label includes a GROUP BY in the query, but ours will be last + # because it is added last. Searching by multiple labels also includes a row + # per issuable, so we have to count those in Ruby - which is bad, but still + # better than performing multiple queries. + # + finder.execute.reorder(nil).group(:state).count.each do |key, value| + counts[Array(key).last.to_sym] += value / labels_count + end + + counts[:all] = counts.values.sum + counts[:opened] += counts[:reopened] + + counts + end + def group return @group if defined?(@group) diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 6584aa3edd5..b85e3f9e60e 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -180,12 +180,9 @@ module IssuablesHelper end def issuables_count_for_state(issuable_type, state) - issuables_finder = public_send("#{issuable_type}_finder") - - params = issuables_finder.params.merge(state: state) - finder = issuables_finder.class.new(issuables_finder.current_user, params) - - finder.execute.page(1).total_count + @counts ||= {} + @counts[issuable_type] ||= public_send("#{issuable_type}_finder").count_by_state + @counts[issuable_type][state] end IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page] -- cgit v1.2.1 From 5eff32946e163455caa00ae67de541f7231759b5 Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Fri, 25 Nov 2016 15:09:41 +0000 Subject: Add changelog entry --- ...24669-merge-request-dashboard-page-takes-over-a-minute-to-load.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/24669-merge-request-dashboard-page-takes-over-a-minute-to-load.yml diff --git a/changelogs/unreleased/24669-merge-request-dashboard-page-takes-over-a-minute-to-load.yml b/changelogs/unreleased/24669-merge-request-dashboard-page-takes-over-a-minute-to-load.yml new file mode 100644 index 00000000000..01b19a47ecd --- /dev/null +++ b/changelogs/unreleased/24669-merge-request-dashboard-page-takes-over-a-minute-to-load.yml @@ -0,0 +1,4 @@ +--- +title: Speed up issuable dashboards +merge_request: +author: -- cgit v1.2.1 From e1e372bab0590291d97cf35690439426a8e89e90 Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Mon, 28 Nov 2016 15:06:40 +0000 Subject: Don't include ignored params in cache key --- app/helpers/issuables_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index b85e3f9e60e..8231f8fa334 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -192,6 +192,7 @@ module IssuablesHelper opts = params.with_indifferent_access opts[:state] = state opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY) + opts.delete_if { |_, value| value.blank? } hexdigest(['issuables_count', issuable_type, opts.sort].flatten.join('-')) end -- cgit v1.2.1 From a610e0dcfc178ad31bcf2392e0d103e2c4ef8407 Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Mon, 28 Nov 2016 16:48:10 +0000 Subject: Fix have_issuable_counts matcher on failure --- spec/support/matchers/have_issuable_counts.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/support/matchers/have_issuable_counts.rb b/spec/support/matchers/have_issuable_counts.rb index 02605d6b70e..92cf3de5448 100644 --- a/spec/support/matchers/have_issuable_counts.rb +++ b/spec/support/matchers/have_issuable_counts.rb @@ -1,9 +1,9 @@ RSpec::Matchers.define :have_issuable_counts do |opts| - match do |actual| - expected_counts = opts.map do |state, count| - "#{state.to_s.humanize} #{count}" - end + expected_counts = opts.map do |state, count| + "#{state.to_s.humanize} #{count}" + end + match do |actual| actual.within '.issues-state-filters' do expected_counts.each do |expected_count| expect(actual).to have_content(expected_count) -- cgit v1.2.1 From 6b4d33566f5f434cc86381a4a1347e42bbe348ee Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Thu, 24 Nov 2016 15:07:44 +0100 Subject: Pass commit data to ProcessCommitWorker By passing commit data to this worker we remove the need for querying the Git repository for every job. This in turn reduces the time spent processing each job. The migration included migrates jobs from the old format to the new format. For this to work properly it requires downtime as otherwise workers may start producing errors until they're using a newer version of the worker code. --- app/models/commit.rb | 4 + app/services/git_push_service.rb | 2 +- app/workers/process_commit_worker.rb | 25 ++- .../process-commit-worker-improvements.yml | 4 + ...124141322_migrate_process_commit_worker_jobs.rb | 92 ++++++++++ spec/lib/gitlab/cycle_analytics/events_spec.rb | 2 +- .../migrate_process_commit_worker_jobs_spec.rb | 194 +++++++++++++++++++++ spec/models/commit_spec.rb | 17 ++ .../projects/cycle_analytics_events_spec.rb | 2 +- spec/services/git_push_service_spec.rb | 6 +- spec/spec_helper.rb | 4 + spec/workers/process_commit_worker_spec.rb | 29 +-- 12 files changed, 356 insertions(+), 25 deletions(-) create mode 100644 changelogs/unreleased/process-commit-worker-improvements.yml create mode 100644 db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb create mode 100644 spec/migrations/migrate_process_commit_worker_jobs_spec.rb diff --git a/app/models/commit.rb b/app/models/commit.rb index 9e7fde9503d..176c524cf7b 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -48,6 +48,10 @@ class Commit max_lines: DIFF_HARD_LIMIT_LINES, } end + + def from_hash(hash, project) + new(Gitlab::Git::Commit.new(hash), project) + end end attr_accessor :raw diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 647930d555c..185556c12cc 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -135,7 +135,7 @@ class GitPushService < BaseService @push_commits.each do |commit| ProcessCommitWorker. - perform_async(project.id, current_user.id, commit.id, default) + perform_async(project.id, current_user.id, commit.to_hash, default) end end diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb index 071741fbacd..e9a5bd7f24e 100644 --- a/app/workers/process_commit_worker.rb +++ b/app/workers/process_commit_worker.rb @@ -10,9 +10,10 @@ class ProcessCommitWorker # project_id - The ID of the project this commit belongs to. # user_id - The ID of the user that pushed the commit. - # commit_sha - The SHA1 of the commit to process. + # commit_hash - Hash containing commit details to use for constructing a + # Commit object without having to use the Git repository. # default - The data was pushed to the default branch. - def perform(project_id, user_id, commit_sha, default = false) + def perform(project_id, user_id, commit_hash, default = false) project = Project.find_by(id: project_id) return unless project @@ -21,10 +22,7 @@ class ProcessCommitWorker return unless user - commit = find_commit(project, commit_sha) - - return unless commit - + commit = build_commit(project, commit_hash) author = commit.author || user process_commit_message(project, commit, user, author, default) @@ -59,9 +57,18 @@ class ProcessCommitWorker update_all(first_mentioned_in_commit_at: commit.committed_date) end - private + def build_commit(project, hash) + date_suffix = '_date' + + # When processing Sidekiq payloads various timestamps are stored as Strings. + # Commit in turn expects Time-like instances upon input, so we have to + # manually parse these values. + hash.each do |key, value| + if key.to_s.end_with?(date_suffix) && value.is_a?(String) + hash[key] = Time.parse(value) + end + end - def find_commit(project, sha) - project.commit(sha) + Commit.from_hash(hash, project) end end diff --git a/changelogs/unreleased/process-commit-worker-improvements.yml b/changelogs/unreleased/process-commit-worker-improvements.yml new file mode 100644 index 00000000000..0038c6e34e6 --- /dev/null +++ b/changelogs/unreleased/process-commit-worker-improvements.yml @@ -0,0 +1,4 @@ +--- +title: Pass commit data to ProcessCommitWorker to reduce Git overhead +merge_request: 7744 +author: diff --git a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb new file mode 100644 index 00000000000..453a44e271a --- /dev/null +++ b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb @@ -0,0 +1,92 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + class Project < ActiveRecord::Base + def self.find_including_path(id) + select("projects.*, CONCAT(namespaces.path, '/', projects.path) AS path_with_namespace"). + joins('INNER JOIN namespaces ON namespaces.id = projects.namespace_id'). + find_by(id: id) + end + + def repository_storage_path + Gitlab.config.repositories.storages[repository_storage] + end + + def repository_path + File.join(repository_storage_path, read_attribute(:path_with_namespace) + '.git') + end + + def repository + @repository ||= Rugged::Repository.new(repository_path) + end + end + + DOWNTIME = true + DOWNTIME_REASON = 'Existing workers will error until they are using a newer version of the code' + + disable_ddl_transaction! + + def up + Sidekiq.redis do |redis| + new_jobs = [] + + while job = redis.lpop('queue:process_commit') + payload = JSON.load(job) + project = Project.find_including_path(payload['args'][0]) + + next unless project + + begin + commit = project.repository.lookup(payload['args'][2]) + rescue Rugged::OdbError + next + end + + hash = { + id: commit.oid, + message: commit.message, + parent_ids: commit.parent_ids, + authored_date: commit.author[:time], + author_name: commit.author[:name], + author_email: commit.author[:email], + committed_date: commit.committer[:time], + committer_email: commit.committer[:email], + committer_name: commit.committer[:name] + } + + payload['args'][2] = hash + + new_jobs << JSON.dump(payload) + end + + redis.multi do |multi| + new_jobs.each do |j| + multi.lpush('queue:process_commit', j) + end + end + end + end + + def down + Sidekiq.redis do |redis| + new_jobs = [] + + while job = redis.lpop('queue:process_commit') + payload = JSON.load(job) + + payload['args'][2] = payload['args'][2]['id'] + + new_jobs << JSON.dump(payload) + end + + redis.multi do |multi| + new_jobs.each do |j| + multi.lpush('queue:process_commit', j) + end + end + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb index 9aeaa6b3ee8..6062e7af4f5 100644 --- a/spec/lib/gitlab/cycle_analytics/events_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb @@ -321,6 +321,6 @@ describe Gitlab::CycleAnalytics::Events do context.update(milestone: milestone) mr = create_merge_request_closing_issue(context) - ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.sha) + ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash) end end diff --git a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb new file mode 100644 index 00000000000..52428547a9f --- /dev/null +++ b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb @@ -0,0 +1,194 @@ +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20161124141322_migrate_process_commit_worker_jobs.rb') + +describe MigrateProcessCommitWorkerJobs do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:commit) { project.commit.raw.raw_commit } + + describe 'Project' do + describe 'find_including_path' do + it 'returns Project instances' do + expect(described_class::Project.find_including_path(project.id)). + to be_an_instance_of(described_class::Project) + end + + it 'selects the full path for every Project' do + migration_project = described_class::Project. + find_including_path(project.id) + + expect(migration_project[:path_with_namespace]). + to eq(project.path_with_namespace) + end + end + + describe '#repository_storage_path' do + it 'returns the storage path for the repository' do + migration_project = described_class::Project. + find_including_path(project.id) + + expect(File.directory?(migration_project.repository_storage_path)). + to eq(true) + end + end + + describe '#repository_path' do + it 'returns the path to the repository' do + migration_project = described_class::Project. + find_including_path(project.id) + + expect(File.directory?(migration_project.repository_path)).to eq(true) + end + end + + describe '#repository' do + it 'returns a Rugged::Repository' do + migration_project = described_class::Project. + find_including_path(project.id) + + expect(migration_project.repository). + to be_an_instance_of(Rugged::Repository) + end + end + end + + describe '#up', :redis do + let(:migration) { described_class.new } + + def job_count + Sidekiq.redis { |r| r.llen('queue:process_commit') } + end + + before do + Sidekiq.redis do |redis| + job = JSON.dump(args: [project.id, user.id, commit.oid]) + redis.lpush('queue:process_commit', job) + end + end + + it 'skips jobs using a project that no longer exists' do + allow(described_class::Project).to receive(:find_including_path). + with(project.id). + and_return(nil) + + migration.up + + expect(job_count).to eq(0) + end + + it 'skips jobs using commits that no longer exist' do + allow_any_instance_of(Rugged::Repository).to receive(:lookup). + with(commit.oid). + and_raise(Rugged::OdbError) + + migration.up + + expect(job_count).to eq(0) + end + + it 'inserts migrated jobs back into the queue' do + migration.up + + expect(job_count).to eq(1) + end + + context 'a migrated job' do + let(:job) do + migration.up + + JSON.load(Sidekiq.redis { |r| r.lpop('queue:process_commit') }) + end + + let(:commit_hash) do + job['args'][2] + end + + it 'includes the project ID' do + expect(job['args'][0]).to eq(project.id) + end + + it 'includes the user ID' do + expect(job['args'][1]).to eq(user.id) + end + + it 'includes the commit ID' do + expect(commit_hash['id']).to eq(commit.oid) + end + + it 'includes the commit message' do + expect(commit_hash['message']).to eq(commit.message) + end + + it 'includes the parent IDs' do + expect(commit_hash['parent_ids']).to eq(commit.parent_ids) + end + + it 'includes the author date' do + expect(commit_hash['authored_date']).to eq(commit.author[:time].to_s) + end + + it 'includes the author name' do + expect(commit_hash['author_name']).to eq(commit.author[:name]) + end + + it 'includes the author Email' do + expect(commit_hash['author_email']).to eq(commit.author[:email]) + end + + it 'includes the commit date' do + expect(commit_hash['committed_date']).to eq(commit.committer[:time].to_s) + end + + it 'includes the committer name' do + expect(commit_hash['committer_name']).to eq(commit.committer[:name]) + end + + it 'includes the committer Email' do + expect(commit_hash['committer_email']).to eq(commit.committer[:email]) + end + end + end + + describe '#down', :redis do + let(:migration) { described_class.new } + + def job_count + Sidekiq.redis { |r| r.llen('queue:process_commit') } + end + + before do + Sidekiq.redis do |redis| + job = JSON.dump(args: [project.id, user.id, commit.oid]) + redis.lpush('queue:process_commit', job) + + migration.up + end + end + + it 'pushes migrated jobs back into the queue' do + migration.down + + expect(job_count).to eq(1) + end + + context 'a migrated job' do + let(:job) do + migration.down + + JSON.load(Sidekiq.redis { |r| r.lpop('queue:process_commit') }) + end + + it 'includes the project ID' do + expect(job['args'][0]).to eq(project.id) + end + + it 'includes the user ID' do + expect(job['args'][1]).to eq(user.id) + end + + it 'includes the commit SHA' do + expect(job['args'][2]).to eq(commit.oid) + end + end + end +end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index e3bb3482d67..7194c20d3bf 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -302,4 +302,21 @@ eos expect(commit.uri_type('this/path/doesnt/exist')).to be_nil end end + + describe '.from_hash' do + let(:new_commit) { described_class.from_hash(commit.to_hash, project) } + + it 'returns a Commit' do + expect(new_commit).to be_an_instance_of(described_class) + end + + it 'wraps a Gitlab::Git::Commit' do + expect(new_commit.raw).to be_an_instance_of(Gitlab::Git::Commit) + end + + it 'stores the correct commit fields' do + expect(new_commit.id).to eq(commit.id) + expect(new_commit.message).to eq(commit.message) + end + end end diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb index 5c90fd9bad9..f5e0fdcda2d 100644 --- a/spec/requests/projects/cycle_analytics_events_spec.rb +++ b/spec/requests/projects/cycle_analytics_events_spec.rb @@ -135,6 +135,6 @@ describe 'cycle analytics events' do merge_merge_requests_closing_issue(issue) - ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.sha) + ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash) end end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 9d7702f5c96..e7624e70725 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -263,7 +263,7 @@ describe GitPushService, services: true do author_email: commit_author.email ) - allow_any_instance_of(ProcessCommitWorker).to receive(:find_commit). + allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit). and_return(commit) allow(project.repository).to receive(:commits_between).and_return([commit]) @@ -321,7 +321,7 @@ describe GitPushService, services: true do committed_date: commit_time ) - allow_any_instance_of(ProcessCommitWorker).to receive(:find_commit). + allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit). and_return(commit) allow(project.repository).to receive(:commits_between).and_return([commit]) @@ -360,7 +360,7 @@ describe GitPushService, services: true do allow(project.repository).to receive(:commits_between). and_return([closing_commit]) - allow_any_instance_of(ProcessCommitWorker).to receive(:find_commit). + allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit). and_return(closing_commit) project.team << [commit_author, :master] diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ef33c473d38..6ee3307512d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -55,8 +55,12 @@ RSpec.configure do |config| config.around(:each, :redis) do |example| Gitlab::Redis.with(&:flushall) + Sidekiq.redis(&:flushall) + example.run + Gitlab::Redis.with(&:flushall) + Sidekiq.redis(&:flushall) end end diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb index 3e4fee42240..75c7fc1efd2 100644 --- a/spec/workers/process_commit_worker_spec.rb +++ b/spec/workers/process_commit_worker_spec.rb @@ -11,31 +11,25 @@ describe ProcessCommitWorker do it 'does not process the commit when the project does not exist' do expect(worker).not_to receive(:close_issues) - worker.perform(-1, user.id, commit.id) + worker.perform(-1, user.id, commit.to_hash) end it 'does not process the commit when the user does not exist' do expect(worker).not_to receive(:close_issues) - worker.perform(project.id, -1, commit.id) - end - - it 'does not process the commit when the commit no longer exists' do - expect(worker).not_to receive(:close_issues) - - worker.perform(project.id, user.id, 'this-should-does-not-exist') + worker.perform(project.id, -1, commit.to_hash) end it 'processes the commit message' do expect(worker).to receive(:process_commit_message).and_call_original - worker.perform(project.id, user.id, commit.id) + worker.perform(project.id, user.id, commit.to_hash) end it 'updates the issue metrics' do expect(worker).to receive(:update_issue_metrics).and_call_original - worker.perform(project.id, user.id, commit.id) + worker.perform(project.id, user.id, commit.to_hash) end end @@ -106,4 +100,19 @@ describe ProcessCommitWorker do expect(metric.first_mentioned_in_commit_at).to eq(commit.committed_date) end end + + describe '#build_commit' do + it 'returns a Commit' do + commit = worker.build_commit(project, id: '123') + + expect(commit).to be_an_instance_of(Commit) + end + + it 'parses date strings into Time instances' do + commit = worker. + build_commit(project, id: '123', authored_date: Time.now.to_s) + + expect(commit.authored_date).to be_an_instance_of(Time) + end + end end -- cgit v1.2.1 From c345edcb7fe7f1daf3687bf7835ef82b54a48c7f Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Thu, 1 Dec 2016 14:24:12 +0100 Subject: Document the need to use a PAT with Registry when 2FA is on GitLab 8.12 introduced a new permissions model which tightened the security of Container Registry. It is now required to use a personal token if 2FA is enabled. [ci skip] --- doc/ci/docker/using_docker_build.md | 32 ++++++++++++++++------ doc/user/project/container_registry.md | 16 ++++++----- doc/user/project/new_ci_build_permissions_model.md | 16 +++++++---- 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index 89088cf9b83..28141cced3b 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -270,12 +270,16 @@ which can be avoided if a different driver is used, for example `overlay`. ## Using the GitLab Container Registry -> **Note:** -This feature requires GitLab 8.8 and GitLab Runner 1.2. - -Once you've built a Docker image, you can push it up to the built-in [GitLab Container Registry](../../user/project/container_registry.md). For example, if you're using -docker-in-docker on your runners, this is how your `.gitlab-ci.yml` could look: +> **Notes:** +- This feature requires GitLab 8.8 and GitLab Runner 1.2. +- Starting from GitLab 8.12, if you have 2FA enabled in your account, you need + to pass a personal access token instead of your password in order to login to + GitLab's Container Registry. +Once you've built a Docker image, you can push it up to the built-in +[GitLab Container Registry](../../user/project/container_registry.md). For example, +if you're using docker-in-docker on your runners, this is how your `.gitlab-ci.yml` +could look like: ```yaml build: @@ -354,10 +358,20 @@ deploy: ``` Some things you should be aware of when using the Container Registry: -* You must log in to the container registry before running commands. Putting this in `before_script` will run it before each build job. -* Using `docker build --pull` makes sure that Docker fetches any changes to base images before building just in case your cache is stale. It takes slightly longer, but means you don’t get stuck without security patches to base images. -* Doing an explicit `docker pull` before each `docker run` makes sure to fetch the latest image that was just built. This is especially important if you are using multiple runners that cache images locally. Using the git SHA in your image tag makes this less necessary since each build will be unique and you shouldn't ever have a stale image, but it's still possible if you re-build a given commit after a dependency has changed. -* You don't want to build directly to `latest` in case there are multiple builds happening simultaneously. + +- You must log in to the container registry before running commands. Putting + this in `before_script` will run it before each build job. +- Using `docker build --pull` makes sure that Docker fetches any changes to base + images before building just in case your cache is stale. It takes slightly + longer, but means you don’t get stuck without security patches to base images. +- Doing an explicit `docker pull` before each `docker run` makes sure to fetch + the latest image that was just built. This is especially important if you are + using multiple runners that cache images locally. Using the git SHA in your + image tag makes this less necessary since each build will be unique and you + shouldn't ever have a stale image, but it's still possible if you re-build a + given commit after a dependency has changed. +- You don't want to build directly to `latest` in case there are multiple builds + happening simultaneously. [docker-in-docker]: https://blog.docker.com/2013/09/docker-can-now-run-within-docker/ [docker-cap]: https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md index b205fea2c40..47a4a3f85d0 100644 --- a/doc/user/project/container_registry.md +++ b/doc/user/project/container_registry.md @@ -4,13 +4,15 @@ --- -> **Note** -Docker Registry manifest `v1` support was added in GitLab 8.9 to support Docker -versions earlier than 1.10. -> -This document is about the user guide. To learn how to enable GitLab Container -Registry across your GitLab instance, visit the -[administrator documentation](../../administration/container_registry.md). +>**Notes:** +- Docker Registry manifest `v1` support was added in GitLab 8.9 to support Docker + versions earlier than 1.10. +- This document is about the user guide. To learn how to enable GitLab Container + Registry across your GitLab instance, visit the + [administrator documentation](../../administration/container_registry.md). +- Starting from GitLab 8.12, if you have 2FA enabled in your account, you need + to pass a personal access token instead of your password in order to login to + GitLab's Container Registry. With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md index 4f12acb8398..320faff65c5 100644 --- a/doc/user/project/new_ci_build_permissions_model.md +++ b/doc/user/project/new_ci_build_permissions_model.md @@ -187,11 +187,17 @@ To properly configure submodules with GitLab CI, read the With the update permission model we also extended the support for accessing Container Registries for private projects. -> **Note:** -As GitLab Runner 1.6 doesn't yet incorporate the introduced changes for -permissions, this makes the `image:` directive to not work with private projects -automatically. The manual configuration by an Administrator is required to use -private images. We plan to remove that limitation in one of the upcoming releases. +> **Notes:** +- GitLab Runner versions prior to 1.8 don't incorporate the introduced changes + for permissions. This makes the `image:` directive to not work with private + projects automatically and it needs to be configured manually on Runner's host + with a predefined account (for example administrator's personal account with + access token created explicitly for this purpose). This issue is resolved with + latest changes in GitLab Runner 1.8 which receives GitLab credentials with + build data. +- Starting with GitLab 8.12, if you have 2FA enabled in your account, you need + to pass a personal access token instead of your password in order to login to + GitLab's Container Registry. Your builds can access all container images that you would normally have access to. The only implication is that you can push to the Container Registry of the -- cgit v1.2.1 From c4244ba4c25e51b8d05009597e98ba8379f9cb9e Mon Sep 17 00:00:00 2001 From: winniehell <git@winniehell.de> Date: Tue, 22 Nov 2016 23:46:58 +0100 Subject: Replace static fixture for notes_spec (!7683) --- changelogs/unreleased/comments-fixture.yml | 4 ++++ spec/javascripts/fixtures/comments.html.haml | 21 --------------------- spec/javascripts/fixtures/issue_note.html.haml | 12 ------------ spec/javascripts/fixtures/issues.rb | 7 ++++++- spec/javascripts/notes_spec.js | 21 +++++++++++---------- 5 files changed, 21 insertions(+), 44 deletions(-) create mode 100644 changelogs/unreleased/comments-fixture.yml delete mode 100644 spec/javascripts/fixtures/comments.html.haml delete mode 100644 spec/javascripts/fixtures/issue_note.html.haml diff --git a/changelogs/unreleased/comments-fixture.yml b/changelogs/unreleased/comments-fixture.yml new file mode 100644 index 00000000000..824c1c88a60 --- /dev/null +++ b/changelogs/unreleased/comments-fixture.yml @@ -0,0 +1,4 @@ +--- +title: Replace static fixture for notes_spec +merge_request: 7683 +author: winniehell diff --git a/spec/javascripts/fixtures/comments.html.haml b/spec/javascripts/fixtures/comments.html.haml deleted file mode 100644 index cc1f8f15c21..00000000000 --- a/spec/javascripts/fixtures/comments.html.haml +++ /dev/null @@ -1,21 +0,0 @@ -.flash-container.timeline-content -.timeline-icon.hidden-xs.hidden-sm - %a.author_link - %img -.timeline-content.timeline-content-form - %form.new-note.js-quick-submit.common-note-form.gfm-form.js-main-target-form - .md-area - .md-header - .md-write-holder - .zen-backdrop.div-dropzone-wrapper - .div-dropzone-wrapper - .div-dropzone.dz-clickable - %textarea.note-textarea.js-note-text.js-gfm-input.js-autosize.markdown-area - .note-form-actions.clearfix - %input.btn.btn-nr.btn-create.append-right-10.comment-btn.js-comment-button{ type: 'submit' } - %a.btn.btn-nr.btn-reopen.btn-comment.js-note-target-reopen - Reopen issue - %a.btn.btn-nr.btn-close.btn-comment.js-note-target-close - Close issue - %a.btn.btn-cancel.js-note-discard - Discard draft \ No newline at end of file diff --git a/spec/javascripts/fixtures/issue_note.html.haml b/spec/javascripts/fixtures/issue_note.html.haml deleted file mode 100644 index 0aecc7334fd..00000000000 --- a/spec/javascripts/fixtures/issue_note.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -%ul - %li.note - .js-task-list-container - .note-text - %ul.task-list - %li.task-list-item - %input.task-list-item-checkbox{type: 'checkbox'} - Task List Item - .note-edit-form - %form - %textarea.js-task-list-field - \- [ ] Task List Item diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb index c10784fe5ae..06f708f9e15 100644 --- a/spec/javascripts/fixtures/issues.rb +++ b/spec/javascripts/fixtures/issues.rb @@ -26,8 +26,13 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller end it 'issues/issue-with-task-list.html.raw' do |example| + issue = create(:issue, project: project, description: '- [ ] Task List Item') + render_issue(example.description, issue) + end + + it 'issues/issue_with_comment.html.raw' do |example| issue = create(:issue, project: project) - issue.update(description: '- [ ] Task List Item') + create(:note, project: project, noteable: issue, note: '- [ ] Task List Item').save render_issue(example.description, issue) end diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 51f2ae8bcbd..2db182d702b 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -6,17 +6,21 @@ (function() { window.gon || (window.gon = {}); - - window.disableButtonIfEmptyField = function() { - return null; - }; + window.gl = window.gl || {}; + gl.utils = gl.utils || {}; describe('Notes', function() { - describe('task lists', function() { - fixture.preload('issue_note.html'); + var commentsTemplate = 'issues/issue_with_comment.raw'; + fixture.preload(commentsTemplate); + beforeEach(function () { + fixture.load(commentsTemplate); + gl.utils.disableButtonIfEmptyField = _.noop; + window.project_uploads_path = 'http://test.host/uploads'; + }); + + describe('task lists', function() { beforeEach(function() { - fixture.load('issue_note.html'); $('form').on('submit', function(e) { e.preventDefault(); }); @@ -41,12 +45,9 @@ }); describe('comments', function() { - var commentsTemplate = 'comments.html'; var textarea = '.js-note-text'; - fixture.preload(commentsTemplate); beforeEach(function() { - fixture.load(commentsTemplate); this.notes = new Notes(); this.autoSizeSpy = spyOnEvent($(textarea), 'autosize:update'); -- cgit v1.2.1 From fe0de99c4e893a30029dc03df582550cf32e8b19 Mon Sep 17 00:00:00 2001 From: Stan Hu <stanhu@gmail.com> Date: Mon, 28 Nov 2016 23:38:14 -0800 Subject: Gracefully recover from Redis connection failures in Sidekiq initializer * Closes gitlab-org/gitlab-ce#25143 * Closes gitlab-org/omnibus-gitlab#1743 --- changelogs/unreleased/rescue-from-redis-init-errors.yml | 4 ++++ config/initializers/sidekiq.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/rescue-from-redis-init-errors.yml diff --git a/changelogs/unreleased/rescue-from-redis-init-errors.yml b/changelogs/unreleased/rescue-from-redis-init-errors.yml new file mode 100644 index 00000000000..c41f49597e7 --- /dev/null +++ b/changelogs/unreleased/rescue-from-redis-init-errors.yml @@ -0,0 +1,4 @@ +--- +title: Gracefully recover from Redis connection failures in Sidekiq initializer +merge_request: +author: diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index b87b31d9697..1d7a3f03ace 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -61,5 +61,5 @@ begin end end end -rescue Redis::BaseError, SocketError +rescue Redis::BaseError, SocketError, Errno::ENOENT, Errno::EAFNOSUPPORT, Errno::ECONNRESET, Errno::ECONNREFUSED end -- cgit v1.2.1 From 86f726a5e969fd69c82c94713bc25bd0f0bba598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Thu, 1 Dec 2016 16:17:57 +0100 Subject: Bump Git version requirement to 2.8.4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- README.md | 2 +- changelogs/unreleased/update-git-version-in-doc.yml | 4 ++++ doc/install/installation.md | 8 ++++---- 3 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 changelogs/unreleased/update-git-version-in-doc.yml diff --git a/README.md b/README.md index 61204630fd2..68b709b85d5 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ GitLab is a Ruby on Rails application that runs on the following software: - Ubuntu/Debian/CentOS/RHEL - Ruby (MRI) 2.3 -- Git 2.7.4+ +- Git 2.8.4+ - Redis 2.8+ - MySQL or PostgreSQL diff --git a/changelogs/unreleased/update-git-version-in-doc.yml b/changelogs/unreleased/update-git-version-in-doc.yml new file mode 100644 index 00000000000..cb3260f71cd --- /dev/null +++ b/changelogs/unreleased/update-git-version-in-doc.yml @@ -0,0 +1,4 @@ +--- +title: Bump Git version requirement to 2.8.4 +merge_request: +author: diff --git a/doc/install/installation.md b/doc/install/installation.md index 77adb4c9f7b..026e69b8ab6 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -76,7 +76,7 @@ Make sure you have the right version of Git installed # Install Git sudo apt-get install -y git-core - # Make sure Git is version 2.7.4 or higher + # Make sure Git is version 2.8.4 or higher git --version Is the system packaged Git too old? Remove it and compile from source. @@ -89,9 +89,9 @@ Is the system packaged Git too old? Remove it and compile from source. # Download and compile from source cd /tmp - curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.7.4.tar.gz - echo '7104c4f5d948a75b499a954524cb281fe30c6649d8abe20982936f75ec1f275b git-2.7.4.tar.gz' | shasum -a256 -c - && tar -xzf git-2.7.4.tar.gz - cd git-2.7.4/ + curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.8.4.tar.gz + echo '626e319f8a24fc0866167ea5f6bf3e2f38f69d6cb2e59e150f13709ca3ebf301 git-2.8.4.tar.gz' | shasum -a256 -c - && tar -xzf git-2.8.4.tar.gz + cd git-2.8.4/ ./configure make prefix=/usr/local all -- cgit v1.2.1 From d757247247ea6015d560eacd29ec7be564e332bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Wed, 30 Nov 2016 15:48:19 +0100 Subject: Allow public access to some Project API endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- changelogs/unreleased/4269-public-api.yml | 4 + lib/api/helpers.rb | 5 + lib/api/projects.rb | 28 ++- spec/requests/api/api_helpers_spec.rb | 54 ++++- spec/requests/api/projects_spec.rb | 376 +++++++++++++++++++----------- 5 files changed, 315 insertions(+), 152 deletions(-) create mode 100644 changelogs/unreleased/4269-public-api.yml diff --git a/changelogs/unreleased/4269-public-api.yml b/changelogs/unreleased/4269-public-api.yml new file mode 100644 index 00000000000..560bc6a4f13 --- /dev/null +++ b/changelogs/unreleased/4269-public-api.yml @@ -0,0 +1,4 @@ +--- +title: Allow public access to some Project API endpoints +merge_request: 7843 +author: diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index cbafa952ef6..7f94ede7940 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -141,6 +141,10 @@ module API unauthorized! unless current_user end + def authenticate_non_get! + authenticate! unless %w[GET HEAD].include?(route.route_method) + end + def authenticate_by_gitlab_shell_token! input = params['secret_token'].try(:chomp) unless Devise.secure_compare(secret_token, input) @@ -149,6 +153,7 @@ module API end def authenticated_as_admin! + authenticate! forbidden! unless current_user.is_admin? end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 8975b1a751c..2929d2157dc 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -3,7 +3,7 @@ module API class Projects < Grape::API include PaginationParams - before { authenticate! } + before { authenticate_non_get! } helpers do params :optional_params do @@ -61,7 +61,7 @@ module API end end - desc 'Get a projects list for authenticated user' do + desc 'Get a list of visible projects for authenticated user' do success Entities::BasicProjectDetails end params do @@ -70,15 +70,15 @@ module API use :filter_params use :pagination end - get do - projects = current_user.authorized_projects + get '/visible' do + projects = ProjectsFinder.new.execute(current_user) projects = filter_projects(projects) - entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess + entity = params[:simple] || !current_user ? Entities::BasicProjectDetails : Entities::ProjectWithAccess present paginate(projects), with: entity, user: current_user end - desc 'Get a list of visible projects for authenticated user' do + desc 'Get a projects list for authenticated user' do success Entities::BasicProjectDetails end params do @@ -87,8 +87,10 @@ module API use :filter_params use :pagination end - get '/visible' do - projects = ProjectsFinder.new.execute(current_user) + get do + authenticate! + + projects = current_user.authorized_projects projects = filter_projects(projects) entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess @@ -103,6 +105,8 @@ module API use :pagination end get '/owned' do + authenticate! + projects = current_user.owned_projects projects = filter_projects(projects) @@ -117,6 +121,8 @@ module API use :pagination end get '/starred' do + authenticate! + projects = current_user.viewable_starred_projects projects = filter_projects(projects) @@ -132,6 +138,7 @@ module API end get '/all' do authenticated_as_admin! + projects = Project.all projects = filter_projects(projects) @@ -213,7 +220,8 @@ module API success Entities::ProjectWithAccess end get ":id" do - present user_project, with: Entities::ProjectWithAccess, user: current_user, + entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails + present user_project, with: entity, user: current_user, user_can_admin_project: can?(current_user, :admin_project, user_project) end @@ -433,7 +441,7 @@ module API use :pagination end get ':id/users' do - users = User.where(id: user_project.team.users.map(&:id)) + users = user_project.team.users users = users.search(params[:search]) if params[:search].present? present paginate(users), with: Entities::UserBasic diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb index 01bb9e955e0..36517ad0f8c 100644 --- a/spec/requests/api/api_helpers_spec.rb +++ b/spec/requests/api/api_helpers_spec.rb @@ -47,7 +47,7 @@ describe API::Helpers, api: true do end def error!(message, status) - raise Exception + raise Exception.new("#{status} - #{message}") end describe ".current_user" do @@ -290,4 +290,56 @@ describe API::Helpers, api: true do handle_api_exception(exception) end end + + describe '.authenticate_non_get!' do + %w[HEAD GET].each do |method_name| + context "method is #{method_name}" do + before do + expect_any_instance_of(self.class).to receive(:route).and_return(double(route_method: method_name)) + end + + it 'does not raise an error' do + expect_any_instance_of(self.class).not_to receive(:authenticate!) + + expect { authenticate_non_get! }.not_to raise_error + end + end + end + + %w[POST PUT PATCH DELETE].each do |method_name| + context "method is #{method_name}" do + before do + expect_any_instance_of(self.class).to receive(:route).and_return(double(route_method: method_name)) + end + + it 'calls authenticate!' do + expect_any_instance_of(self.class).to receive(:authenticate!) + + authenticate_non_get! + end + end + end + end + + describe '.authenticate!' do + context 'current_user is nil' do + before do + expect_any_instance_of(self.class).to receive(:current_user).and_return(nil) + end + + it 'returns a 401 response' do + expect { authenticate! }.to raise_error '401 - {"message"=>"401 Unauthorized"}' + end + end + + context 'current_user is present' do + before do + expect_any_instance_of(self.class).to receive(:current_user).and_return(true) + end + + it 'does not raise an error' do + expect { authenticate! }.not_to raise_error + end + end + end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 482e81b29a6..5b3427e66e8 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -200,32 +200,43 @@ describe API::API, api: true do end describe 'GET /projects/visible' do - let(:public_project) { create(:project, :public) } + shared_examples_for 'visible projects response' do + it 'returns the visible projects' do + get api('/projects/visible', current_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id)) + end + end + let!(:public_project) { create(:project, :public) } before do - public_project project project2 project3 project4 end - it 'returns the projects viewable by the user' do - get api('/projects/visible', user) - - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.map { |project| project['id'] }). - to contain_exactly(public_project.id, project.id, project2.id, project3.id) + context 'when unauthenticated' do + it_behaves_like 'visible projects response' do + let(:current_user) { nil } + let(:projects) { [public_project] } + end end - it 'shows only public projects when the user only has access to those' do - get api('/projects/visible', user2) + context 'when authenticated' do + it_behaves_like 'visible projects response' do + let(:current_user) { user } + let(:projects) { [public_project, project, project2, project3] } + end + end - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.map { |project| project['id'] }). - to contain_exactly(public_project.id) + context 'when authenticated as a different user' do + it_behaves_like 'visible projects response' do + let(:current_user) { user2 } + let(:projects) { [public_project] } + end end end @@ -528,135 +539,150 @@ describe API::API, api: true do end describe 'GET /projects/:id' do - before { project } - before { project_member } - - it 'returns a project by id' do - group = create(:group) - link = create(:project_group_link, project: project, group: group) + context 'when unauthenticated' do + it 'returns the public projects' do + public_project = create(:project, :public) - get api("/projects/#{project.id}", user) + get api("/projects/#{public_project.id}") - expect(response).to have_http_status(200) - expect(json_response['id']).to eq(project.id) - expect(json_response['description']).to eq(project.description) - expect(json_response['default_branch']).to eq(project.default_branch) - expect(json_response['tag_list']).to be_an Array - expect(json_response['public']).to be_falsey - expect(json_response['archived']).to be_falsey - expect(json_response['visibility_level']).to be_present - expect(json_response['ssh_url_to_repo']).to be_present - expect(json_response['http_url_to_repo']).to be_present - expect(json_response['web_url']).to be_present - expect(json_response['owner']).to be_a Hash - expect(json_response['owner']).to be_a Hash - expect(json_response['name']).to eq(project.name) - expect(json_response['path']).to be_present - expect(json_response['issues_enabled']).to be_present - expect(json_response['merge_requests_enabled']).to be_present - expect(json_response['wiki_enabled']).to be_present - expect(json_response['builds_enabled']).to be_present - expect(json_response['snippets_enabled']).to be_present - expect(json_response['container_registry_enabled']).to be_present - expect(json_response['created_at']).to be_present - expect(json_response['last_activity_at']).to be_present - expect(json_response['shared_runners_enabled']).to be_present - expect(json_response['creator_id']).to be_present - expect(json_response['namespace']).to be_present - expect(json_response['avatar_url']).to be_nil - expect(json_response['star_count']).to be_present - expect(json_response['forks_count']).to be_present - expect(json_response['public_builds']).to be_present - expect(json_response['shared_with_groups']).to be_an Array - expect(json_response['shared_with_groups'].length).to eq(1) - expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id) - expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name) - expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access) - expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_build_succeeds) - expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved) - end - - it 'returns a project by path name' do - get api("/projects/#{project.id}", user) - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(project.name) + expect(response).to have_http_status(200) + expect(json_response['id']).to eq(public_project.id) + expect(json_response['description']).to eq(public_project.description) + expect(json_response.keys).not_to include('permissions') + end end - it 'returns a 404 error if not found' do - get api('/projects/42', user) - expect(response).to have_http_status(404) - expect(json_response['message']).to eq('404 Project Not Found') - end + context 'when authenticated' do + before do + project + project_member + end - it 'returns a 404 error if user is not a member' do - other_user = create(:user) - get api("/projects/#{project.id}", other_user) - expect(response).to have_http_status(404) - end + it 'returns a project by id' do + group = create(:group) + link = create(:project_group_link, project: project, group: group) - it 'handles users with dots' do - dot_user = create(:user, username: 'dot.user') - project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace) + get api("/projects/#{project.id}", user) - get api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user) - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(project.name) - end + expect(response).to have_http_status(200) + expect(json_response['id']).to eq(project.id) + expect(json_response['description']).to eq(project.description) + expect(json_response['default_branch']).to eq(project.default_branch) + expect(json_response['tag_list']).to be_an Array + expect(json_response['public']).to be_falsey + expect(json_response['archived']).to be_falsey + expect(json_response['visibility_level']).to be_present + expect(json_response['ssh_url_to_repo']).to be_present + expect(json_response['http_url_to_repo']).to be_present + expect(json_response['web_url']).to be_present + expect(json_response['owner']).to be_a Hash + expect(json_response['owner']).to be_a Hash + expect(json_response['name']).to eq(project.name) + expect(json_response['path']).to be_present + expect(json_response['issues_enabled']).to be_present + expect(json_response['merge_requests_enabled']).to be_present + expect(json_response['wiki_enabled']).to be_present + expect(json_response['builds_enabled']).to be_present + expect(json_response['snippets_enabled']).to be_present + expect(json_response['container_registry_enabled']).to be_present + expect(json_response['created_at']).to be_present + expect(json_response['last_activity_at']).to be_present + expect(json_response['shared_runners_enabled']).to be_present + expect(json_response['creator_id']).to be_present + expect(json_response['namespace']).to be_present + expect(json_response['avatar_url']).to be_nil + expect(json_response['star_count']).to be_present + expect(json_response['forks_count']).to be_present + expect(json_response['public_builds']).to be_present + expect(json_response['shared_with_groups']).to be_an Array + expect(json_response['shared_with_groups'].length).to eq(1) + expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id) + expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name) + expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access) + expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_build_succeeds) + expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved) + end + + it 'returns a project by path name' do + get api("/projects/#{project.id}", user) + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(project.name) + end - describe 'permissions' do - context 'all projects' do - before { project.team << [user, :master] } + it 'returns a 404 error if not found' do + get api('/projects/42', user) + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Project Not Found') + end - it 'contains permission information' do - get api("/projects", user) + it 'returns a 404 error if user is not a member' do + other_user = create(:user) + get api("/projects/#{project.id}", other_user) + expect(response).to have_http_status(404) + end - expect(response).to have_http_status(200) - expect(json_response.first['permissions']['project_access']['access_level']). - to eq(Gitlab::Access::MASTER) - expect(json_response.first['permissions']['group_access']).to be_nil - end + it 'handles users with dots' do + dot_user = create(:user, username: 'dot.user') + project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace) + + get api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user) + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(project.name) end - context 'personal project' do - it 'sets project access and returns 200' do - project.team << [user, :master] - get api("/projects/#{project.id}", user) + describe 'permissions' do + context 'all projects' do + before { project.team << [user, :master] } - expect(response).to have_http_status(200) - expect(json_response['permissions']['project_access']['access_level']). + it 'contains permission information' do + get api("/projects", user) + + expect(response).to have_http_status(200) + expect(json_response.first['permissions']['project_access']['access_level']). to eq(Gitlab::Access::MASTER) - expect(json_response['permissions']['group_access']).to be_nil + expect(json_response.first['permissions']['group_access']).to be_nil + end end - end - context 'group project' do - let(:project2) { create(:project, group: create(:group)) } + context 'personal project' do + it 'sets project access and returns 200' do + project.team << [user, :master] + get api("/projects/#{project.id}", user) - before { project2.group.add_owner(user) } + expect(response).to have_http_status(200) + expect(json_response['permissions']['project_access']['access_level']). + to eq(Gitlab::Access::MASTER) + expect(json_response['permissions']['group_access']).to be_nil + end + end - it 'sets the owner and return 200' do - get api("/projects/#{project2.id}", user) + context 'group project' do + let(:project2) { create(:project, group: create(:group)) } - expect(response).to have_http_status(200) - expect(json_response['permissions']['project_access']).to be_nil - expect(json_response['permissions']['group_access']['access_level']). + before { project2.group.add_owner(user) } + + it 'sets the owner and return 200' do + get api("/projects/#{project2.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['permissions']['project_access']).to be_nil + expect(json_response['permissions']['group_access']['access_level']). to eq(Gitlab::Access::OWNER) + end end end end end describe 'GET /projects/:id/events' do - before { project_member2 } - - context 'valid request' do - before do + shared_examples_for 'project events response' do + it 'returns the project events' do + member = create(:user) + create(:project_member, :developer, user: member, project: project) note = create(:note_on_issue, note: 'What an awesome day!', project: project) EventCreateService.new.leave_note(note, note.author) - end - it 'returns all events' do - get api("/projects/#{project.id}/events", user) + get api("/projects/#{project.id}/events", current_user) expect(response).to have_http_status(200) @@ -669,24 +695,90 @@ describe API::API, api: true do expect(last_event['action_name']).to eq('joined') expect(last_event['project_id'].to_i).to eq(project.id) - expect(last_event['author_username']).to eq(user3.username) - expect(last_event['author']['name']).to eq(user3.name) + expect(last_event['author_username']).to eq(member.username) + expect(last_event['author']['name']).to eq(member.name) end end - it 'returns a 404 error if not found' do - get api('/projects/42/events', user) + context 'when unauthenticated' do + it_behaves_like 'project events response' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end + end - expect(response).to have_http_status(404) - expect(json_response['message']).to eq('404 Project Not Found') + context 'when authenticated' do + context 'valid request' do + it_behaves_like 'project events response' do + let(:current_user) { user } + end + end + + it 'returns a 404 error if not found' do + get api('/projects/42/events', user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Project Not Found') + end + + it 'returns a 404 error if user is not a member' do + other_user = create(:user) + + get api("/projects/#{project.id}/events", other_user) + + expect(response).to have_http_status(404) + end end + end - it 'returns a 404 error if user is not a member' do - other_user = create(:user) + describe 'GET /projects/:id/users' do + shared_examples_for 'project users response' do + it 'returns the project users' do + member = create(:user) + create(:project_member, :developer, user: member, project: project) - get api("/projects/#{project.id}/events", other_user) + get api("/projects/#{project.id}/users", current_user) - expect(response).to have_http_status(404) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + + first_user = json_response.first + + expect(first_user['username']).to eq(member.username) + expect(first_user['name']).to eq(member.name) + expect(first_user.keys).to contain_exactly(*%w[name username id state avatar_url web_url]) + end + end + + context 'when unauthenticated' do + it_behaves_like 'project users response' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end + end + + context 'when authenticated' do + context 'valid request' do + it_behaves_like 'project users response' do + let(:current_user) { user } + end + end + + it 'returns a 404 error if not found' do + get api('/projects/42/users', user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Project Not Found') + end + + it 'returns a 404 error if user is not a member' do + other_user = create(:user) + + get api("/projects/#{project.id}/users", other_user) + + expect(response).to have_http_status(404) + end end end @@ -950,35 +1042,37 @@ describe API::API, api: true do let!(:public) { create(:empty_project, :public, name: "public #{query}") } let!(:unfound_public) { create(:empty_project, :public, name: 'unfound public') } + shared_examples_for 'project search response' do |args = {}| + it 'returns project search responses' do + get api("/projects/search/#{query}", current_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(args[:results]) + json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*query.*/) } + end + end + context 'when unauthenticated' do - it 'returns authentication error' do - get api("/projects/search/#{query}") - expect(response).to have_http_status(401) + it_behaves_like 'project search response', results: 1 do + let(:current_user) { nil } end end context 'when authenticated' do - it 'returns an array of projects' do - get api("/projects/search/#{query}", user) - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(6) - json_response.each {|project| expect(project['name']).to match(/.*query.*/)} + it_behaves_like 'project search response', results: 6 do + let(:current_user) { user } end end context 'when authenticated as a different user' do - it 'returns matching public projects' do - get api("/projects/search/#{query}", user2) - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(2) - json_response.each {|project| expect(project['name']).to match(/(internal|public) query/)} + it_behaves_like 'project search response', results: 2, match_regex: /(internal|public) query/ do + let(:current_user) { user2 } end end end - describe 'PUT /projects/:id̈́' do + describe 'PUT /projects/:id' do before { project } before { user } before { user3 } -- cgit v1.2.1 From 633538151b99c658bcbb2173e91eb5deba4408f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Thu, 1 Dec 2016 12:07:52 +0100 Subject: Fix URL rewritting in the Help section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- app/controllers/help_controller.rb | 6 ++++-- .../25199-fix-broken-urls-in-help-page.yml | 4 ++++ spec/controllers/help_controller_spec.rb | 24 ++++++++++++++-------- spec/features/help_pages_spec.rb | 13 ++++++++++-- 4 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 changelogs/unreleased/25199-fix-broken-urls-in-help-page.yml diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index a10cdcce72b..37feff79999 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -7,8 +7,10 @@ class HelpController < ApplicationController @help_index = File.read(Rails.root.join('doc', 'README.md')) # Prefix Markdown links with `help/` unless they are external links - # See http://rubular.com/r/MioSrVLK3S - @help_index.gsub!(%r{(\]\()(?!.+://)([^\)\(]+\))}, '\1/help/\2') + # See http://rubular.com/r/X3baHTbPO2 + @help_index.gsub!(%r{(?<delim>\]\()(?!.+://)(?!/)(?<link>[^\)\(]+\))}) do + "#{$~[:delim]}#{Gitlab.config.gitlab.relative_url_root}/help/#{$~[:link]}" + end end def show diff --git a/changelogs/unreleased/25199-fix-broken-urls-in-help-page.yml b/changelogs/unreleased/25199-fix-broken-urls-in-help-page.yml new file mode 100644 index 00000000000..58efd9113f2 --- /dev/null +++ b/changelogs/unreleased/25199-fix-broken-urls-in-help-page.yml @@ -0,0 +1,4 @@ +--- +title: Don't change relative URLs to absolute URLs in the Help page +merge_request: +author: diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb index cffed987f6b..d3489324a9c 100644 --- a/spec/controllers/help_controller_spec.rb +++ b/spec/controllers/help_controller_spec.rb @@ -8,26 +8,32 @@ describe HelpController do end describe 'GET #index' do - context 'when url prefixed without /help/' do - it 'has correct url prefix' do - stub_readme("[API](api/README.md)") + context 'with absolute url' do + it 'keeps the URL absolute' do + stub_readme("[API](/api/README.md)") + get :index - expect(assigns[:help_index]).to eq '[API](/help/api/README.md)' + + expect(assigns[:help_index]).to eq '[API](/api/README.md)' end end - context 'when url prefixed with help' do - it 'will be an absolute path' do - stub_readme("[API](helpful_hints/README.md)") + context 'with relative url' do + it 'prefixes it with /help/' do + stub_readme("[API](api/README.md)") + get :index - expect(assigns[:help_index]).to eq '[API](/help/helpful_hints/README.md)' + + expect(assigns[:help_index]).to eq '[API](/help/api/README.md)' end end context 'when url is an external link' do - it 'will not be changed' do + it 'does not change it' do stub_readme("[external](https://some.external.link)") + get :index + expect(assigns[:help_index]).to eq '[external](https://some.external.link)' end end diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb index 73d03837144..4319d6db0d2 100644 --- a/spec/features/help_pages_spec.rb +++ b/spec/features/help_pages_spec.rb @@ -12,9 +12,9 @@ describe 'Help Pages', feature: true do end describe 'Get the main help page' do - shared_examples_for 'help page' do + shared_examples_for 'help page' do |prefix: ''| it 'prefixes links correctly' do - expect(page).to have_selector('div.documentation-index > ul a[href="/help/api/README.md"]') + expect(page).to have_selector(%(div.documentation-index > ul a[href="#{prefix}/help/api/README.md"])) end end @@ -33,5 +33,14 @@ describe 'Help Pages', feature: true do it_behaves_like 'help page' end + + context 'with a relative installation' do + before do + stub_config_setting(relative_url_root: '/gitlab') + visit help_path + end + + it_behaves_like 'help page', prefix: '/gitlab' + end end end -- cgit v1.2.1 From 6bfe683d427daafbee3cbc73324e42b06e9f1fec Mon Sep 17 00:00:00 2001 From: Ryan Harris <harrisryan1@gmail.com> Date: Thu, 1 Dec 2016 12:46:54 -0500 Subject: Fixed row margins on Pipeline Settings page in order to unify styles --- app/views/projects/pipelines_settings/_badge.html.haml | 2 +- changelogs/unreleased/25098-header-margins-on-pipeline-settings.yml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/25098-header-margins-on-pipeline-settings.yml diff --git a/app/views/projects/pipelines_settings/_badge.html.haml b/app/views/projects/pipelines_settings/_badge.html.haml index 7b7fa56d993..22a3b884520 100644 --- a/app/views/projects/pipelines_settings/_badge.html.haml +++ b/app/views/projects/pipelines_settings/_badge.html.haml @@ -1,4 +1,4 @@ -.row{ class: badge.title.gsub(' ', '-') } +%div{ class: badge.title.gsub(' ', '-') } .col-lg-3.profile-settings-sidebar %h4.prepend-top-0 = badge.title.capitalize diff --git a/changelogs/unreleased/25098-header-margins-on-pipeline-settings.yml b/changelogs/unreleased/25098-header-margins-on-pipeline-settings.yml new file mode 100644 index 00000000000..1799fad1631 --- /dev/null +++ b/changelogs/unreleased/25098-header-margins-on-pipeline-settings.yml @@ -0,0 +1,5 @@ +--- +title: Adjusted margins for Build Status and Coverage Report rows to match those of + the CI/CD Pipeline row +merge_request: +author: Ryan Harris -- cgit v1.2.1 From fde754e2676e40dcf2600190983ef54030c5d5a5 Mon Sep 17 00:00:00 2001 From: Guyzmo <guyzmo+gitlab+pub@m0g.net> Date: Sat, 26 Nov 2016 16:37:26 +0100 Subject: API: Endpoint to expose personal snippets as /snippets Adding the necessary API for the new /snippets Restful resource added with this commit. Added a new Grape class `Snippets`, as well as a `PersonalSnippet` entity. Issue: #20042 Merge-Request: !6373 Signed-off-by: Guyzmo <guyzmo+gitlab+pub@m0g.net> --- app/finders/snippets_finder.rb | 5 +- app/helpers/gitlab_routing_helper.rb | 5 + app/policies/personal_snippet_policy.rb | 5 + changelogs/unreleased/features-api-snippets.yml | 4 + doc/api/snippets.md | 232 ++++++++++++++++++++++++ lib/api/api.rb | 1 + lib/api/entities.rb | 13 ++ lib/api/snippets.rb | 137 ++++++++++++++ lib/gitlab/url_builder.rb | 2 + spec/finders/snippets_finder_spec.rb | 59 +++--- spec/requests/api/snippets_spec.rb | 157 ++++++++++++++++ 11 files changed, 594 insertions(+), 26 deletions(-) create mode 100644 changelogs/unreleased/features-api-snippets.yml create mode 100644 doc/api/snippets.md create mode 100644 lib/api/snippets.rb create mode 100644 spec/requests/api/snippets_spec.rb diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index 00ff1611039..0586a923a74 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -1,12 +1,15 @@ class SnippetsFinder def execute(current_user, params = {}) filter = params[:filter] + user = params.fetch(:user, current_user) case filter when :all then snippets(current_user).fresh + when :public then + Snippet.are_public.fresh when :by_user then - by_user(current_user, params[:user], params[:scope]) + by_user(current_user, user, params[:scope]) when :by_project by_project(current_user, params[:project]) end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index af9087d8326..99db73c9ee0 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -159,6 +159,11 @@ module GitlabRoutingHelper resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) end + # Snippets + def personal_snippet_url(snippet, *args) + snippet_url(snippet) + end + # Groups ## Members diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb index 46c5aa1a5be..d3913986cd8 100644 --- a/app/policies/personal_snippet_policy.rb +++ b/app/policies/personal_snippet_policy.rb @@ -6,9 +6,14 @@ class PersonalSnippetPolicy < BasePolicy if @subject.author == @user can! :read_personal_snippet can! :update_personal_snippet + can! :destroy_personal_snippet can! :admin_personal_snippet end + unless @user.external? + can! :create_personal_snippet + end + if @subject.internal? && !@user.external? can! :read_personal_snippet end diff --git a/changelogs/unreleased/features-api-snippets.yml b/changelogs/unreleased/features-api-snippets.yml new file mode 100644 index 00000000000..80c7bb75359 --- /dev/null +++ b/changelogs/unreleased/features-api-snippets.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Endpoint to expose personal snippets as /snippets' +merge_request: 6373 +author: Bernard Guyzmo Pratz diff --git a/doc/api/snippets.md b/doc/api/snippets.md new file mode 100644 index 00000000000..5a5dc162ffe --- /dev/null +++ b/doc/api/snippets.md @@ -0,0 +1,232 @@ +# Snippets + +> [Introduced][ce-6373] in GitLab 8.15. + +### Snippet visibility level + +Snippets in GitLab can be either private, internal, or public. +You can set it with the `visibility_level` field in the snippet. + +Constants for snippet visibility levels are: + +| Visibility | Visibility level | Description | +| ---------- | ---------------- | ----------- | +| Private | `0` | The snippet is visible only to the snippet creator | +| Internal | `10` | The snippet is visible for any logged in user | +| Public | `20` | The snippet can be accessed without any authentication | + +## List snippets + +Get a list of current user's snippets. + +``` +GET /snippets +``` + +## Single snippet + +Get a single snippet. + +``` +GET /snippets/:id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | Integer | yes | The ID of a snippet | + +``` bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/1 +``` + +Example response: + +``` json +{ + "id": 1, + "title": "test", + "file_name": "add.rb", + "author": { + "id": 1, + "username": "john_smith", + "email": "john@example.com", + "name": "John Smith", + "state": "active", + "created_at": "2012-05-23T08:00:58Z" + }, + "expires_at": null, + "updated_at": "2012-06-28T10:52:04Z", + "created_at": "2012-06-28T10:52:04Z", + "web_url": "http://example.com/snippets/1", +} +``` + +## Create new snippet + +Creates a new snippet. The user must have permission to create new snippets. + +``` +POST /snippets +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `title` | String | yes | The title of a snippet | +| `file_name` | String | yes | The name of a snippet file | +| `content` | String | yes | The content of a snippet | +| `visibility_level` | Integer | yes | The snippet's visibility | + + +``` bash +curl --request POST --data '{"title": "This is a snippet", "content": "Hello world", "file_name": "test.txt", "visibility_level": 10 }' --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets +``` + +Example response: + +``` json +{ + "id": 1, + "title": "This is a snippet", + "file_name": "test.txt", + "author": { + "id": 1, + "username": "john_smith", + "email": "john@example.com", + "name": "John Smith", + "state": "active", + "created_at": "2012-05-23T08:00:58Z" + }, + "expires_at": null, + "updated_at": "2012-06-28T10:52:04Z", + "created_at": "2012-06-28T10:52:04Z", + "web_url": "http://example.com/snippets/1", +} +``` + +## Update snippet + +Updates an existing snippet. The user must have permission to change an existing snippet. + +``` +PUT /snippets/:id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | Integer | yes | The ID of a snippet | +| `title` | String | no | The title of a snippet | +| `file_name` | String | no | The name of a snippet file | +| `content` | String | no | The content of a snippet | +| `visibility_level` | Integer | no | The snippet's visibility | + + +``` bash +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data '{"title": "foo", "content": "bar"}' https://gitlab.example.com/api/v3/snippets/1 +``` + +Example response: + +``` json +{ + "id": 1, + "title": "test", + "file_name": "add.rb", + "author": { + "id": 1, + "username": "john_smith", + "email": "john@example.com", + "name": "John Smith", + "state": "active", + "created_at": "2012-05-23T08:00:58Z" + }, + "expires_at": null, + "updated_at": "2012-06-28T10:52:04Z", + "created_at": "2012-06-28T10:52:04Z", + "web_url": "http://example.com/snippets/1", +} +``` + +## Delete snippet + +Deletes an existing snippet. + +``` +DELETE /snippets/:id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | Integer | yes | The ID of a snippet | + + +``` +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/snippets/1" +``` + +upon successful delete a `204 No content` HTTP code shall be expected, with no data, +but if the snippet is non-existent, a `404 Not Found` will be returned. + +## Explore all public snippets + +``` +GET /snippets/public +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `per_page` | Integer | no | number of snippets to return per page | +| `page` | Integer | no | the page to retrieve | + +``` bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/public?per_page=2&page=1 +``` + +Example response: + +``` json +[ + { + "author": { + "avatar_url": "http://www.gravatar.com/avatar/edaf55a9e363ea263e3b981d09e0f7f7?s=80&d=identicon", + "id": 12, + "name": "Libby Rolfson", + "state": "active", + "username": "elton_wehner", + "web_url": "http://localhost:3000/elton_wehner" + }, + "created_at": "2016-11-25T16:53:34.504Z", + "file_name": "oconnerrice.rb", + "id": 49, + "raw_url": "http://localhost:3000/snippets/49/raw", + "title": "Ratione cupiditate et laborum temporibus.", + "updated_at": "2016-11-25T16:53:34.504Z", + "web_url": "http://localhost:3000/snippets/49" + }, + { + "author": { + "avatar_url": "http://www.gravatar.com/avatar/36583b28626de71061e6e5a77972c3bd?s=80&d=identicon", + "id": 16, + "name": "Llewellyn Flatley", + "state": "active", + "username": "adaline", + "web_url": "http://localhost:3000/adaline" + }, + "created_at": "2016-11-25T16:53:34.479Z", + "file_name": "muellershields.rb", + "id": 48, + "raw_url": "http://localhost:3000/snippets/48/raw", + "title": "Minus similique nesciunt vel fugiat qui ullam sunt.", + "updated_at": "2016-11-25T16:53:34.479Z", + "web_url": "http://localhost:3000/snippets/48" + } +] +``` + diff --git a/lib/api/api.rb b/lib/api/api.rb index 67109ceeef9..cec2702e44d 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -64,6 +64,7 @@ module API mount ::API::Session mount ::API::Settings mount ::API::SidekiqMetrics + mount ::API::Snippets mount ::API::Subscriptions mount ::API::SystemHooks mount ::API::Tags diff --git a/lib/api/entities.rb b/lib/api/entities.rb index d5dfb8d00be..dc6d085925d 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -200,6 +200,19 @@ module API end end + class PersonalSnippet < Grape::Entity + expose :id, :title, :file_name + expose :author, using: Entities::UserBasic + expose :updated_at, :created_at + + expose :web_url do |snippet| + Gitlab::UrlBuilder.build(snippet) + end + expose :raw_url do |snippet| + Gitlab::UrlBuilder.build(snippet) + "/raw" + end + end + class ProjectEntity < Grape::Entity expose :id, :iid expose(:project_id) { |entity| entity.project.id } diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb new file mode 100644 index 00000000000..e096e636806 --- /dev/null +++ b/lib/api/snippets.rb @@ -0,0 +1,137 @@ +module API + # Snippets API + class Snippets < Grape::API + include PaginationParams + + before { authenticate! } + + resource :snippets do + helpers do + def snippets_for_current_user + SnippetsFinder.new.execute(current_user, filter: :by_user, user: current_user) + end + + def public_snippets + SnippetsFinder.new.execute(current_user, filter: :public) + end + end + + desc 'Get a snippets list for authenticated user' do + detail 'This feature was introduced in GitLab 8.15.' + success Entities::PersonalSnippet + end + params do + use :pagination + end + get do + present paginate(snippets_for_current_user), with: Entities::PersonalSnippet + end + + desc 'List all public snippets current_user has access to' do + detail 'This feature was introduced in GitLab 8.15.' + success Entities::PersonalSnippet + end + params do + use :pagination + end + get 'public' do + present paginate(public_snippets), with: Entities::PersonalSnippet + end + + desc 'Get a single snippet' do + detail 'This feature was introduced in GitLab 8.15.' + success Entities::PersonalSnippet + end + params do + requires :id, type: Integer, desc: 'The ID of a snippet' + end + get ':id' do + snippet = snippets_for_current_user.find(params[:id]) + present snippet, with: Entities::PersonalSnippet + end + + desc 'Create new snippet' do + detail 'This feature was introduced in GitLab 8.15.' + success Entities::PersonalSnippet + end + params do + requires :title, type: String, desc: 'The title of a snippet' + requires :file_name, type: String, desc: 'The name of a snippet file' + requires :content, type: String, desc: 'The content of a snippet' + optional :visibility_level, type: Integer, + values: Gitlab::VisibilityLevel.values, + default: Gitlab::VisibilityLevel::INTERNAL, + desc: 'The visibility level of the snippet' + end + post do + attrs = declared_params(include_missing: false) + snippet = CreateSnippetService.new(nil, current_user, attrs).execute + + if snippet.persisted? + present snippet, with: Entities::PersonalSnippet + else + render_validation_error!(snippet) + end + end + + desc 'Update an existing snippet' do + detail 'This feature was introduced in GitLab 8.15.' + success Entities::PersonalSnippet + end + params do + requires :id, type: Integer, desc: 'The ID of a snippet' + optional :title, type: String, desc: 'The title of a snippet' + optional :file_name, type: String, desc: 'The name of a snippet file' + optional :content, type: String, desc: 'The content of a snippet' + optional :visibility_level, type: Integer, + values: Gitlab::VisibilityLevel.values, + desc: 'The visibility level of the snippet' + at_least_one_of :title, :file_name, :content, :visibility_level + end + put ':id' do + snippet = snippets_for_current_user.find_by(id: params.delete(:id)) + return not_found!('Snippet') unless snippet + authorize! :update_personal_snippet, snippet + + attrs = declared_params(include_missing: false) + + UpdateSnippetService.new(nil, current_user, snippet, attrs).execute + if snippet.persisted? + present snippet, with: Entities::PersonalSnippet + else + render_validation_error!(snippet) + end + end + + desc 'Remove snippet' do + detail 'This feature was introduced in GitLab 8.15.' + success Entities::PersonalSnippet + end + params do + requires :id, type: Integer, desc: 'The ID of a snippet' + end + delete ':id' do + snippet = snippets_for_current_user.find_by(id: params.delete(:id)) + return not_found!('Snippet') unless snippet + authorize! :destroy_personal_snippet, snippet + snippet.destroy + no_content! + end + + desc 'Get a raw snippet' do + detail 'This feature was introduced in GitLab 8.15.' + end + params do + requires :id, type: Integer, desc: 'The ID of a snippet' + end + get ":id/raw" do + snippet = snippets_for_current_user.find_by(id: params.delete(:id)) + return not_found!('Snippet') unless snippet + + env['api.format'] = :txt + content_type 'text/plain' + present snippet.content + end + end + end +end diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index 99d0c28e749..ccb456bcc94 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -24,6 +24,8 @@ module Gitlab wiki_page_url when ProjectSnippet project_snippet_url(object) + when Snippet + personal_snippet_url(object) else raise NotImplementedError.new("No URL builder defined for #{object.class}") end diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index 28bdc18e840..4427443208a 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -9,65 +9,74 @@ describe SnippetsFinder do let(:project2) { create(:empty_project, :private, group: group) } context ':all filter' do - before do - @snippet1 = create(:personal_snippet, :private) - @snippet2 = create(:personal_snippet, :internal) - @snippet3 = create(:personal_snippet, :public) - end + let!(:snippet1) { create(:personal_snippet, :private) } + let!(:snippet2) { create(:personal_snippet, :internal) } + let!(:snippet3) { create(:personal_snippet, :public) } it "returns all private and internal snippets" do snippets = SnippetsFinder.new.execute(user, filter: :all) - expect(snippets).to include(@snippet2, @snippet3) - expect(snippets).not_to include(@snippet1) + expect(snippets).to include(snippet2, snippet3) + expect(snippets).not_to include(snippet1) end it "returns all public snippets" do snippets = SnippetsFinder.new.execute(nil, filter: :all) - expect(snippets).to include(@snippet3) - expect(snippets).not_to include(@snippet1, @snippet2) + expect(snippets).to include(snippet3) + expect(snippets).not_to include(snippet1, snippet2) end end - context ':by_user filter' do - before do - @snippet1 = create(:personal_snippet, :private, author: user) - @snippet2 = create(:personal_snippet, :internal, author: user) - @snippet3 = create(:personal_snippet, :public, author: user) + context ':public filter' do + let!(:snippet1) { create(:personal_snippet, :private) } + let!(:snippet2) { create(:personal_snippet, :internal) } + let!(:snippet3) { create(:personal_snippet, :public) } + + it "returns public public snippets" do + snippets = SnippetsFinder.new.execute(nil, filter: :public) + + expect(snippets).to include(snippet3) + expect(snippets).not_to include(snippet1, snippet2) end + end + + context ':by_user filter' do + let!(:snippet1) { create(:personal_snippet, :private, author: user) } + let!(:snippet2) { create(:personal_snippet, :internal, author: user) } + let!(:snippet3) { create(:personal_snippet, :public, author: user) } it "returns all public and internal snippets" do snippets = SnippetsFinder.new.execute(user1, filter: :by_user, user: user) - expect(snippets).to include(@snippet2, @snippet3) - expect(snippets).not_to include(@snippet1) + expect(snippets).to include(snippet2, snippet3) + expect(snippets).not_to include(snippet1) end it "returns internal snippets" do snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_internal") - expect(snippets).to include(@snippet2) - expect(snippets).not_to include(@snippet1, @snippet3) + expect(snippets).to include(snippet2) + expect(snippets).not_to include(snippet1, snippet3) end it "returns private snippets" do snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_private") - expect(snippets).to include(@snippet1) - expect(snippets).not_to include(@snippet2, @snippet3) + expect(snippets).to include(snippet1) + expect(snippets).not_to include(snippet2, snippet3) end it "returns public snippets" do snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_public") - expect(snippets).to include(@snippet3) - expect(snippets).not_to include(@snippet1, @snippet2) + expect(snippets).to include(snippet3) + expect(snippets).not_to include(snippet1, snippet2) end it "returns all snippets" do snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user) - expect(snippets).to include(@snippet1, @snippet2, @snippet3) + expect(snippets).to include(snippet1, snippet2, snippet3) end it "returns only public snippets if unauthenticated user" do snippets = SnippetsFinder.new.execute(nil, filter: :by_user, user: user) - expect(snippets).to include(@snippet3) - expect(snippets).not_to include(@snippet2, @snippet1) + expect(snippets).to include(snippet3) + expect(snippets).not_to include(snippet2, snippet1) end end diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb new file mode 100644 index 00000000000..f6fb6ea5506 --- /dev/null +++ b/spec/requests/api/snippets_spec.rb @@ -0,0 +1,157 @@ +require 'rails_helper' + +describe API::Snippets, api: true do + include ApiHelpers + let!(:user) { create(:user) } + + describe 'GET /snippets/' do + it 'returns snippets available' do + public_snippet = create(:personal_snippet, :public, author: user) + private_snippet = create(:personal_snippet, :private, author: user) + internal_snippet = create(:personal_snippet, :internal, author: user) + + get api("/snippets/", user) + + expect(response).to have_http_status(200) + expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly( + public_snippet.id, + internal_snippet.id, + private_snippet.id) + expect(json_response.last).to have_key('web_url') + expect(json_response.last).to have_key('raw_url') + end + + it 'hides private snippets from regular user' do + create(:personal_snippet, :private) + + get api("/snippets/", user) + expect(response).to have_http_status(200) + expect(json_response.size).to eq(0) + end + end + + describe 'GET /snippets/public' do + let!(:other_user) { create(:user) } + let!(:public_snippet) { create(:personal_snippet, :public, author: user) } + let!(:private_snippet) { create(:personal_snippet, :private, author: user) } + let!(:internal_snippet) { create(:personal_snippet, :internal, author: user) } + let!(:public_snippet_other) { create(:personal_snippet, :public, author: other_user) } + let!(:private_snippet_other) { create(:personal_snippet, :private, author: other_user) } + let!(:internal_snippet_other) { create(:personal_snippet, :internal, author: other_user) } + + it 'returns all snippets with public visibility from all users' do + get api("/snippets/public", user) + + expect(response).to have_http_status(200) + expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly( + public_snippet.id, + public_snippet_other.id) + expect(json_response.map{ |snippet| snippet['web_url']} ).to include( + "http://localhost/snippets/#{public_snippet.id}", + "http://localhost/snippets/#{public_snippet_other.id}") + expect(json_response.map{ |snippet| snippet['raw_url']} ).to include( + "http://localhost/snippets/#{public_snippet.id}/raw", + "http://localhost/snippets/#{public_snippet_other.id}/raw") + end + end + + describe 'GET /snippets/:id/raw' do + let(:snippet) { create(:personal_snippet, author: user) } + + it 'returns raw text' do + get api("/snippets/#{snippet.id}/raw", user) + + expect(response).to have_http_status(200) + expect(response.content_type).to eq 'text/plain' + expect(response.body).to eq(snippet.content) + end + + it 'returns 404 for invalid snippet id' do + delete api("/snippets/1234", user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Snippet Not Found') + end + end + + describe 'POST /snippets/' do + let(:params) do + { + title: 'Test Title', + file_name: 'test.rb', + content: 'puts "hello world"', + visibility_level: Gitlab::VisibilityLevel::PUBLIC + } + end + + it 'creates a new snippet' do + expect do + post api("/snippets/", user), params + end.to change { PersonalSnippet.count }.by(1) + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq(params[:title]) + expect(json_response['file_name']).to eq(params[:file_name]) + end + + it 'returns 400 for missing parameters' do + params.delete(:title) + + post api("/snippets/", user), params + + expect(response).to have_http_status(400) + end + end + + describe 'PUT /snippets/:id' do + let(:other_user) { create(:user) } + let(:public_snippet) { create(:personal_snippet, :public, author: user) } + it 'updates snippet' do + new_content = 'New content' + + put api("/snippets/#{public_snippet.id}", user), content: new_content + + expect(response).to have_http_status(200) + public_snippet.reload + expect(public_snippet.content).to eq(new_content) + end + + it 'returns 404 for invalid snippet id' do + put api("/snippets/1234", user), title: 'foo' + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Snippet Not Found') + end + + it "returns 404 for another user's snippet" do + put api("/snippets/#{public_snippet.id}", other_user), title: 'fubar' + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Snippet Not Found') + end + + it 'returns 400 for missing parameters' do + put api("/snippets/1234", user) + + expect(response).to have_http_status(400) + end + end + + describe 'DELETE /snippets/:id' do + let!(:public_snippet) { create(:personal_snippet, :public, author: user) } + it 'deletes snippet' do + expect do + delete api("/snippets/#{public_snippet.id}", user) + + expect(response).to have_http_status(204) + end.to change { PersonalSnippet.count }.by(-1) + end + + it 'returns 404 for invalid snippet id' do + delete api("/snippets/1234", user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Snippet Not Found') + end + end +end -- cgit v1.2.1 From 48d89919e6a0eebc43780c41a4d789cdcb716bb5 Mon Sep 17 00:00:00 2001 From: Sam Carrington <github@gwawr.co.uk> Date: Thu, 1 Dec 2016 18:08:37 +0000 Subject: Update php.md - /.dockerinit was removed in v1.11 so the test always results in an early exit https://github.com/docker/docker/pull/19490 --- doc/ci/examples/php.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md index 175e9d79904..82ffb841729 100644 --- a/doc/ci/examples/php.md +++ b/doc/ci/examples/php.md @@ -40,7 +40,7 @@ repository with the following content: #!/bin/bash # We need to install dependencies only for Docker -[[ ! -e /.dockerenv ]] && [[ ! -e /.dockerinit ]] && exit 0 +[[ ! -e /.dockerenv ]] && exit 0 set -xe -- cgit v1.2.1 From 2c0bcefdc6af442f67f74c6124fc1a9cbacd2831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Thu, 1 Dec 2016 16:24:35 +0100 Subject: Don't allow to specify a repo or version when installing Workhorse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The task will use the canonical repo and the required version. Signed-off-by: Rémy Coutable <remy@rymai.me> --- doc/install/installation.md | 12 ------------ doc/update/patch_versions.md | 7 +++---- lib/tasks/gitlab/workhorse.rake | 4 ++-- spec/tasks/gitlab/workhorse_rake_spec.rb | 26 -------------------------- 4 files changed, 5 insertions(+), 44 deletions(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index 77adb4c9f7b..4b0c585e51e 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -404,18 +404,6 @@ which is the recommended location. sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production -You can specify a different Git repository by providing `GITLAB_WORKHORSE_REPO`: - - cd /home/git/gitlab - - sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" GITLAB_WORKHORSE_REPO=https://example.com/gitlab-workhorse.git RAILS_ENV=production - -You can specify a different version to use by providing `GITLAB_WORKHORSE_VERSION`: - - cd /home/git/gitlab - - sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" GITLAB_WORKHORSE_VERSION=0.8.1 RAILS_ENV=production - ### Initialize Database and Activate Advanced Features # Go to GitLab installation folder diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index 60729316cde..e98c40ca4c0 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -45,10 +45,9 @@ sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`ca ### 4. Update gitlab-workhorse to the corresponding version ```bash -cd /home/git/gitlab-workhorse -sudo -u git -H git fetch -sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION` -b v`cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION` -sudo -u git -H make +cd /home/git/gitlab + +sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production ``` ### 5. Install libs, migrations, etc. diff --git a/lib/tasks/gitlab/workhorse.rake b/lib/tasks/gitlab/workhorse.rake index 46bd0bf2e7b..baea94bf8ca 100644 --- a/lib/tasks/gitlab/workhorse.rake +++ b/lib/tasks/gitlab/workhorse.rake @@ -7,8 +7,8 @@ namespace :gitlab do abort %(Please specify the directory where you want to install gitlab-workhorse:\n rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]") end - tag = "v#{ENV['GITLAB_WORKHORSE_VERSION'] || Gitlab::Workhorse.version}" - repo = ENV['GITLAB_WORKHORSE_REPO'] || 'https://gitlab.com/gitlab-org/gitlab-workhorse.git' + tag = "v#{Gitlab::Workhorse.version}" + repo = 'https://gitlab.com/gitlab-org/gitlab-workhorse.git' checkout_or_clone_tag(tag: tag, repo: repo, target_dir: args.dir) diff --git a/spec/tasks/gitlab/workhorse_rake_spec.rb b/spec/tasks/gitlab/workhorse_rake_spec.rb index b695abce091..6de66c3cf07 100644 --- a/spec/tasks/gitlab/workhorse_rake_spec.rb +++ b/spec/tasks/gitlab/workhorse_rake_spec.rb @@ -41,32 +41,6 @@ describe 'gitlab:workhorse namespace rake task' do run_rake_task('gitlab:workhorse:install', clone_path) end - - context 'given a specific repo' do - before do - expect(ENV).to receive(:[]).with('GITLAB_WORKHORSE_REPO').and_return('https://gitlab.com/user1/gitlab-workhorse.git') - end - - it 'calls checkout_or_clone_tag with the given repo' do - expect_any_instance_of(Object). - to receive(:checkout_or_clone_tag).with(tag: tag, repo: 'https://gitlab.com/user1/gitlab-workhorse.git', target_dir: clone_path) - - run_rake_task('gitlab:workhorse:install', clone_path) - end - end - - context 'given a specific version' do - before do - allow(ENV).to receive(:[]).with('GITLAB_WORKHORSE_VERSION').and_return('42.42.0') - end - - it 'calls checkout_or_clone_tag with the given repo' do - expect_any_instance_of(Object). - to receive(:checkout_or_clone_tag).with(tag: 'v42.42.0', repo: repo, target_dir: clone_path) - - run_rake_task('gitlab:workhorse:install', clone_path) - end - end end describe 'gmake/make' do -- cgit v1.2.1 From d3f1ad82ce6466c322aff37469638bb3bc586185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= <alejorro70@gmail.com> Date: Thu, 1 Dec 2016 17:31:04 -0300 Subject: Update CHANGELOG.md for 8.14.2 [ci skip] --- CHANGELOG.md | 20 ++++++++++++++++++++ ...th-developer-access-can-no-longer-create-tags.yml | 4 ---- ...-error-undefined-method-size-for-nil-nilclass.yml | 4 ---- .../24894-style-system-note-in-commit-discussion.yml | 4 ---- .../25055-pipelines-info-missing-from-mr-widget.yml | 4 ---- changelogs/unreleased/boards-issue-sorting.yml | 4 ---- changelogs/unreleased/clean-up-jira-service.yml | 4 ---- changelogs/unreleased/events-cache-invalidation.yml | 4 ---- changelogs/unreleased/fix-ca-no-date.yml | 4 ---- ...-access-wiki-when-repository-feature-disabled.yml | 4 ---- changelogs/unreleased/fixed-commit-timeago.yml | 4 ---- .../unreleased/refresh-authorizations-with-lease.yml | 4 ---- changelogs/unreleased/rephrase-system-notes.yml | 4 ---- .../unreleased/rescue-from-redis-init-errors.yml | 4 ---- .../unreleased/resolve-discussions-timeago.yml | 4 ---- changelogs/unreleased/sh-update-sidekiq-cron.yml | 4 ---- .../timeout-merge-request-for-binary-file.yml | 4 ---- changelogs/unreleased/workhorse-v1-0-1.yml | 4 ---- 18 files changed, 20 insertions(+), 68 deletions(-) delete mode 100644 changelogs/unreleased/24813-project-members-with-developer-access-can-no-longer-create-tags.yml delete mode 100644 changelogs/unreleased/24860-actionview-template-error-undefined-method-size-for-nil-nilclass.yml delete mode 100644 changelogs/unreleased/24894-style-system-note-in-commit-discussion.yml delete mode 100644 changelogs/unreleased/25055-pipelines-info-missing-from-mr-widget.yml delete mode 100644 changelogs/unreleased/boards-issue-sorting.yml delete mode 100644 changelogs/unreleased/clean-up-jira-service.yml delete mode 100644 changelogs/unreleased/events-cache-invalidation.yml delete mode 100644 changelogs/unreleased/fix-ca-no-date.yml delete mode 100644 changelogs/unreleased/fix-git-access-wiki-when-repository-feature-disabled.yml delete mode 100644 changelogs/unreleased/fixed-commit-timeago.yml delete mode 100644 changelogs/unreleased/refresh-authorizations-with-lease.yml delete mode 100644 changelogs/unreleased/rephrase-system-notes.yml delete mode 100644 changelogs/unreleased/rescue-from-redis-init-errors.yml delete mode 100644 changelogs/unreleased/resolve-discussions-timeago.yml delete mode 100644 changelogs/unreleased/sh-update-sidekiq-cron.yml delete mode 100644 changelogs/unreleased/timeout-merge-request-for-binary-file.yml delete mode 100644 changelogs/unreleased/workhorse-v1-0-1.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 12a3e63ed2e..86b30d2832d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 8.14.2 (2016-12-01) + +- Remove caching of events data. !6578 +- Rephrase some system notes to be compatible with new system note style. !7692 +- Pass tag SHA to post-receive hook when tag is created via UI. !7700 +- Prevent error when submitting a merge request and pipeline is not defined. !7707 +- Fixes system note style in commit discussion. !7721 +- Use a Redis lease for updating authorized projects. !7733 +- Refactor JiraService by moving code out of JiraService#execute method. !7756 +- Update GitLab Workhorse to v1.0.1. !7759 +- Fix pipelines info being hidden in merge request widget. !7808 +- Fixed commit timeago not rendering after initial page. +- Fix for error thrown in cycle analytics events if build has not started. +- Fixed issue boards issue sorting when dragging issue into list. +- Allow access to the wiki with git when repository feature disabled. +- Fixed timeago not rendering when resolving a discussion. +- Update Sidekiq-cron to fix compatibility issues with Sidekiq 4.2.1. +- Timeout creating and viewing merge request for binary file. +- Gracefully recover from Redis connection failures in Sidekiq initializer. + ## 8.14.1 (2016-11-28) - Fix deselecting calendar days on contribution graph. !6453 (ClemMakesApps) diff --git a/changelogs/unreleased/24813-project-members-with-developer-access-can-no-longer-create-tags.yml b/changelogs/unreleased/24813-project-members-with-developer-access-can-no-longer-create-tags.yml deleted file mode 100644 index 9254db40742..00000000000 --- a/changelogs/unreleased/24813-project-members-with-developer-access-can-no-longer-create-tags.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Pass tag SHA to post-receive hook when tag is created via UI -merge_request: 7700 -author: diff --git a/changelogs/unreleased/24860-actionview-template-error-undefined-method-size-for-nil-nilclass.yml b/changelogs/unreleased/24860-actionview-template-error-undefined-method-size-for-nil-nilclass.yml deleted file mode 100644 index 4b4aea79380..00000000000 --- a/changelogs/unreleased/24860-actionview-template-error-undefined-method-size-for-nil-nilclass.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Prevent error when submitting a merge request and pipeline is not defined -merge_request: 7707 -author: diff --git a/changelogs/unreleased/24894-style-system-note-in-commit-discussion.yml b/changelogs/unreleased/24894-style-system-note-in-commit-discussion.yml deleted file mode 100644 index 7ddf0b46d4c..00000000000 --- a/changelogs/unreleased/24894-style-system-note-in-commit-discussion.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixes system note style in commit discussion -merge_request: 7721 -author: diff --git a/changelogs/unreleased/25055-pipelines-info-missing-from-mr-widget.yml b/changelogs/unreleased/25055-pipelines-info-missing-from-mr-widget.yml deleted file mode 100644 index dad9db0ffef..00000000000 --- a/changelogs/unreleased/25055-pipelines-info-missing-from-mr-widget.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix pipelines info being hidden in merge request widget -merge_request: 7808 -author: diff --git a/changelogs/unreleased/boards-issue-sorting.yml b/changelogs/unreleased/boards-issue-sorting.yml deleted file mode 100644 index fb7dc2f9190..00000000000 --- a/changelogs/unreleased/boards-issue-sorting.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed issue boards issue sorting when dragging issue into list -merge_request: -author: diff --git a/changelogs/unreleased/clean-up-jira-service.yml b/changelogs/unreleased/clean-up-jira-service.yml deleted file mode 100644 index a309cb57532..00000000000 --- a/changelogs/unreleased/clean-up-jira-service.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Refactor JiraService by moving code out of JiraService#execute method -merge_request: 7756 -author: diff --git a/changelogs/unreleased/events-cache-invalidation.yml b/changelogs/unreleased/events-cache-invalidation.yml deleted file mode 100644 index 2b30f4dcbce..00000000000 --- a/changelogs/unreleased/events-cache-invalidation.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove caching of events data -merge_request: 6578 -author: diff --git a/changelogs/unreleased/fix-ca-no-date.yml b/changelogs/unreleased/fix-ca-no-date.yml deleted file mode 100644 index 6de4a56ac0d..00000000000 --- a/changelogs/unreleased/fix-ca-no-date.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix for error thrown in cycle analytics events if build has not started -merge_request: -author: diff --git a/changelogs/unreleased/fix-git-access-wiki-when-repository-feature-disabled.yml b/changelogs/unreleased/fix-git-access-wiki-when-repository-feature-disabled.yml deleted file mode 100644 index 82ca6316876..00000000000 --- a/changelogs/unreleased/fix-git-access-wiki-when-repository-feature-disabled.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow access to the wiki with git when repository feature disabled -merge_request: -author: diff --git a/changelogs/unreleased/fixed-commit-timeago.yml b/changelogs/unreleased/fixed-commit-timeago.yml deleted file mode 100644 index 295d8db63d0..00000000000 --- a/changelogs/unreleased/fixed-commit-timeago.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed commit timeago not rendering after initial page -merge_request: -author: diff --git a/changelogs/unreleased/refresh-authorizations-with-lease.yml b/changelogs/unreleased/refresh-authorizations-with-lease.yml deleted file mode 100644 index bb9b77018e3..00000000000 --- a/changelogs/unreleased/refresh-authorizations-with-lease.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use a Redis lease for updating authorized projects -merge_request: 7733 -author: diff --git a/changelogs/unreleased/rephrase-system-notes.yml b/changelogs/unreleased/rephrase-system-notes.yml deleted file mode 100644 index e77c3a31cb4..00000000000 --- a/changelogs/unreleased/rephrase-system-notes.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Rephrase some system notes to be compatible with new system note style -merge_request: 7692 -author: diff --git a/changelogs/unreleased/rescue-from-redis-init-errors.yml b/changelogs/unreleased/rescue-from-redis-init-errors.yml deleted file mode 100644 index c41f49597e7..00000000000 --- a/changelogs/unreleased/rescue-from-redis-init-errors.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Gracefully recover from Redis connection failures in Sidekiq initializer -merge_request: -author: diff --git a/changelogs/unreleased/resolve-discussions-timeago.yml b/changelogs/unreleased/resolve-discussions-timeago.yml deleted file mode 100644 index ffedeb93f1d..00000000000 --- a/changelogs/unreleased/resolve-discussions-timeago.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed timeago not rendering when resolving a discussion -merge_request: -author: diff --git a/changelogs/unreleased/sh-update-sidekiq-cron.yml b/changelogs/unreleased/sh-update-sidekiq-cron.yml deleted file mode 100644 index d79ba817a18..00000000000 --- a/changelogs/unreleased/sh-update-sidekiq-cron.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Update Sidekiq-cron to fix compatibility issues with Sidekiq 4.2.1 -merge_request: -author: diff --git a/changelogs/unreleased/timeout-merge-request-for-binary-file.yml b/changelogs/unreleased/timeout-merge-request-for-binary-file.yml deleted file mode 100644 index 5161265d1bd..00000000000 --- a/changelogs/unreleased/timeout-merge-request-for-binary-file.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Timeout creating and viewing merge request for binary file -merge_request: -author: diff --git a/changelogs/unreleased/workhorse-v1-0-1.yml b/changelogs/unreleased/workhorse-v1-0-1.yml deleted file mode 100644 index c26c2d45b1d..00000000000 --- a/changelogs/unreleased/workhorse-v1-0-1.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Update GitLab Workhorse to v1.0.1 -merge_request: 7759 -author: -- cgit v1.2.1 From 5aaea39b8b24512a6e8c5006cf8d7758582e0fdf Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Thu, 17 Nov 2016 16:00:12 -0700 Subject: Use system font --- app/assets/stylesheets/framework/variables.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 8a9c279d124..19c2edb0489 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -210,7 +210,10 @@ $line-select-yellow-dark: #f0e2bd; * Fonts */ $monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; -$regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif; +$regular_font: -apple-system, BlinkMacSystemFont, + "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", + "Droid Sans", "Helvetica Neue", sans-serif; /* * Dropdowns -- cgit v1.2.1 From 9b10cc16cb7b623a91a444d83d987173bfeb2d8c Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Fri, 18 Nov 2016 08:41:11 -0700 Subject: Remove source sans pro --- app/assets/fonts/OFL.txt | 93 --------------------- app/assets/fonts/SourceSansPro-Black.ttf.woff | Bin 113800 -> 0 bytes app/assets/fonts/SourceSansPro-Black.ttf.woff2 | Bin 82052 -> 0 bytes app/assets/fonts/SourceSansPro-BlackIt.ttf.woff | Bin 49704 -> 0 bytes app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2 | Bin 34812 -> 0 bytes app/assets/fonts/SourceSansPro-Bold.ttf.woff | Bin 117872 -> 0 bytes app/assets/fonts/SourceSansPro-Bold.ttf.woff2 | Bin 85604 -> 0 bytes app/assets/fonts/SourceSansPro-BoldIt.ttf.woff | Bin 50608 -> 0 bytes app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2 | Bin 35864 -> 0 bytes app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff | Bin 114336 -> 0 bytes .../fonts/SourceSansPro-ExtraLight.ttf.woff2 | Bin 82808 -> 0 bytes .../fonts/SourceSansPro-ExtraLightIt.ttf.woff | Bin 49684 -> 0 bytes .../fonts/SourceSansPro-ExtraLightIt.ttf.woff2 | Bin 34560 -> 0 bytes app/assets/fonts/SourceSansPro-It.ttf.woff | Bin 51012 -> 0 bytes app/assets/fonts/SourceSansPro-It.ttf.woff2 | Bin 36016 -> 0 bytes app/assets/fonts/SourceSansPro-Light.ttf.woff | Bin 118284 -> 0 bytes app/assets/fonts/SourceSansPro-Light.ttf.woff2 | Bin 86336 -> 0 bytes app/assets/fonts/SourceSansPro-LightIt.ttf.woff | Bin 50992 -> 0 bytes app/assets/fonts/SourceSansPro-LightIt.ttf.woff2 | Bin 35952 -> 0 bytes app/assets/fonts/SourceSansPro-Regular.ttf.woff | Bin 119064 -> 0 bytes app/assets/fonts/SourceSansPro-Regular.ttf.woff2 | Bin 86844 -> 0 bytes app/assets/fonts/SourceSansPro-Semibold.ttf.woff | Bin 118412 -> 0 bytes app/assets/fonts/SourceSansPro-Semibold.ttf.woff2 | Bin 86196 -> 0 bytes app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff | Bin 50924 -> 0 bytes .../fonts/SourceSansPro-SemiboldIt.ttf.woff2 | Bin 35984 -> 0 bytes app/assets/stylesheets/framework.scss | 1 - app/assets/stylesheets/framework/fonts.scss | 45 ---------- app/assets/stylesheets/framework/variables.scss | 5 +- 28 files changed, 1 insertion(+), 143 deletions(-) delete mode 100644 app/assets/fonts/OFL.txt delete mode 100644 app/assets/fonts/SourceSansPro-Black.ttf.woff delete mode 100644 app/assets/fonts/SourceSansPro-Black.ttf.woff2 delete mode 100644 app/assets/fonts/SourceSansPro-BlackIt.ttf.woff delete mode 100644 app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2 delete mode 100644 app/assets/fonts/SourceSansPro-Bold.ttf.woff delete mode 100644 app/assets/fonts/SourceSansPro-Bold.ttf.woff2 delete mode 100644 app/assets/fonts/SourceSansPro-BoldIt.ttf.woff delete mode 100644 app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2 delete mode 100644 app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff delete mode 100644 app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2 delete mode 100644 app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff delete mode 100644 app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2 delete mode 100644 app/assets/fonts/SourceSansPro-It.ttf.woff delete mode 100644 app/assets/fonts/SourceSansPro-It.ttf.woff2 delete mode 100644 app/assets/fonts/SourceSansPro-Light.ttf.woff delete mode 100644 app/assets/fonts/SourceSansPro-Light.ttf.woff2 delete mode 100644 app/assets/fonts/SourceSansPro-LightIt.ttf.woff delete mode 100644 app/assets/fonts/SourceSansPro-LightIt.ttf.woff2 delete mode 100644 app/assets/fonts/SourceSansPro-Regular.ttf.woff delete mode 100644 app/assets/fonts/SourceSansPro-Regular.ttf.woff2 delete mode 100644 app/assets/fonts/SourceSansPro-Semibold.ttf.woff delete mode 100644 app/assets/fonts/SourceSansPro-Semibold.ttf.woff2 delete mode 100644 app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff delete mode 100644 app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2 delete mode 100644 app/assets/stylesheets/framework/fonts.scss diff --git a/app/assets/fonts/OFL.txt b/app/assets/fonts/OFL.txt deleted file mode 100644 index df187637e18..00000000000 --- a/app/assets/fonts/OFL.txt +++ /dev/null @@ -1,93 +0,0 @@ -Copyright 2010, 2012, 2014 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. - -This Font Software is licensed under the SIL Open Font License, Version 1.1. - -This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/app/assets/fonts/SourceSansPro-Black.ttf.woff b/app/assets/fonts/SourceSansPro-Black.ttf.woff deleted file mode 100644 index b7e86200927..00000000000 Binary files a/app/assets/fonts/SourceSansPro-Black.ttf.woff and /dev/null differ diff --git a/app/assets/fonts/SourceSansPro-Black.ttf.woff2 b/app/assets/fonts/SourceSansPro-Black.ttf.woff2 deleted file mode 100644 index c90d078406c..00000000000 Binary files a/app/assets/fonts/SourceSansPro-Black.ttf.woff2 and /dev/null differ diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff deleted file mode 100644 index c3314b1ef06..00000000000 Binary files a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff and /dev/null differ diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2 deleted file mode 100644 index b87e22c41b5..00000000000 Binary files a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2 and /dev/null differ diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf.woff b/app/assets/fonts/SourceSansPro-Bold.ttf.woff deleted file mode 100644 index d1d40f840f8..00000000000 Binary files a/app/assets/fonts/SourceSansPro-Bold.ttf.woff and /dev/null differ diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf.woff2 b/app/assets/fonts/SourceSansPro-Bold.ttf.woff2 deleted file mode 100644 index 0f46f3e833a..00000000000 Binary files a/app/assets/fonts/SourceSansPro-Bold.ttf.woff2 and /dev/null differ diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff deleted file mode 100644 index ef6ff514d3a..00000000000 Binary files a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff and /dev/null differ diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2 deleted file mode 100644 index 8007df6df32..00000000000 Binary files a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2 and /dev/null differ diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff deleted file mode 100644 index 1e6c94d9eb3..00000000000 Binary files a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff and /dev/null differ diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2 b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2 deleted file mode 100644 index b715f274082..00000000000 Binary files a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2 and /dev/null differ diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff deleted file mode 100644 index 7a408b1ec73..00000000000 Binary files a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff and /dev/null differ diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2 deleted file mode 100644 index d8f9d29d4aa..00000000000 Binary files a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2 and /dev/null differ diff --git a/app/assets/fonts/SourceSansPro-It.ttf.woff b/app/assets/fonts/SourceSansPro-It.ttf.woff deleted file mode 100644 index 4d54bc95718..00000000000 Binary files a/app/assets/fonts/SourceSansPro-It.ttf.woff and /dev/null differ diff --git a/app/assets/fonts/SourceSansPro-It.ttf.woff2 b/app/assets/fonts/SourceSansPro-It.ttf.woff2 deleted file mode 100644 index a00852641f8..00000000000 Binary files a/app/assets/fonts/SourceSansPro-It.ttf.woff2 and /dev/null differ diff --git a/app/assets/fonts/SourceSansPro-Light.ttf.woff b/app/assets/fonts/SourceSansPro-Light.ttf.woff deleted file mode 100644 index 1706d57d3c5..00000000000 Binary files a/app/assets/fonts/SourceSansPro-Light.ttf.woff and /dev/null differ diff --git a/app/assets/fonts/SourceSansPro-Light.ttf.woff2 b/app/assets/fonts/SourceSansPro-Light.ttf.woff2 deleted file mode 100644 index d8b610ad76e..00000000000 Binary files a/app/assets/fonts/SourceSansPro-Light.ttf.woff2 and /dev/null differ diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff deleted file mode 100644 index 87378d6c609..00000000000 Binary files a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff and /dev/null differ diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2 deleted file mode 100644 index e0eebac8273..00000000000 Binary files a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2 and /dev/null differ diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf.woff b/app/assets/fonts/SourceSansPro-Regular.ttf.woff deleted file mode 100644 index 460ab12a638..00000000000 Binary files a/app/assets/fonts/SourceSansPro-Regular.ttf.woff and /dev/null differ diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf.woff2 b/app/assets/fonts/SourceSansPro-Regular.ttf.woff2 deleted file mode 100644 index 0dd3464c74b..00000000000 Binary files a/app/assets/fonts/SourceSansPro-Regular.ttf.woff2 and /dev/null differ diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff deleted file mode 100644 index 43379631b2d..00000000000 Binary files a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff and /dev/null differ diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2 b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2 deleted file mode 100644 index 2526d2e1b60..00000000000 Binary files a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2 and /dev/null differ diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff deleted file mode 100644 index 232c2048ae7..00000000000 Binary files a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff and /dev/null differ diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2 deleted file mode 100644 index 606935af089..00000000000 Binary files a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2 and /dev/null differ diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 7c7f991dd87..4aaff7d04f1 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -1,4 +1,3 @@ -@import "framework/fonts"; @import "framework/variables"; @import "framework/mixins"; @import 'framework/tw_bootstrap_variables'; diff --git a/app/assets/stylesheets/framework/fonts.scss b/app/assets/stylesheets/framework/fonts.scss deleted file mode 100644 index 5f9685bc71a..00000000000 --- a/app/assets/stylesheets/framework/fonts.scss +++ /dev/null @@ -1,45 +0,0 @@ -// Disabling "SpaceAfterPropertyColon" linter because the linter doesn't like -// the way the `src` property is formatted in this file. -// scss-lint:disable SpaceAfterPropertyColon - -/* latin-ext */ -@font-face { - font-family: 'Source Sans Pro'; - font-style: normal; - font-weight: 300; - src: - local('Source Sans Pro Light'), - local('SourceSansPro-Light'), - font-url('SourceSansPro-Light.ttf.woff2') format('woff2'), - font-url('SourceSansPro-Light.ttf.woff') format('woff'); -} -@font-face { - font-family: 'Source Sans Pro'; - font-style: normal; - font-weight: 400; - src: - local('Source Sans Pro'), - local('SourceSansPro-Regular'), - font-url('SourceSansPro-Regular.ttf.woff2') format('woff2'), - font-url('SourceSansPro-Regular.ttf.woff') format('woff'); -} -@font-face { - font-family: 'Source Sans Pro'; - font-style: normal; - font-weight: 600; - src: - local('Source Sans Pro Semibold'), - local('SourceSansPro-Semibold'), - font-url('SourceSansPro-Semibold.ttf.woff2') format('woff2'), - font-url('SourceSansPro-Semibold.ttf.woff') format('woff'); -} -@font-face { - font-family: 'Source Sans Pro'; - font-style: normal; - font-weight: 700; - src: - local('Source Sans Pro Bold'), - local('SourceSansPro-Bold'), - font-url('SourceSansPro-Bold.ttf.woff2') format('woff2'), - font-url('SourceSansPro-Bold.ttf.woff') format('woff'); -} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 19c2edb0489..26e08d08571 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -210,10 +210,7 @@ $line-select-yellow-dark: #f0e2bd; * Fonts */ $monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; -$regular_font: -apple-system, BlinkMacSystemFont, - "Segoe UI", "Roboto", "Oxygen", - "Ubuntu", "Cantarell", "Fira Sans", - "Droid Sans", "Helvetica Neue", sans-serif; +$regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; /* * Dropdowns -- cgit v1.2.1 From 33faed1135d94f2ebc3c868ccc78366439204c2d Mon Sep 17 00:00:00 2001 From: tauriedavis <taurie@gitlab.com> Date: Thu, 1 Dec 2016 11:04:32 -0800 Subject: 24726 Remove Across GitLab from side navigation --- app/assets/stylesheets/framework/gitlab-theme.scss | 1 - app/assets/stylesheets/framework/sidebar.scss | 5 ----- app/views/layouts/nav/_dashboard.html.haml | 1 - changelogs/unreleased/24726-remove-across-gitlab.yml | 4 ++++ 4 files changed, 4 insertions(+), 7 deletions(-) create mode 100644 changelogs/unreleased/24726-remove-across-gitlab.yml diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index 91ab1503439..c5e5dad574d 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -21,7 +21,6 @@ background: $color-darker; } - .sidebar-header, .sidebar-action-buttons { color: $color-light; background-color: lighten($color-darker, 5%); diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 44c445c0543..53f79a1fc94 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -59,11 +59,6 @@ padding: 0 !important; } - .sidebar-header { - padding: 11px 22px 12px; - font-size: 20px; - } - li { &.separate-item { padding-top: 10px; diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 2a6d9cda379..817e4bebb05 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,5 +1,4 @@ .nav-sidebar - .sidebar-header Across GitLab %ul.nav = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do diff --git a/changelogs/unreleased/24726-remove-across-gitlab.yml b/changelogs/unreleased/24726-remove-across-gitlab.yml new file mode 100644 index 00000000000..6436e4b688f --- /dev/null +++ b/changelogs/unreleased/24726-remove-across-gitlab.yml @@ -0,0 +1,4 @@ +--- +title: 24726 Remove Across GitLab from side navigation +merge_request: +author: -- cgit v1.2.1 From f90b6200e450f6b87cc310ab5734a09719d891d3 Mon Sep 17 00:00:00 2001 From: winniehell <git@winniehell.de> Date: Mon, 7 Nov 2016 01:16:39 +0100 Subject: Clean up common_utils.js (!7318) --- app/assets/javascripts/application.js | 18 +++++++- app/assets/javascripts/gfm_auto_complete.js.es6 | 18 +++++--- app/assets/javascripts/lib/utils/common_utils.js | 54 ----------------------- app/assets/javascripts/merge_request_tabs.js | 3 +- changelogs/unreleased/cleanup-common_utils-js.yml | 4 ++ spec/javascripts/application_spec.js | 37 ---------------- spec/javascripts/fixtures/application.html.haml | 2 - 7 files changed, 33 insertions(+), 103 deletions(-) create mode 100644 changelogs/unreleased/cleanup-common_utils-js.yml delete mode 100644 spec/javascripts/application_spec.js delete mode 100644 spec/javascripts/fixtures/application.html.haml diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index cfab4721f4b..b7c4673c8e3 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -56,7 +56,13 @@ /*= require es6-promise.auto */ (function () { - document.addEventListener('page:fetch', gl.utils.cleanupBeforeFetch); + document.addEventListener('page:fetch', function () { + // Unbind scroll events + $(document).off('scroll'); + // Close any open tooltips + $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy'); + }); + window.addEventListener('hashchange', gl.utils.handleLocationHash); window.addEventListener('load', function onLoad() { window.removeEventListener('load', onLoad, false); @@ -76,7 +82,15 @@ // Set the default path for all cookies to GitLab's root directory Cookies.defaults.path = gon.relative_url_root || '/'; - gl.utils.preventDisabledButtons(); + // prevent default action for disabled buttons + $('.btn').click(function(e) { + if ($(this).hasClass('disabled')) { + e.preventDefault(); + e.stopImmediatePropagation(); + return false; + } + }); + $('.nav-sidebar').niceScroll({ cursoropacitymax: '0.4', cursorcolor: '#FFF', diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 10769b7fd4f..6f9d6283071 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -5,6 +5,10 @@ window.GitLab = {}; } + function sanitize(str) { + return str.replace(/<(?:.|\n)*?>/gm, ''); + } + GitLab.GfmAutoComplete = { dataLoading: false, dataLoaded: false, @@ -160,8 +164,8 @@ return { username: m.username, avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar, - title: gl.utils.sanitize(title), - search: gl.utils.sanitize(m.username + " " + m.name) + title: sanitize(title), + search: sanitize(m.username + " " + m.name) }; }); } @@ -195,7 +199,7 @@ } return { id: i.iid, - title: gl.utils.sanitize(i.title), + title: sanitize(i.title), search: i.iid + " " + i.title }; }); @@ -228,7 +232,7 @@ } return { id: m.iid, - title: gl.utils.sanitize(m.title), + title: sanitize(m.title), search: "" + m.title }; }); @@ -263,7 +267,7 @@ } return { id: m.iid, - title: gl.utils.sanitize(m.title), + title: sanitize(m.title), search: m.iid + " " + m.title }; }); @@ -284,9 +288,9 @@ var sanitizeLabelTitle; sanitizeLabelTitle = function(title) { if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) { - return "\"" + (gl.utils.sanitize(title)) + "\""; + return "\"" + (sanitize(title)) + "\""; } else { - return gl.utils.sanitize(title); + return sanitize(title); } }; return $.map(merges, function(m) { diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index c5846068b07..29cba1a49dd 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -33,10 +33,6 @@ }); }; - w.gl.utils.split = function(val) { - return val.split(/,\s*/); - }; - w.gl.utils.extractLast = function(term) { return this.split(term).pop(); }; @@ -67,33 +63,6 @@ }); }; - w.gl.utils.disableButtonIfAnyEmptyField = function(form, form_selector, button_selector) { - var closest_submit, updateButtons; - closest_submit = form.find(button_selector); - updateButtons = function() { - var filled; - filled = true; - form.find('input').filter(form_selector).each(function() { - return filled = this.rstrip($(this).val()) !== "" || !$(this).attr('required'); - }); - if (filled) { - return closest_submit.enable(); - } else { - return closest_submit.disable(); - } - }; - updateButtons(); - return form.keyup(updateButtons); - }; - - w.gl.utils.sanitize = function(str) { - return str.replace(/<(?:.|\n)*?>/gm, ''); - }; - - w.gl.utils.unbindEvents = function() { - return $(document).off('scroll'); - }; - // automatically adjust scroll position for hash urls taking the height of the navbar into account // https://github.com/twitter/bootstrap/issues/1768 w.gl.utils.handleLocationHash = function() { @@ -124,32 +93,9 @@ } }; - gl.utils.updateTooltipTitle = function($tooltipEl, newTitle) { - return $tooltipEl.tooltip('destroy').attr('title', newTitle).tooltip('fixTitle'); - }; - gl.utils.preventDisabledButtons = function() { - return $('.btn').click(function(e) { - if ($(this).hasClass('disabled')) { - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - } - }); - }; gl.utils.getPagePath = function() { return $('body').data('page').split(':')[0]; }; - gl.utils.parseUrl = function (url) { - var parser = document.createElement('a'); - parser.href = url; - return parser; - }; - gl.utils.cleanupBeforeFetch = function() { - // Unbind scroll events - $(document).off('scroll'); - // Close any open tooltips - $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy'); - }; gl.utils.isMetaKey = function(e) { return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index b1928f8d279..4a192ca5796 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -220,7 +220,8 @@ // We extract pathname for the current Changes tab anchor href // some pages like MergeRequestsController#new has query parameters on that anchor - var url = gl.utils.parseUrl(source); + var url = document.createElement('a'); + url.href = source; return this._get({ url: (url.pathname + ".json") + this._location.search, diff --git a/changelogs/unreleased/cleanup-common_utils-js.yml b/changelogs/unreleased/cleanup-common_utils-js.yml new file mode 100644 index 00000000000..54d81b76c28 --- /dev/null +++ b/changelogs/unreleased/cleanup-common_utils-js.yml @@ -0,0 +1,4 @@ +--- +title: Clean up common_utils.js +merge_request: 7318 +author: winniehell diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js deleted file mode 100644 index 7e38abc608e..00000000000 --- a/spec/javascripts/application_spec.js +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, padded-blocks, max-len */ - -/*= require lib/utils/common_utils */ - -(function() { - describe('Application', function() { - return describe('disable buttons', function() { - fixture.preload('application.html'); - beforeEach(function() { - return fixture.load('application.html'); - }); - it('should prevent default action for disabled buttons', function() { - var $button, isClicked; - gl.utils.preventDisabledButtons(); - isClicked = false; - $button = $('#test-button'); - expect($button).toExist(); - $button.click(function() { - return isClicked = true; - }); - $button.trigger('click'); - return expect(isClicked).toBe(false); - }); - - it('should be on the same page if a disabled link clicked', function() { - var locationBeforeLinkClick, $link; - locationBeforeLinkClick = window.location.href; - gl.utils.preventDisabledButtons(); - $link = $('#test-link'); - expect($link).toExist(); - $link.click(); - return expect(window.location.href).toBe(locationBeforeLinkClick); - }); - }); - }); - -}).call(this); diff --git a/spec/javascripts/fixtures/application.html.haml b/spec/javascripts/fixtures/application.html.haml deleted file mode 100644 index 3fc6114407d..00000000000 --- a/spec/javascripts/fixtures/application.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -%a#test-link.btn.disabled{:href => "/foo"} Test link -%button#test-button.btn.disabled Test Button -- cgit v1.2.1 From fb5f7733f14b701f6e22a54bc8b4c08330bc036f Mon Sep 17 00:00:00 2001 From: DJ Mountney <david@twkie.net> Date: Sun, 2 Oct 2016 15:30:44 -0700 Subject: Allow users to seed the initial runner registration token using an environment variable This is useful for when runner is bundled with gitlab, like in a kubernetes stack, and we want the runner to be able to register with gitlab as soon as they both come up. --- app/models/concerns/token_authenticatable.rb | 4 ++++ db/fixtures/production/010_settings.rb | 16 +++++++++++++++ doc/administration/environment_variables.md | 23 +++++++++++----------- spec/db/production/settings.rb | 16 +++++++++++++++ spec/models/concerns/token_authenticatable_spec.rb | 7 +++++++ 5 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 db/fixtures/production/010_settings.rb create mode 100644 spec/db/production/settings.rb diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index 04d30f46210..1ca7f91dc03 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -39,6 +39,10 @@ module TokenAuthenticatable current_token.blank? ? write_new_token(token_field) : current_token end + define_method("set_#{token_field}") do |token| + write_attribute(token_field, token) if token + end + define_method("ensure_#{token_field}!") do send("reset_#{token_field}!") if read_attribute(token_field).blank? read_attribute(token_field) diff --git a/db/fixtures/production/010_settings.rb b/db/fixtures/production/010_settings.rb new file mode 100644 index 00000000000..5522f31629a --- /dev/null +++ b/db/fixtures/production/010_settings.rb @@ -0,0 +1,16 @@ +if ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN'].present? + settings = ApplicationSetting.current || ApplicationSetting.create_from_defaults + settings.set_runners_registration_token(ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN']) + + if settings.save + puts "Saved Runner Registration Token".color(:green) + else + puts "Could not save Runner Registration Token".color(:red) + puts + settings.errors.full_messages.map do |message| + puts "--> #{message}".color(:red) + end + puts + exit 1 + end +end diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md index b4a953d1ccc..76029b30dd8 100644 --- a/doc/administration/environment_variables.md +++ b/doc/administration/environment_variables.md @@ -13,17 +13,18 @@ override certain values. Variable | Type | Description -------- | ---- | ----------- -`GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation -`GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`) -`RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test` -`DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development` -`GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab -`GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab -`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab -`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab -`GITLAB_EMAIL_SUBJECT_SUFFIX` | string | The e-mail subject suffix used in e-mails sent by GitLab -`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer -`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer +`GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation +`GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`) +`RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test` +`DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development` +`GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab +`GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab +`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab +`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab +`GITLAB_EMAIL_SUBJECT_SUFFIX` | string | The e-mail subject suffix used in e-mails sent by GitLab +`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer +`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer +`GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN` | string | Sets the initial registration token used for GitLab Runners ## Complete database variables diff --git a/spec/db/production/settings.rb b/spec/db/production/settings.rb new file mode 100644 index 00000000000..a7c5283df94 --- /dev/null +++ b/spec/db/production/settings.rb @@ -0,0 +1,16 @@ +require 'spec_helper' +require 'rainbow/ext/string' + +describe 'seed production settings', lib: true do + context 'GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN is set in the environment' do + before do + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:[]).with('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN').and_return('013456789') + end + + it 'writes the token to the database' do + load(File.join(__dir__, '../../../db/fixtures/production/010_settings.rb')) + expect(ApplicationSetting.current.runners_registration_token).to eq('013456789') + end + end +end diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb index eb64f3d0c83..4b0bfa43abf 100644 --- a/spec/models/concerns/token_authenticatable_spec.rb +++ b/spec/models/concerns/token_authenticatable_spec.rb @@ -6,6 +6,7 @@ shared_examples 'TokenAuthenticatable' do it { expect(described_class).to be_private_method_defined(:write_new_token) } it { expect(described_class).to respond_to("find_by_#{token_field}") } it { is_expected.to respond_to("ensure_#{token_field}") } + it { is_expected.to respond_to("set_#{token_field}") } it { is_expected.to respond_to("reset_#{token_field}!") } end end @@ -55,6 +56,12 @@ describe ApplicationSetting, 'TokenAuthenticatable' do end end + describe 'setting new token' do + subject { described_class.new.send("set_#{token_field}", '0123456789') } + + it { is_expected.to eq '0123456789' } + end + describe 'multiple token fields' do before do described_class.send(:add_authentication_token_field, :yet_another_token) -- cgit v1.2.1 From d6d22a5dbd2ee04b59b2a80b3b6228575c3ea3ec Mon Sep 17 00:00:00 2001 From: Sam Rose <samrose3@gmail.com> Date: Wed, 30 Nov 2016 08:25:25 -0500 Subject: Enable ColorVariable in scss-lint --- .scss-lint.yml | 2 +- app/assets/stylesheets/framework/avatar.scss | 2 +- app/assets/stylesheets/framework/blocks.scss | 4 +- app/assets/stylesheets/framework/buttons.scss | 22 +- app/assets/stylesheets/framework/calendar.scss | 4 +- app/assets/stylesheets/framework/callout.scss | 24 +- app/assets/stylesheets/framework/common.scss | 62 +- app/assets/stylesheets/framework/dropdowns.scss | 4 +- app/assets/stylesheets/framework/files.scss | 20 +- app/assets/stylesheets/framework/forms.scss | 12 +- app/assets/stylesheets/framework/gitlab-theme.scss | 32 +- app/assets/stylesheets/framework/header.scss | 4 +- app/assets/stylesheets/framework/issue_box.scss | 6 +- app/assets/stylesheets/framework/jquery.scss | 16 +- app/assets/stylesheets/framework/layout.scss | 2 +- app/assets/stylesheets/framework/lists.scss | 12 +- .../stylesheets/framework/markdown_area.scss | 10 +- app/assets/stylesheets/framework/mixins.scss | 6 +- app/assets/stylesheets/framework/mobile.scss | 6 +- app/assets/stylesheets/framework/nav.scss | 2 +- app/assets/stylesheets/framework/selects.scss | 16 +- app/assets/stylesheets/framework/sidebar.scss | 2 +- app/assets/stylesheets/framework/tables.scss | 4 +- app/assets/stylesheets/framework/tw_bootstrap.scss | 8 +- .../framework/tw_bootstrap_variables.scss | 18 +- app/assets/stylesheets/framework/typography.scss | 20 +- app/assets/stylesheets/framework/variables.scss | 282 +++++++- app/assets/stylesheets/framework/zen.scss | 6 +- app/assets/stylesheets/highlight/dark.scss | 230 ++++-- app/assets/stylesheets/highlight/monokai.scss | 225 ++++-- .../stylesheets/highlight/solarized_dark.scss | 243 ++++--- .../stylesheets/highlight/solarized_light.scss | 243 ++++--- app/assets/stylesheets/highlight/white.scss | 181 +++-- app/assets/stylesheets/mailers/devise.scss | 4 +- .../mailers/highlighted_diff_email.scss | 174 +++-- app/assets/stylesheets/notify.scss | 10 +- app/assets/stylesheets/pages/admin.scss | 2 +- app/assets/stylesheets/pages/awards.scss | 2 +- app/assets/stylesheets/pages/boards.scss | 6 +- app/assets/stylesheets/pages/builds.scss | 4 +- app/assets/stylesheets/pages/ci_projects.scss | 2 +- app/assets/stylesheets/pages/commit.scss | 8 +- app/assets/stylesheets/pages/commits.scss | 12 +- app/assets/stylesheets/pages/confirmation.scss | 2 +- app/assets/stylesheets/pages/cycle_analytics.scss | 4 +- app/assets/stylesheets/pages/dashboard.scss | 2 +- app/assets/stylesheets/pages/detail_page.scss | 6 +- app/assets/stylesheets/pages/diff.scss | 32 +- app/assets/stylesheets/pages/editor.scss | 4 +- app/assets/stylesheets/pages/events.scss | 9 +- app/assets/stylesheets/pages/graph.scss | 13 +- app/assets/stylesheets/pages/help.scss | 8 +- app/assets/stylesheets/pages/issuable.scss | 6 +- app/assets/stylesheets/pages/issues.scss | 8 +- app/assets/stylesheets/pages/labels.scss | 2 +- app/assets/stylesheets/pages/lint.scss | 4 +- app/assets/stylesheets/pages/login.scss | 7 +- app/assets/stylesheets/pages/merge_conflicts.scss | 8 +- app/assets/stylesheets/pages/merge_requests.scss | 4 +- app/assets/stylesheets/pages/note_form.scss | 2 +- app/assets/stylesheets/pages/notes.scss | 12 +- app/assets/stylesheets/pages/projects.scss | 32 +- app/assets/stylesheets/pages/runners.scss | 12 +- app/assets/stylesheets/pages/stat_graph.scss | 14 +- app/assets/stylesheets/pages/status.scss | 2 +- app/assets/stylesheets/pages/todos.scss | 10 +- app/assets/stylesheets/pages/ui_dev_kit.scss | 4 +- app/assets/stylesheets/pages/xterm.scss | 772 ++++++++++++++------- .../unreleased/23500-enable-colorvariable.yml | 4 + 69 files changed, 1982 insertions(+), 955 deletions(-) create mode 100644 changelogs/unreleased/23500-enable-colorvariable.yml diff --git a/.scss-lint.yml b/.scss-lint.yml index aae8d9b6dbe..83c68309fa8 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -30,7 +30,7 @@ linters: # variable declarations. They should be referred to via variables everywhere # else. ColorVariable: - enabled: false + enabled: true # Which form of comments to prefer in CSS. Comment: diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index c0dd1cb3667..000e591e09c 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -8,7 +8,7 @@ float: left; margin-right: 15px; border-radius: $avatar_radius; - border: 1px solid rgba(0, 0, 0, .1); + border: 1px solid $avatar-border; &.s16 { @include avatar-size(16px, 6px); } &.s20 { @include avatar-size(20px, 7px); } &.s24 { @include avatar-size(24px, 8px); } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 77ae9e9a6e7..57db5eaa2b3 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -41,7 +41,7 @@ } &.white { - background-color: white; + background-color: $white-light; } &.top-block { @@ -158,7 +158,7 @@ p { padding: 0 $gl-padding; - color: #5c5d5e; + color: $gl-text-color-dark; } } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 36f530af685..8da3da2ad08 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -68,23 +68,23 @@ } @mixin btn-green { - @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, #fff); + @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, $white-light); } @mixin btn-blue { - @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, #fff); + @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, $white-light); } @mixin btn-blue-medium { - @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, #fff); + @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, $white-light); } @mixin btn-orange { - @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, #fff); + @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, $white-light); } @mixin btn-red { - @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, #fff); + @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, $white-light); } @mixin btn-gray { @@ -289,8 +289,8 @@ .active { box-shadow: $gl-btn-active-background; - border: 1px solid #c6cacf !important; - background-color: #e4e7ed !important; + border: 1px solid $border-white-dark !important; + background-color: $btn-active-gray-light !important; } } @@ -345,13 +345,13 @@ .btn-static { background-color: $background-color !important; - border: 1px solid lightgrey; + border: 1px solid $border-gray-light; cursor: default; &:active { - -moz-box-shadow: inset 0 0 0 white; - -webkit-box-shadow: inset 0 0 0 white; - box-shadow: inset 0 0 0 white; + -moz-box-shadow: inset 0 0 0 $white-light; + -webkit-box-shadow: inset 0 0 0 $white-light; + box-shadow: inset 0 0 0 $white-light; } } diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index 81852158b94..ef921a8c6a9 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -28,13 +28,13 @@ .user-contrib-cell { &:hover { cursor: pointer; - stroke: #000; + stroke: $black; } } .user-contrib-text { font-size: 12px; - fill: #959494; + fill: $calendar-user-contrib-text; } .calendar-hint { diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss index f3b6ad88ad6..2a100980aca 100644 --- a/app/assets/stylesheets/framework/callout.scss +++ b/app/assets/stylesheets/framework/callout.scss @@ -25,25 +25,25 @@ /* Variations */ .bs-callout-danger { - background-color: #fdf7f7; - border-color: #eed3d7; - color: #b94a48; + background-color: $callout-danger-bg; + border-color: $callout-danger-border; + color: $callout-danger-color; } .bs-callout-warning { - background-color: #faf8f0; - border-color: #faebcc; - color: #8a6d3b; + background-color: $callout-warning-bg; + border-color: $callout-warning-border; + color: $callout-warning-color; } .bs-callout-info { - background-color: #f4f8fa; - border-color: #bce8f1; - color: #34789a; + background-color: $callout-info-bg; + border-color: $callout-info-border; + color: $callout-info-color; } .bs-callout-success { - background-color: #dff0d8; - border-color: #5ca64d; - color: #3c763d; + background-color: $callout-success-bg; + border-color: $callout-success-border; + color: $callout-success-color; } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index b24fce6f0c2..cdeef6fcc9e 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -1,9 +1,9 @@ /** COLORS **/ -.cgray { color: $gl-gray; } -.clgray { color: #bbb; } -.cred { color: $gl-text-red; } -.cgreen { color: $gl-text-green; } -.cdark { color: #444; } +.cgray { color: $common-gray; } +.clgray { color: $common-gray-light; } +.cred { color: $common-red; } +.cgreen { color: $common-green; } +.cdark { color: $common-gray-dark; } /** COMMON CLASSES **/ .prepend-top-0 { margin-top: 0; } @@ -28,11 +28,11 @@ .center { text-align: center; } .underlined-link { text-decoration: underline; } -.hint { font-style: italic; color: #999; } -.light { color: $gl-gray; } +.hint { font-style: italic; color: $hint-color; } +.light { color: $common-gray; } .slead { - color: $gl-gray; + color: $common-gray; font-size: 15px; margin-bottom: 12px; font-weight: normal; @@ -52,10 +52,10 @@ pre { } &.well-pre { - border: 1px solid #eee; + border: 1px solid $well-pre-bg; background: $gray-light; border-radius: 0; - color: #555; + color: $well-pre-color; } } @@ -87,14 +87,14 @@ table a code { .loading { margin: 20px auto; height: 40px; - color: #555; + color: $loading-color; font-size: 32px; text-align: center; } span.update-author { display: block; - color: #999; + color: $update-author-color; font-weight: normal; font-style: italic; @@ -105,7 +105,7 @@ span.update-author { } .user-mention { - color: #2fa0bb; + color: $user-mention-color; font-weight: bold; } @@ -114,7 +114,7 @@ span.update-author { } p.time { - color: #999; + color: $time-color; font-size: 90%; margin: 30px 3px 3px 2px; } @@ -150,7 +150,7 @@ li.note { .project_member_show { td:first-child { - color: #aaa; + color: $project-member-show-color; } } @@ -176,7 +176,7 @@ li.note { margin-top: 40px; pre { - background: white; + background: $white-light; border: none; font-size: 12px; } @@ -184,12 +184,12 @@ li.note { .error-message { padding: 10px; - background: #c67; + background: $error-bg; margin: 0; - color: #fff; + color: $white-light; a { - color: #fff; + color: $white-light; text-decoration: underline; } } @@ -197,22 +197,22 @@ li.note { .browser-alert { padding: 10px; text-align: center; - background: #c67; - color: #fff; + background: $error-bg; + color: $white-light; font-weight: bold; a { - color: #fff; + color: $white-light; text-decoration: underline; } } .warning_message { - border-left: 4px solid #ed9; - color: #b90; + border-left: 4px solid $warning-message-border; + color: $warning-message-color; padding: 10px; margin-bottom: 10px; - background: #ffffe6; + background: $warning-message-bg; padding-left: 20px; &.centered { @@ -222,7 +222,7 @@ li.note { .gitlab-promo { a { - color: #aaa; + color: $gl-promo-color; margin-right: 30px; } } @@ -245,7 +245,7 @@ li.note { position: relative; top: 2px; left: 5px; - color: #666; + color: $control-group-descr-color; } } } @@ -270,7 +270,7 @@ img.emoji { table { td.permission-x { - background: #d9edf7 !important; + background: $table-permission-x-bg !important; text-align: center; } } @@ -323,13 +323,13 @@ table { .username { font-size: 18px; - color: #666; + color: $username-color; margin-top: 8px; } .description { font-size: $gl-font-size; - color: #666; + color: $description-color; margin-top: 8px; } } @@ -339,7 +339,7 @@ table { .profiler-button, .profiler-controls { - border-color: #eee !important; + border-color: $profiler-border !important; } } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 6d77aadd753..e6229a35b88 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -376,7 +376,7 @@ position: absolute; top: 10px; right: 20px; - color: #c7c7c7; + color: $dropdown-input-fa-color; font-size: 12px; pointer-events: none; } @@ -529,7 +529,7 @@ .ui-datepicker-calendar { .ui-state-hover, .ui-state-active { - color: #fff; + color: $white-light; border: 0; } } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index f49d7b92a00..ab0b81f77f7 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -59,10 +59,10 @@ } .file-content { - background: #fff; + background: $white-light; &.image_file { - background: #eee; + background: $file-image-bg; text-align: center; img { @@ -84,8 +84,8 @@ } &.blob-no-preview { - background: #eee; - text-shadow: 0 1px 2px #fff; + background: $blob-bg; + text-shadow: 0 1px 2px $white-light; padding: 100px 0; } @@ -99,7 +99,7 @@ } tr { - border-bottom: 1px solid #eee; + border-bottom: 1px solid $blame-border; } td { @@ -120,7 +120,7 @@ td.line-numbers { float: none; - border-left: 1px solid #ddd; + border-left: 1px solid $blame-line-numbers-border; i { float: none; @@ -134,7 +134,7 @@ } &.logs { - background: #eee; + background: $logs-bg; max-height: 700px; overflow-y: auto; @@ -143,14 +143,14 @@ padding: 10px 0; border-left: 1px solid $border-color; margin-bottom: 0; - background: white; + background: $white-light; li { - color: #888; + color: $logs-li-color; p { margin: 0; - color: #333; + color: $logs-p-color; line-height: 24px; padding-left: 10px; } diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index a01899ccbd2..25a2b38baaa 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -7,9 +7,9 @@ input { } input[type='text'].danger { - background: #f2dede!important; - border-color: #d66; - text-shadow: 0 1px 1px #fff; + background: $input-danger-bg !important; + border-color: $input-danger-border; + text-shadow: 0 1px 1px $white-light; } .datetime-controls { @@ -159,7 +159,7 @@ label { } .input-group-addon { - background-color: #f7f8fa; + background-color: $input-group-addon-bg; } .input-group-addon:not(:first-child):not(:last-child) { @@ -181,7 +181,7 @@ label { border: 1px solid $green-normal; &:focus { - box-shadow: 0 0 0 1px $green-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 $green-normal; + box-shadow: 0 0 0 1px $green-normal inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $green-normal; border: 0 none; } } @@ -190,7 +190,7 @@ label { border: 1px solid $red-normal; &:focus { - box-shadow: 0 0 0 1px $red-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 rgba(210, 40, 82, 0.6); + box-shadow: 0 0 0 1px $red-normal inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $gl-field-focus-shadow-error; border: 0 none; } } diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index c5e5dad574d..5cd242af91d 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -85,37 +85,57 @@ } $theme-charcoal: #3d454d; +$theme-charcoal-light: #485157; $theme-charcoal-dark: #383f45; $theme-charcoal-text: #b9bbbe; +$theme-blue-light: #becde9; $theme-blue: #2980b9; +$theme-blue-dark: #1970a9; +$theme-blue-darker: #096099; + +$theme-graphite-lighter: #ccc; +$theme-graphite-light: #777; $theme-graphite: #666; +$theme-graphite-dark: #555; + +$theme-gray-light: #979797; $theme-gray: #373737; +$theme-gray-dark: #272727; +$theme-gray-darker: #222; + +$theme-green-light: #adc; $theme-green: #019875; +$theme-green-dark: #018865; +$theme-green-darker: #017855; + +$theme-violet-light: #98c; $theme-violet: #548; +$theme-violet-dark: #436; +$theme-violet-darker: #325; body { &.ui_blue { - @include gitlab-theme(#becde9, $theme-blue, #1970a9, #096099); + @include gitlab-theme($theme-blue-light, $theme-blue, $theme-blue-dark, $theme-blue-darker); } &.ui_charcoal { - @include gitlab-theme($theme-charcoal-text, #485157, $theme-charcoal, $theme-charcoal-dark); + @include gitlab-theme($theme-charcoal-text, $theme-charcoal-light, $theme-charcoal, $theme-charcoal-dark); } &.ui_graphite { - @include gitlab-theme(#ccc, #777, $theme-graphite, #555); + @include gitlab-theme($theme-graphite-lighter, $theme-graphite-light, $theme-graphite, $theme-graphite-dark); } &.ui_gray { - @include gitlab-theme(#979797, $theme-gray, #272727, #222); + @include gitlab-theme($theme-gray-light, $theme-gray, $theme-gray-dark, $theme-gray-darker); } &.ui_green { - @include gitlab-theme(#adc, $theme-green, #018865, #017855); + @include gitlab-theme($theme-green-light, $theme-green, $theme-green-dark, $theme-green-darker); } &.ui_violet { - @include gitlab-theme(#98c, $theme-violet, #436, #325); + @include gitlab-theme($theme-violet-light, $theme-violet, $theme-violet-dark, $theme-violet-darker); } } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index f9bcbbf2ca5..e40ff4d4688 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -8,7 +8,7 @@ header { &.navbar-empty { height: $header-height; - background: #fff; + background: $white-light; border-bottom: 1px solid $btn-gray-hover; .center-logo { @@ -76,7 +76,7 @@ header { } .navbar-toggle { - color: #666; + color: $nav-toggle-gray; margin: 6px 0; border-radius: 0; position: absolute; diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index ff6f316d576..44834a84234 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -20,7 +20,7 @@ display: block; float: left; margin-right: 10px; - color: #fff; + color: $white-light; font-size: $gl-font-size; line-height: 25px; @@ -37,10 +37,10 @@ } &.status-box-expired { - background: #cea61b; + background-color: $issue-status-expired; } &.status-box-upcoming { - background: #8f8f8f; + background: $issue-box-upcoming-bg; } } diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss index 30a5b837d69..18f2f316f02 100644 --- a/app/assets/stylesheets/framework/jquery.scss +++ b/app/assets/stylesheets/framework/jquery.scss @@ -4,13 +4,13 @@ &.ui-datepicker, &.ui-datepicker-inline { - border: 1px solid #ddd; + border: 1px solid $jq-ui-border; padding: 10px; width: 270px; .ui-datepicker-header { - background: #fff; - border-color: #ddd; + background: $white-light; + border-color: $jq-ui-border; .ui-datepicker-prev, .ui-datepicker-next { @@ -39,7 +39,7 @@ } &.ui-autocomplete { - border-color: #ddd; + border-color: $jq-ui-border; padding: 0; margin-top: 2px; z-index: 1001; @@ -50,9 +50,9 @@ } .ui-state-default { - border: 1px solid #fff; - background: #fff; - color: #777; + border: 1px solid $white-light; + background: $white-light; + color: $jq-ui-default-color; } .ui-state-highlight { @@ -66,7 +66,7 @@ .ui-state-focus { border: 1px solid $gl-primary; background: $gl-primary; - color: #fff; + color: $white-light; } } } diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 7baa4296abf..dfaf2f7f1d3 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -6,7 +6,7 @@ html { body { &.navless { - background-color: white !important; + background-color: $white-light !important; } } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index bc0610cc417..db8677433bb 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -11,8 +11,8 @@ > li { padding: 10px 15px; min-height: 20px; - border-bottom: 1px solid #eee; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); + border-bottom: 1px solid $list-border-light; + border-bottom: 1px solid $list-border; &::after { content: " "; @@ -21,7 +21,7 @@ } &.disabled { - color: #888; + color: $list-text-disabled-color; } &.unstyled { @@ -31,9 +31,9 @@ } &.warning-row { - background-color: #fcf8e3; - border-color: #faebcc; - color: #8a6d3b; + background-color: $list-warning-row-bg; + border-color: $list-warning-row-border; + color: $list-warning-row-color; } &.smoke { background-color: $background-color; } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 4bd7ff8fefd..59a30d31ac7 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -73,7 +73,7 @@ } .referenced-users { - color: #4c4e54; + color: $gl-header-color; padding-top: 10px; } @@ -85,8 +85,8 @@ .markdown-area { border-radius: 0; - background: #fff; - border: 1px solid #ddd; + background: $white-light; + border: 1px solid $md-area-border; min-height: 140px; max-height: 500px; padding: 5px; @@ -108,7 +108,7 @@ hr { // Darken 'whitesmoke' a bit to make it more visible in note bodies - border-color: darken(#f5f5f5, 8%); + border-color: darken($gray-normal, 8%); margin: 10px 0; } @@ -135,7 +135,7 @@ .toolbar-btn { float: left; padding: 0 5px; - color: #959494; + color: $note-toolbar-color; background: transparent; border: 0; outline: 0; diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index f84ca36d10f..4f2ac77f228 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -24,7 +24,7 @@ @include clearfix; padding: 10px 0; - border-bottom: 1px solid #eee; + border-bottom: 1px solid $list-border-light; display: block; margin: 0; @@ -67,8 +67,8 @@ } @mixin dark-diff-match-line { - color: rgba(255, 255, 255, 0.3); - background: rgba(255, 255, 255, 0.1); + color: $dark-diff-match-bg; + background: $dark-diff-match-color; } @mixin webkit-prefix($property, $value) { diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 9391661a595..abfdd7a759d 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -133,9 +133,9 @@ right: 0; top: 30%; padding: 5px 15px; - background: #eee; + background: $show-aside-bg; font-size: 20px; - color: #777; + color: $show-aside-color; z-index: 100; - box-shadow: 0 1px 2px #ddd; + box-shadow: 0 1px 2px $show-aside-shadow; } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 98f72e58c23..d2d3fc23b6c 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -75,7 +75,7 @@ .badge { font-weight: normal; - background-color: #eee; + background-color: $nav-badge-bg; color: $btn-transparent-color; vertical-align: baseline; } diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 920ce249b9a..fde1431b13e 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -6,7 +6,7 @@ .select2-container, .select2-container.select2-drop-above { .select2-choice { - background: #fff; + background: $white-light; border-color: $input-border; height: 35px; padding: $gl-vert-padding $gl-input-padding; @@ -47,7 +47,7 @@ } .select2-drop { - box-shadow: rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0; + box-shadow: $select2-drop-shadow1 0 0 1px 0, $select2-drop-shadow2 0 2px 18px 0; border-radius: $border-radius-default; border: none; min-width: 175px; @@ -59,7 +59,7 @@ } .select2-drop { - color: #7f8fa4; + color: $gl-grayish-blue; } .select2-highlighted { @@ -156,7 +156,7 @@ .select2-search input { padding: 2px 25px 2px 5px; - background: #fff image-url('select2.png'); + background: $white-light image-url('select2.png'); background-repeat: no-repeat; background-position: right 0 bottom 6px; border: 1px solid $input-border; @@ -169,7 +169,7 @@ } .select2-search input.select2-active { - background-color: #fff; + background-color: $white-light; background-image: image-url('select2-spinner.gif') !important; background-repeat: no-repeat; background-position: right 5px center !important; @@ -206,7 +206,7 @@ .select2-highlighted { .group-result { .group-path { - color: #fff; + color: $white-light; } } } @@ -221,7 +221,7 @@ } .group-path { - color: #999; + color: $group-path-color; } } @@ -241,7 +241,7 @@ .namespace-result { .namespace-kind { - color: #aaa; + color: $namespace-kind-color; font-weight: normal; } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 4269d365578..0aa609b8dd5 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -36,7 +36,7 @@ transition: padding $sidebar-transition-duration; .container-fluid { - background: #fff; + background: $white-light; padding: 0 $gl-padding; &.container-blank { diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index 9a90d3794fd..a5f36c177fc 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -14,11 +14,11 @@ table { .warning, .danger, .info { - color: #fff; + color: $white-light; a:not(.btn) { text-decoration: underline; - color: #fff; + color: $white-light; } } diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index 59f4594bb83..55bc325b858 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -97,13 +97,13 @@ display: inline-block; &.label-gray { - background-color: #f8fafc; + background-color: $label-gray-bg; color: $gl-gray; text-shadow: none; } &.label-inverse { - background-color: #333; + background-color: $label-inverse-bg; } } @@ -158,7 +158,7 @@ font-weight: normal; a { - color: #777; + color: $panel-heading-link-color; } } } @@ -172,7 +172,7 @@ .alert { a:not(.btn) { @extend .alert-link; - color: #fff; + color: $white-light; text-decoration: underline; } } diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index 44fe37d3a4a..c731a8f222f 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -48,7 +48,7 @@ $font-size-base: $gl-font-size; $padding-base-vertical: $gl-vert-padding; $padding-base-horizontal: $gl-padding; -$component-active-color: #fff; +$component-active-color: $white-light; $component-active-bg: $brand-info; //== Forms @@ -66,7 +66,7 @@ $legend-color: $text-color; //## $pagination-color: $gl-gray; -$pagination-bg: #fff; +$pagination-bg: $white-light; $pagination-border: $border-color; $pagination-hover-color: $gl-gray; @@ -74,7 +74,7 @@ $pagination-hover-bg: $row-hover; $pagination-hover-border: $border-color; $pagination-active-color: $blue-dark; -$pagination-active-bg: #fff; +$pagination-active-bg: $white-light; $pagination-active-border: $border-color; $pagination-disabled-color: #cdcdcd; @@ -86,19 +86,19 @@ $pagination-disabled-border: $border-color; // //## Define colors for form feedback states and, by default, alerts. -$state-success-text: #fff; +$state-success-text: $white-light; $state-success-bg: $brand-success; $state-success-border: $brand-success; -$state-info-text: #fff; +$state-info-text: $white-light; $state-info-bg: $brand-info; $state-info-border: $brand-info; -$state-warning-text: #fff; +$state-warning-text: $white-light; $state-warning-bg: $brand-warning; $state-warning-border: $brand-warning; -$state-danger-text: #fff; +$state-danger-text: $white-light; $state-danger-bg: $brand-danger; $state-danger-border: $brand-danger; @@ -135,14 +135,14 @@ $well-border: #eee; $code-color: #c7254e; $code-bg: #f9f2f4; -$kbd-color: #fff; +$kbd-color: $white-light; $kbd-bg: #333; //== Buttons // //## $btn-default-color: $gl-text-color; -$btn-default-bg: #fff; +$btn-default-bg: $white-light; $btn-default-border: #e7e9ed; //== Nav diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index aa604b1cd19..d906d26bba9 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -33,15 +33,15 @@ padding: 3px 5px; font-size: 11px; line-height: 10px; - color: #555; + color: $kdb-color; vertical-align: middle; - background-color: #fcfcfc; + background-color: $kdb-bg; border-width: 1px; border-style: solid; - border-color: #ccc #ccc #bbb; + border-color: $kdb-border $kdb-border $kdb-border-bottom; border-image: none; border-radius: 3px; - box-shadow: 0 -1px 0 #bbb inset; + box-shadow: 0 -1px 0 $kdb-shadow inset; } h1 { @@ -81,7 +81,7 @@ } blockquote { - color: #7f8fa4; + color: $gl-grayish-blue; font-size: inherit; padding: 8px 21px; margin: 12px 0; @@ -94,13 +94,13 @@ } blockquote p { - color: #7f8fa4 !important; + color: $gl-grayish-blue !important; font-size: inherit; line-height: 1.5; } p { - color: #5c5d5e; + color: $gl-text-color-dark; margin: 6px 0 0; } @@ -108,10 +108,10 @@ @extend .table; @extend .table-bordered; margin: 12px 0; - color: #5c5d5e; + color: $gl-text-color-dark; th { - background: #f8fafc; + background: $label-gray-bg; } } @@ -202,7 +202,7 @@ * */ body { - -webkit-text-shadow: rgba(255,255,255,0.01) 0 0 1px; + -webkit-text-shadow: $body-text-shadow 0 0 1px; } .page-title { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 8a9c279d124..cca5cf5b6b4 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -96,6 +96,8 @@ $dark-background-color: #f5f5f5; $table-text-gray: #8f8f8f; $well-expand-item: #e8f2f7; $well-inner-border: #eef0f2; +$well-light-border: #f1f1f1; +$well-light-text-color: #5b6169; /* * Text @@ -103,11 +105,13 @@ $well-inner-border: #eef0f2; $gl-font-size: 15px; $gl-title-color: #333; $gl-text-color: #5c5c5c; +$gl-text-color-dark: #5c5d5e; $gl-text-color-light: #8c8c8c; $gl-text-green: #4a2; $gl-text-red: #d12f19; $gl-text-orange: #d90; $gl-link-color: #3777b0; +$gl-diff-text-color: #555; $gl-dark-link-color: #333; $gl-placeholder-color: #8f8f8f; $gl-icon-color: $gl-placeholder-color; @@ -123,13 +127,20 @@ $gl-header-color: #4c4e54; $list-font-size: $gl-font-size; $list-title-color: $gl-title-color; $list-text-color: $gl-text-color; +$list-text-disabled-color: #888; +$list-border-light: #eee; +$list-border: rgba(0, 0, 0, 0.05); $list-text-height: 42px; +$list-warning-row-bg: #fcf8e3; +$list-warning-row-border: #faebcc; +$list-warning-row-color: #8a6d3b; /* * Markdown */ $md-text-color: $gl-text-color; $md-link-color: $gl-link-color; +$md-area-border: #ddd; /* * Code @@ -153,10 +164,8 @@ $gl-sidebar-padding: 22px; $row-hover: #f7faff; $row-hover-border: #b2d7ff; $progress-color: #c0392b; -$avatar_radius: 50%; $header-height: 50px; $fixed-layout-width: 1280px; -$gl-avatar-size: 40px; $error-exclamation-point: #e62958; $border-radius-default: 2px; $btn-transparent-color: #8f8f8f; @@ -166,10 +175,47 @@ $provider-btn-not-active-color: #4688f1; $link-underline-blue: #4a8bee; $active-item-blue: #4a8bee; $layout-link-gray: #7e7c7c; -$todo-alert-blue: #428bca; $btn-side-margin: 10px; $btn-sm-side-margin: 7px; $btn-xs-side-margin: 5px; +$issue-status-expired: #cea61b; +$issuable-sidebar-color: #999; +$issuable-avatar-hover-border: #999; +$issuable-clipboard-color: #999; +$show-aside-bg: #eee; +$show-aside-color: #777; +$show-aside-shadow: #ddd; +$group-path-color: #999; +$namespace-kind-color: #aaa; +$panel-heading-link-color: #777; +$graph-author-email-color: #777; +$count-arrow-border: #dce0e5; +$save-project-loader-color: #555; +$divergence-graph-bar-bg: #ccc; +$divergence-graph-separator-bg: #ccc; +$issue-box-upcoming-bg: #8f8f8f; + +/* +* Common component specific colors +*/ +$hint-color: #999; +$well-pre-bg: #eee; +$well-pre-color: #555; +$loading-color: #555; +$update-author-color: #999; +$user-mention-color: #2fa0bb; +$time-color: #999; +$project-member-show-color: #aaa; +$gl-promo-color: #aaa; +$error-bg: #c67; +$warning-message-bg: #ffffe6; +$warning-message-border: #ed9; +$warning-message-color: #b90; +$control-group-descr-color: #666; +$table-permission-x-bg: #d9edf7; +$username-color: #666; +$description-color: #666; +$profiler-border: #eee; /* tanuki logo colors */ $tanuki-red: #e24329; @@ -205,6 +251,16 @@ $table-border-gray: #f0f0f0; $line-target-blue: #f6faff; $line-select-yellow: #fcf8e7; $line-select-yellow-dark: #f0e2bd; +$dark-diff-match-bg: rgba(255, 255, 255, 0.3); +$dark-diff-match-color: rgba(255, 255, 255, 0.1); +$file-mode-changed: #777; +$file-mode-changed: #777; +$diff-image-bg: #ddd; +$diff-image-info-color: grey; +$diff-image-img-bg: #e5e5e5; +$diff-swipe-border: #999; +$diff-view-modes-color: grey; +$diff-view-modes-border: #c1c1c1; /* * Fonts @@ -226,6 +282,7 @@ $dropdown-divider-color: rgba(#000, .1); $dropdown-header-color: #959494; $dropdown-title-btn-color: #bfbfbf; $dropdown-input-color: #555; +$dropdown-input-fa-color: #c7c7c7; $dropdown-input-focus-border: $focus-border-color; $dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, .4); $dropdown-loading-bg: rgba(#fff, .6); @@ -243,6 +300,7 @@ $dropdown-toggle-hover-icon-color: darken($dropdown-toggle-icon-color, 7%); * Buttons */ $btn-active-gray: #ececec; +$btn-active-gray-light: e4e7ed; $btn-placeholder-gray: #c7c7c7; $btn-white-active: #848484; $btn-gray-hover: #eee; @@ -252,6 +310,7 @@ $btn-gray-hover: #eee; */ $award-emoji-menu-bg: #fff; $award-emoji-menu-border: #f1f2f4; +$award-emoji-menu-shadow: rgba(0,0,0,.175); $award-emoji-new-btn-icon-color: #dcdcdc; /* @@ -273,17 +332,28 @@ $notes-light-color: #8e8e8e; $notes-action-color: #c3c3c3; $notes-role-color: #8e8e8e; $notes-role-border-color: #e4e4e4; - $note-disabled-comment-color: #b2b2b2; $note-form-border-color: #e5e5e5; $note-toolbar-color: #959494; +$note-targe3-outside: #fffff0; +$note-targe3-inside: #ffffd3; +$note-line2-border: #ddd; + +/* +* Zen +*/ +$zen-control-color: #555; $zen-control-hover-color: #111; +/* +* Calendar +*/ $calendar-header-color: #b8b8b8; $calendar-hover-bg: #ecf3fe; $calendar-border-color: rgba(#000, .1); $calendar-unselectable-bg: $gray-light; +$calendar-user-contrib-text: #959494; /* * Cycle Analytics @@ -293,13 +363,217 @@ $cycle-analytics-box-text-color: #8c8c8c; $cycle-analytics-big-font: 19px; $cycle-analytics-dark-text: $gl-title-color; $cycle-analytics-light-gray: #bfbfbf; +$cycle-analytics-dismiss-icon-color: #b2b2b2; /* * Personal Access Tokens */ $personal-access-tokens-disabled-label-color: #bbb; +/* +* CI +*/ $ci-output-bg: #1d1f21; $ci-text-color: #c5c8c6; +$ci-skipped-color: #888; +/* +* Boards +*/ $issue-boards-font-size: 15px; +$issue-boards-card-shadow: rgba(186, 186, 186, 0.5); + +/* +* Avatar +*/ +$avatar_radius: 50%; +$avatar-border: rgba(0, 0, 0, .1); +$gl-avatar-size: 40px; + +/* +* Builds +*/ +$builds-trace-bg: #111; + +/* +* Callout +*/ +$callout-danger-bg: #fdf7f7; +$callout-danger-border: #eed3d7; +$callout-danger-color: #b94a48; +$callout-warning-bg: #faf8f0; +$callout-warning-border: #faebcc; +$callout-warning-color: #8a6d3b; +$callout-info-bg: #f4f8fa; +$callout-info-border: #bce8f1; +$callout-info-color: #34789a; +$callout-success-bg: #dff0d8; +$callout-success-border: #5ca64d; +$callout-success-color: #3c763d; + +/* +* Commit Page +*/ +$commit-committer-color: #999; +$commit-max-width-marker-color: rgba(0, 0, 0, 0.0); +$commit-message-text-area-bg: rgba(0, 0, 0, 0.0); + +/* +* Common +*/ +$common-gray: $gl-gray; +$common-gray-light: #bbb; +$common-gray-dark: #444; +$common-red: $gl-text-red; +$common-green: $gl-text-green; + + +/* +* Dashboard +*/ +$dashboard-project-access-icon-color: #888; + +/* +* Editor +*/ +$editor-cancel-color: #b94a48; + +/* +* Events +*/ +$events-pre-color: #777; +$events-note-icon-color: #777; +$events-body-border: #ddd; + +/* +* Files +*/ +$file-image-bg: #eee; +$blob-bg: #eee; +$blame-border: #eee; +$blame-line-numbers-border: #ddd; +$logs-bg: #eee; +$logs-li-color: #888; +$logs-p-color: #333; + +/* +* Forms +*/ +$input-danger-bg: #f2dede; +$input-danger-border: #d66; +$input-group-addon-bg: #f7f8fa; +$gl-field-focus-shadow: rgba(0, 0, 0, 0.075); +$gl-field-focus-shadow-error: rgba(210, 40, 82, 0.6); + +/* +* Help +*/ +$document-index-color: #888; +$help-shortcut-color: #999; +$help-shortcut-mapping-color: #555; +$help-shortcut-header-color: #333; + +/* +* Issues +*/ +$issues-border: #e5e5e5; +$issues-today-bg: #f3fff2; +$issues-today-border: #e1e8d5; + +/* +* jQuery UI +*/ +$jq-ui-border: #ddd; +$jq-ui-default-color: #777; + +/* +* Label +*/ +$label-gray-bg: #f8fafc; +$label-inverse-bg: #333; +$label-remove-border: rgba(0, 0, 0, .1); + +/* +* Lint +*/ +$lint-incorrect-color: red; +$lint-correct-color: #47a447; + +/* +* Login +*/ +$login-brand-holder-color: #888; +$login-devise-error-color: #a00; + +/* +* Nav +*/ +$nav-link-gray: #959494; +$nav-badge-bg: #eee; +$nav-toggle-gray: #666; + +/* +* Notify +*/ +$notify-details: #777; +$notify-footer: #777; +$notify-new-file: #090; +$notify-deleted-file: #b00; + +/* +* Projects +*/ +$project-option-descr-color: #54565b; +$project-breadcrumb-color: #999; +$project-private-forks-notice-odd: #2aa056; +$project-network-controls-color: #888; +$project-limit-message-bg: #f28d35; + +/* +* Runners +*/ +$runner-state-shared-bg: #32b186; +$runner-state-specific-bg: #3498db; +$runner-status-online-color: green; +$runner-status-offline-color: gray; +$runner-status-paused-color: red; + +/* +Stat Graph +*/ +$stat-graph-common-bg: #f3f3f3; +$stat-graph-area-fill: #1db34f; +$stat-graph-axis-fill: #aaa; +$stat-graph-orange-fill: #f17f49; +$stat-graph-selection-fill: #333; +$stat-graph-selection-stroke: #333; + +/* +* Selects +*/ +$select2-drop-shadow1: rgba(76, 86, 103, 0.247059); +$select2-drop-shadow2: rgba(31, 37, 50, 0.317647); + + +/* +* Todo +*/ +$todo-alert-blue: #428bca; +$todo-body-pre-color: #777; +$todo-body-border: #ddd; + +/* +* Typography +*/ +$kdb-bg: #fcfcfc; +$kdb-color: #555; +$kdb-border: #ccc; +$kdb-border-bottom: #bbb; +$kdb-shadow: #bbb; +$body-text-shadow: rgba(255,255,255,0.01); + +/* +* UI Dev Kit +*/ +$ui-dev-kit-example-color: #bbb; +$ui-dev-kit-example-border: #ddd; diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index ff02ebdd34c..e5c7d70d45a 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -1,6 +1,6 @@ .zen-backdrop { &.fullscreen { - background-color: white; + background-color: $white-light; position: fixed; top: 0; bottom: 0; @@ -12,7 +12,7 @@ border: none; box-shadow: none; border-radius: 0; - color: #000; + color: $black; font-size: 20px; line-height: 26px; padding: 30px; @@ -34,7 +34,7 @@ .zen-control { padding: 0; - color: #555; + color: $zen-control-color; background: none; border: 0; } diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index d22d9b01495..cb923166b25 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -1,27 +1,109 @@ /* https://github.com/MozMorris/tomorrow-pygments */ + +/* +* Dark syntax colors +*/ +$dark-new-bg: rgba(51, 255, 51, 0.1); +$dark-new-idiff: rgba(51, 255, 51, 0.2); +$dark-old-bg: rgba(255, 51, 51, 0.2); +$dark-old-idiff: rgba(255, 51, 51, 0.25); +$dark-border: #808080; +$dark-code-border: #666; +$dark-main-bg: #1d1f21; +$dark-main-color: #1d1f21; +$dark-line-color: #c5c8c6; +$dark-line-num-color: rgba(255, 255, 255, 0.3); +$dark-diff-not-empty-bg: #557; +$dark-highlight-bg: #ffe792; +$dark-highlight-color: $black; +$dark-pre-hll-bg: #373b41; +$dark-hll-bg: #373b41; +$dark-c: #969896; +$dark-err: #c66; +$dark-k: #b294bb; +$dark-l: #de935f; +$dark-n: #c5c8c6; +$dark-o: #8abeb7; +$dark-p: #c5c8c6; +$dark-cm: #969896; +$dark-cp: #969896; +$dark-c1: #969896; +$dark-cs: #969896; +$dark-gd: #c66; +$dark-gh: #c5c8c6; +$dark-gi: #b5bd68; +$dark-gp: #969896; +$dark-gu: #8abeb7; +$dark-kc: #b294bb; +$dark-kd: #b294bb; +$dark-kn: #8abeb7; +$dark-kp: #b294bb; +$dark-kr: #b294bb; +$dark-kt: #f0c674; +$dark-ld: #b5bd68; +$dark-m: #de935f; +$dark-s: #b5bd68; +$dark-na: #81a2be; +$dark-nb: #c5c8c6; +$dark-nc: #f0c674; +$dark-no: #c66; +$dark-nd: #8abeb7; +$dark-ni: #c5c8c6; +$dark-ne: #c66; +$dark-nf: #81a2be; +$dark-nl: #c5c8c6; +$dark-nn: #f0c674; +$dark-nx: #81a2be; +$dark-py: #c5c8c6; +$dark-nt: #8abeb7; +$dark-nv: #c66; +$dark-ow: #8abeb7; +$dark-w: #c5c8c6; +$dark-mf: #de935f; +$dark-mh: #de935f; +$dark-mi: #de935f; +$dark-mo: #de935f; +$dark-sb: #b5bd68; +$dark-sc: #c5c8c6; +$dark-sd: #969896; +$dark-s2: #b5bd68; +$dark-se: #de935f; +$dark-sh: #b5bd68; +$dark-si: #de935f; +$dark-sx: #b5bd68; +$dark-sr: #b5bd68; +$dark-s1: #b5bd68; +$dark-ss: #b5bd68; +$dark-bp: #c5c8c6; +$dark-vc: #c66; +$dark-vg: #c66; +$dark-vi: #c66; +$dark-il: #de935f; + .code.dark { // Line numbers .line-numbers, .diff-line-num { - background-color: #1d1f21; + background-color: $dark-main-bg; } .diff-line-num, .diff-line-num a { - color: rgba(255, 255, 255, 0.3); + color: $dark-main-color; + color: $dark-line-num-color; } // Code itself pre.code, .diff-line-num { - border-color: #666; + border-color: $dark-code-border; } &, pre.code, .line_holder .line_content { - background-color: #1d1f21; - color: #c5c8c6; + background-color: $dark-main-bg; + color: $dark-line-color; } // Diff line @@ -32,18 +114,18 @@ td.diff-line-num.hll:not(.empty-cell), td.line_content.hll:not(.empty-cell) { - background-color: #557; - border-color: darken(#557, 15%); + background-color: $dark-diff-not-empty-bg; + border-color: darken($dark-diff-not-empty-bg, 15%); } .diff-line-num.new, .line_content.new { - @include diff_background(rgba(51, 255, 51, 0.1), rgba(51, 255, 51, 0.2), #808080); + @include diff_background($dark-new-bg, $dark-new-idiff, $dark-border); } .diff-line-num.old, .line_content.old { - @include diff_background(rgba(255, 51, 51, 0.2), rgba(255, 51, 51, 0.25), #808080); + @include diff_background($dark-old-bg, $dark-old-idiff, $dark-border); } .line_content.match { @@ -53,77 +135,77 @@ // highlight line via anchor pre .hll { - background-color: #557 !important; + background-color: $dark-pre-hll-bg !important; } // Search result highlight span.highlight_word { - background-color: #ffe792 !important; - color: #000 !important; + background-color: $dark-highlight-bg !important; + color: $dark-highlight-color !important; } - .hll { background-color: #373b41; } - .c { color: #969896; } /* Comment */ - .err { color: #c66; } /* Error */ - .k { color: #b294bb; } /* Keyword */ - .l { color: #de935f; } /* Literal */ - .n { color: #c5c8c6; } /* Name */ - .o { color: #8abeb7; } /* Operator */ - .p { color: #c5c8c6; } /* Punctuation */ - .cm { color: #969896; } /* Comment.Multiline */ - .cp { color: #969896; } /* Comment.Preproc */ - .c1 { color: #969896; } /* Comment.Single */ - .cs { color: #969896; } /* Comment.Special */ - .gd { color: #c66; } /* Generic.Deleted */ + .hll { background-color: $dark-hll-bg; } + .c { color: $dark-c; } /* Comment */ + .err { color: $dark-err; } /* Error */ + .k { color: $dark-k; } /* Keyword */ + .l { color: $dark-l; } /* Literal */ + .n { color: $dark-n; } /* Name */ + .o { color: $dark-o; } /* Operator */ + .p { color: $dark-p; } /* Punctuation */ + .cm { color: $dark-cm; } /* Comment.Multiline */ + .cp { color: $dark-cp; } /* Comment.Preproc */ + .c1 { color: $dark-c1; } /* Comment.Single */ + .cs { color: $dark-cs; } /* Comment.Special */ + .gd { color: $dark-gd; } /* Generic.Deleted */ .ge { font-style: italic; } /* Generic.Emph */ - .gh { color: #c5c8c6; font-weight: bold; } /* Generic.Heading */ - .gi { color: #b5bd68; } /* Generic.Inserted */ - .gp { color: #969896; font-weight: bold; } /* Generic.Prompt */ + .gh { color: $dark-gh; font-weight: bold; } /* Generic.Heading */ + .gi { color: $dark-gi; } /* Generic.Inserted */ + .gp { color: $dark-gp; font-weight: bold; } /* Generic.Prompt */ .gs { font-weight: bold; } /* Generic.Strong */ - .gu { color: #8abeb7; font-weight: bold; } /* Generic.Subheading */ - .kc { color: #b294bb; } /* Keyword.Constant */ - .kd { color: #b294bb; } /* Keyword.Declaration */ - .kn { color: #8abeb7; } /* Keyword.Namespace */ - .kp { color: #b294bb; } /* Keyword.Pseudo */ - .kr { color: #b294bb; } /* Keyword.Reserved */ - .kt { color: #f0c674; } /* Keyword.Type */ - .ld { color: #b5bd68; } /* Literal.Date */ - .m { color: #de935f; } /* Literal.Number */ - .s { color: #b5bd68; } /* Literal.String */ - .na { color: #81a2be; } /* Name.Attribute */ - .nb { color: #c5c8c6; } /* Name.Builtin */ - .nc { color: #f0c674; } /* Name.Class */ - .no { color: #c66; } /* Name.Constant */ - .nd { color: #8abeb7; } /* Name.Decorator */ - .ni { color: #c5c8c6; } /* Name.Entity */ - .ne { color: #c66; } /* Name.Exception */ - .nf { color: #81a2be; } /* Name.Function */ - .nl { color: #c5c8c6; } /* Name.Label */ - .nn { color: #f0c674; } /* Name.Namespace */ - .nx { color: #81a2be; } /* Name.Other */ - .py { color: #c5c8c6; } /* Name.Property */ - .nt { color: #8abeb7; } /* Name.Tag */ - .nv { color: #c66; } /* Name.Variable */ - .ow { color: #8abeb7; } /* Operator.Word */ - .w { color: #c5c8c6; } /* Text.Whitespace */ - .mf { color: #de935f; } /* Literal.Number.Float */ - .mh { color: #de935f; } /* Literal.Number.Hex */ - .mi { color: #de935f; } /* Literal.Number.Integer */ - .mo { color: #de935f; } /* Literal.Number.Oct */ - .sb { color: #b5bd68; } /* Literal.String.Backtick */ - .sc { color: #c5c8c6; } /* Literal.String.Char */ - .sd { color: #969896; } /* Literal.String.Doc */ - .s2 { color: #b5bd68; } /* Literal.String.Double */ - .se { color: #de935f; } /* Literal.String.Escape */ - .sh { color: #b5bd68; } /* Literal.String.Heredoc */ - .si { color: #de935f; } /* Literal.String.Interpol */ - .sx { color: #b5bd68; } /* Literal.String.Other */ - .sr { color: #b5bd68; } /* Literal.String.Regex */ - .s1 { color: #b5bd68; } /* Literal.String.Single */ - .ss { color: #b5bd68; } /* Literal.String.Symbol */ - .bp { color: #c5c8c6; } /* Name.Builtin.Pseudo */ - .vc { color: #c66; } /* Name.Variable.Class */ - .vg { color: #c66; } /* Name.Variable.Global */ - .vi { color: #c66; } /* Name.Variable.Instance */ - .il { color: #de935f; } /* Literal.Number.Integer.Long */ + .gu { color: $dark-gu; font-weight: bold; } /* Generic.Subheading */ + .kc { color: $dark-kc; } /* Keyword.Constant */ + .kd { color: $dark-kd; } /* Keyword.Declaration */ + .kn { color: $dark-kn; } /* Keyword.Namespace */ + .kp { color: $dark-kp; } /* Keyword.Pseudo */ + .kr { color: $dark-kr; } /* Keyword.Reserved */ + .kt { color: $dark-kt; } /* Keyword.Type */ + .ld { color: $dark-ld; } /* Literal.Date */ + .m { color: $dark-m; } /* Literal.Number */ + .s { color: $dark-s; } /* Literal.String */ + .na { color: $dark-na; } /* Name.Attribute */ + .nb { color: $dark-nb; } /* Name.Builtin */ + .nc { color: $dark-nc; } /* Name.Class */ + .no { color: $dark-no; } /* Name.Constant */ + .nd { color: $dark-nd; } /* Name.Decorator */ + .ni { color: $dark-ni; } /* Name.Entity */ + .ne { color: $dark-ne; } /* Name.Exception */ + .nf { color: $dark-nf; } /* Name.Function */ + .nl { color: $dark-nl; } /* Name.Label */ + .nn { color: $dark-nn; } /* Name.Namespace */ + .nx { color: $dark-nx; } /* Name.Other */ + .py { color: $dark-py; } /* Name.Property */ + .nt { color: $dark-nt; } /* Name.Tag */ + .nv { color: $dark-nv; } /* Name.Variable */ + .ow { color: $dark-ow; } /* Operator.Word */ + .w { color: $dark-w; } /* Text.Whitespace */ + .mf { color: $dark-mf; } /* Literal.Number.Float */ + .mh { color: $dark-mh; } /* Literal.Number.Hex */ + .mi { color: $dark-mi; } /* Literal.Number.Integer */ + .mo { color: $dark-mo; } /* Literal.Number.Oct */ + .sb { color: $dark-sb; } /* Literal.String.Backtick */ + .sc { color: $dark-sc; } /* Literal.String.Char */ + .sd { color: $dark-sd; } /* Literal.String.Doc */ + .s2 { color: $dark-s2; } /* Literal.String.Double */ + .se { color: $dark-se; } /* Literal.String.Escape */ + .sh { color: $dark-sh; } /* Literal.String.Heredoc */ + .si { color: $dark-si; } /* Literal.String.Interpol */ + .sx { color: $dark-sx; } /* Literal.String.Other */ + .sr { color: $dark-sr; } /* Literal.String.Regex */ + .s1 { color: $dark-s1; } /* Literal.String.Single */ + .ss { color: $dark-ss; } /* Literal.String.Symbol */ + .bp { color: $dark-bp; } /* Name.Builtin.Pseudo */ + .vc { color: $dark-vc; } /* Name.Variable.Class */ + .vg { color: $dark-vg; } /* Name.Variable.Global */ + .vi { color: $dark-vi; } /* Name.Variable.Instance */ + .il { color: $dark-il; } /* Literal.Number.Integer.Long */ } diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index db8da8aab10..d8510baad8a 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -1,27 +1,108 @@ /* https://github.com/richleland/pygments-css/blob/master/monokai.css */ + +/* +* Monokai Colors +*/ +$monokai-bg: #272822; +$monokai-border: #555; +$monokai-text-color: #f8f8f2; +$monokai-line-num-color: rgba(255, 255, 255, 0.3); +$monokai-line-empty-bg: #49483e; +$monokai-line-empty-border: darken($monokai-line-empty-bg, 15%); +$monokai-diff-border: #808080; +$monokai-highlight-bg: #ffe792; + +$monokai-new-bg: rgba(166, 226, 46, 0.1); +$monokai-new-idiff: rgba(166, 226, 46, 0.15); + +$monokai-old-bg: rgba(254, 147, 140, 0.15); +$monokai-old-idiff: rgba(254, 147, 140, 0.2); + +$monokai-hll: #49483e; +$monokai-c: #75715e; +$monokai-err-color: #960050; +$monokai-err-bg: #1e0010; +$monokai-k: #66d9ef; +$monokai-l: #ae81ff; +$monokai-n: #f8f8f2; +$monokai-o: #f92672; +$monokai-p: #f8f8f2; +$monokai-cm: #75715e; +$monokai-cp: #75715e; +$monokai-c1: #75715e; +$monokai-cs: #75715e; +$monokai-kc: #66d9ef; +$monokai-kd: #66d9ef; +$monokai-kn: #f92672; +$monokai-kp: #66d9ef; +$monokai-kr: #66d9ef; +$monokai-kt: #66d9ef; +$monokai-ld: #e6db74; +$monokai-m: #ae81ff; +$monokai-s: #e6db74; +$monokai-na: #a6e22e; +$monokai-nb: #f8f8f2; +$monokai-nc: #a6e22e; +$monokai-no: #66d9ef; +$monokai-nd: #a6e22e; +$monokai-ni: #f8f8f2; +$monokai-ne: #a6e22e; +$monokai-nf: #a6e22e; +$monokai-nl: #f8f8f2; +$monokai-nn: #f8f8f2; +$monokai-nx: #a6e22e; +$monokai-py: #f8f8f2; +$monokai-nt: #f92672; +$monokai-nv: #f8f8f2; +$monokai-ow: #f92672; +$monokai-w: #f8f8f2; +$monokai-mf: #ae81ff; +$monokai-mh: #ae81ff; +$monokai-mi: #ae81ff; +$monokai-mo: #ae81ff; +$monokai-sb: #e6db74; +$monokai-sc: #e6db74; +$monokai-sd: #e6db74; +$monokai-s2: #e6db74; +$monokai-se: #ae81ff; +$monokai-sh: #e6db74; +$monokai-si: #e6db74; +$monokai-sx: #e6db74; +$monokai-sr: #e6db74; +$monokai-s1: #e6db74; +$monokai-ss: #e6db74; +$monokai-bp: #f8f8f2; +$monokai-vc: #f8f8f2; +$monokai-vg: #f8f8f2; +$monokai-vi: #f8f8f2; +$monokai-il: #ae81ff; +$monokai-gu: #75715e; +$monokai-gd: #f92672; +$monokai-gi: #a6e22e; + .code.monokai { // Line numbers .line-numbers, .diff-line-num { - background-color: #272822; + background-color: $monokai-bg; } .diff-line-num, .diff-line-num a { - color: rgba(255, 255, 255, 0.3); + color: $monokai-line-num-color; } // Code itself pre.code, .diff-line-num { - border-color: #555; + border-color: $monokai-border; } &, pre.code, .line_holder .line_content { - background-color: #272822; - color: #f8f8f2; + background-color: $monokai-bg; + color: $monokai-text-color; } // Diff line @@ -32,18 +113,18 @@ td.diff-line-num.hll:not(.empty-cell), td.line_content.hll:not(.empty-cell) { - background-color: #49483e; - border-color: darken(#49483e, 15%); + background-color: $monokai-line-empty-bg; + border-color: $monokai-line-empty-border; } .diff-line-num.new, .line_content.new { - @include diff_background(rgba(166, 226, 46, 0.1), rgba(166, 226, 46, 0.15), #808080); + @include diff_background($monokai-new-bg, $monokai-new-idiff, $monokai-diff-border); } .diff-line-num.old, .line_content.old { - @include diff_background(rgba(254, 147, 140, 0.15), rgba(254, 147, 140, 0.2), #808080); + @include diff_background($monokai-old-bg, $monokai-old-idiff, $monokai-diff-border); } .line_content.match { @@ -53,75 +134,75 @@ // highlight line via anchor pre .hll { - background-color: #49483e !important; + background-color: $monokai-hll !important; } // Search result highlight span.highlight_word { - background-color: #ffe792 !important; - color: #000 !important; + background-color: $monokai-highlight-bg !important; + color: $black !important; } - .hll { background-color: #49483e; } - .c { color: #75715e; } /* Comment */ - .err { color: #960050; background-color: #1e0010; } /* Error */ - .k { color: #66d9ef; } /* Keyword */ - .l { color: #ae81ff; } /* Literal */ - .n { color: #f8f8f2; } /* Name */ - .o { color: #f92672; } /* Operator */ - .p { color: #f8f8f2; } /* Punctuation */ - .cm { color: #75715e; } /* Comment.Multiline */ - .cp { color: #75715e; } /* Comment.Preproc */ - .c1 { color: #75715e; } /* Comment.Single */ - .cs { color: #75715e; } /* Comment.Special */ + .hll { background-color: $monokai-hll; } + .c { color: $monokai-c; } /* Comment */ + .err { color: $monokai-err-color; background-color: $monokai-err-bg; } /* Error */ + .k { color: $monokai-k; } /* Keyword */ + .l { color: $monokai-l; } /* Literal */ + .n { color: $monokai-n; } /* Name */ + .o { color: $monokai-o; } /* Operator */ + .p { color: $monokai-p; } /* Punctuation */ + .cm { color: $monokai-cm; } /* Comment.Multiline */ + .cp { color: $monokai-cp; } /* Comment.Preproc */ + .c1 { color: $monokai-c1; } /* Comment.Single */ + .cs { color: $monokai-cs; } /* Comment.Special */ .ge { font-style: italic; } /* Generic.Emph */ .gs { font-weight: bold; } /* Generic.Strong */ - .kc { color: #66d9ef; } /* Keyword.Constant */ - .kd { color: #66d9ef; } /* Keyword.Declaration */ - .kn { color: #f92672; } /* Keyword.Namespace */ - .kp { color: #66d9ef; } /* Keyword.Pseudo */ - .kr { color: #66d9ef; } /* Keyword.Reserved */ - .kt { color: #66d9ef; } /* Keyword.Type */ - .ld { color: #e6db74; } /* Literal.Date */ - .m { color: #ae81ff; } /* Literal.Number */ - .s { color: #e6db74; } /* Literal.String */ - .na { color: #a6e22e; } /* Name.Attribute */ - .nb { color: #f8f8f2; } /* Name.Builtin */ - .nc { color: #a6e22e; } /* Name.Class */ - .no { color: #66d9ef; } /* Name.Constant */ - .nd { color: #a6e22e; } /* Name.Decorator */ - .ni { color: #f8f8f2; } /* Name.Entity */ - .ne { color: #a6e22e; } /* Name.Exception */ - .nf { color: #a6e22e; } /* Name.Function */ - .nl { color: #f8f8f2; } /* Name.Label */ - .nn { color: #f8f8f2; } /* Name.Namespace */ - .nx { color: #a6e22e; } /* Name.Other */ - .py { color: #f8f8f2; } /* Name.Property */ - .nt { color: #f92672; } /* Name.Tag */ - .nv { color: #f8f8f2; } /* Name.Variable */ - .ow { color: #f92672; } /* Operator.Word */ - .w { color: #f8f8f2; } /* Text.Whitespace */ - .mf { color: #ae81ff; } /* Literal.Number.Float */ - .mh { color: #ae81ff; } /* Literal.Number.Hex */ - .mi { color: #ae81ff; } /* Literal.Number.Integer */ - .mo { color: #ae81ff; } /* Literal.Number.Oct */ - .sb { color: #e6db74; } /* Literal.String.Backtick */ - .sc { color: #e6db74; } /* Literal.String.Char */ - .sd { color: #e6db74; } /* Literal.String.Doc */ - .s2 { color: #e6db74; } /* Literal.String.Double */ - .se { color: #ae81ff; } /* Literal.String.Escape */ - .sh { color: #e6db74; } /* Literal.String.Heredoc */ - .si { color: #e6db74; } /* Literal.String.Interpol */ - .sx { color: #e6db74; } /* Literal.String.Other */ - .sr { color: #e6db74; } /* Literal.String.Regex */ - .s1 { color: #e6db74; } /* Literal.String.Single */ - .ss { color: #e6db74; } /* Literal.String.Symbol */ - .bp { color: #f8f8f2; } /* Name.Builtin.Pseudo */ - .vc { color: #f8f8f2; } /* Name.Variable.Class */ - .vg { color: #f8f8f2; } /* Name.Variable.Global */ - .vi { color: #f8f8f2; } /* Name.Variable.Instance */ - .il { color: #ae81ff; } /* Literal.Number.Integer.Long */ - .gu { color: #75715e; } /* Generic.Subheading & Diff Unified/Comment? */ - .gd { color: #f92672; } /* Generic.Deleted & Diff Deleted */ - .gi { color: #a6e22e; } /* Generic.Inserted & Diff Inserted */ + .kc { color: $monokai-kc; } /* Keyword.Constant */ + .kd { color: $monokai-kd; } /* Keyword.Declaration */ + .kn { color: $monokai-kn; } /* Keyword.Namespace */ + .kp { color: $monokai-kp; } /* Keyword.Pseudo */ + .kr { color: $monokai-kr; } /* Keyword.Reserved */ + .kt { color: $monokai-kt; } /* Keyword.Type */ + .ld { color: $monokai-ld; } /* Literal.Date */ + .m { color: $monokai-m; } /* Literal.Number */ + .s { color: $monokai-s; } /* Literal.String */ + .na { color: $monokai-na; } /* Name.Attribute */ + .nb { color: $monokai-nb; } /* Name.Builtin */ + .nc { color: $monokai-nc; } /* Name.Class */ + .no { color: $monokai-no; } /* Name.Constant */ + .nd { color: $monokai-nd; } /* Name.Decorator */ + .ni { color: $monokai-ni; } /* Name.Entity */ + .ne { color: $monokai-ne; } /* Name.Exception */ + .nf { color: $monokai-nf; } /* Name.Function */ + .nl { color: $monokai-nl; } /* Name.Label */ + .nn { color: $monokai-nn; } /* Name.Namespace */ + .nx { color: $monokai-nx; } /* Name.Other */ + .py { color: $monokai-py; } /* Name.Property */ + .nt { color: $monokai-nt; } /* Name.Tag */ + .nv { color: $monokai-nv; } /* Name.Variable */ + .ow { color: $monokai-ow; } /* Operator.Word */ + .w { color: $monokai-w; } /* Text.Whitespace */ + .mf { color: $monokai-mf; } /* Literal.Number.Float */ + .mh { color: $monokai-mh; } /* Literal.Number.Hex */ + .mi { color: $monokai-mi; } /* Literal.Number.Integer */ + .mo { color: $monokai-mo; } /* Literal.Number.Oct */ + .sb { color: $monokai-sb; } /* Literal.String.Backtick */ + .sc { color: $monokai-sc; } /* Literal.String.Char */ + .sd { color: $monokai-sd; } /* Literal.String.Doc */ + .s2 { color: $monokai-s2; } /* Literal.String.Double */ + .se { color: $monokai-se; } /* Literal.String.Escape */ + .sh { color: $monokai-sh; } /* Literal.String.Heredoc */ + .si { color: $monokai-si; } /* Literal.String.Interpol */ + .sx { color: $monokai-sx; } /* Literal.String.Other */ + .sr { color: $monokai-sr; } /* Literal.String.Regex */ + .s1 { color: $monokai-s1; } /* Literal.String.Single */ + .ss { color: $monokai-ss; } /* Literal.String.Symbol */ + .bp { color: $monokai-bp; } /* Name.Builtin.Pseudo */ + .vc { color: $monokai-vc; } /* Name.Variable.Class */ + .vg { color: $monokai-vg; } /* Name.Variable.Global */ + .vi { color: $monokai-vi; } /* Name.Variable.Instance */ + .il { color: $monokai-il; } /* Literal.Number.Integer.Long */ + .gu { color: $monokai-gu; } /* Generic.Subheading & Diff Unified/Comment? */ + .gd { color: $monokai-gd; } /* Generic.Deleted & Diff Deleted */ + .gi { color: $monokai-gi; } /* Generic.Inserted & Diff Inserted */ } diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index a87333146de..874aecb5e16 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -1,27 +1,112 @@ /* https://gist.github.com/qguv/7936275 */ + +/* +* Solarized dark colors +*/ +$solarized-dark-new-bg: rgba(133, 153, 0, 0.15); +$solarized-dark-new-idiff: rgba(133, 153, 0, 0.25); +$solarized-dark-old-bg: rgba(220, 50, 47, 0.3); +$solarized-dark-old-idiff: rgba(220, 50, 47, 0.25); +$solarized-dark-border: #113b46; +$solarized-dark-pre-bg: #002b36; +$solarized-dark-pre-color: #93a1a1; +$solarized-dark-pre-border: #113b46; +$solarized-dark-line-bg: #002b36; +$solarized-dark-line-color: rgba(255, 255, 255, 0.3); +$solarized-dark-highlight: #094554; +$solarized-dark-hll-bg: #174652; +$solarized-dark-c: #586e75; +$solarized-dark-err: #93a1a1; +$solarized-dark-g: #93a1a1; +$solarized-dark-k: #859900; +$solarized-dark-l: #93a1a1; +$solarized-dark-n: #93a1a1; +$solarized-dark-o: #859900; +$solarized-dark-x: #cb4b16; +$solarized-dark-p: #93a1a1; +$solarized-dark-cm: #586e75; +$solarized-dark-cp: #859900; +$solarized-dark-c1: #586e75; +$solarized-dark-cs: #859900; +$solarized-dark-gd: #2aa198; +$solarized-dark-ge: #93a1a1; +$solarized-dark-gr: #dc322f; +$solarized-dark-gh: #cb4b16; +$solarized-dark-gi: #859900; +$solarized-dark-go: #93a1a1; +$solarized-dark-gp: #93a1a1; +$solarized-dark-gs: #93a1a1; +$solarized-dark-gu: #cb4b16; +$solarized-dark-gt: #93a1a1; +$solarized-dark-kc: #cb4b16; +$solarized-dark-kd: #268bd2; +$solarized-dark-kn: #859900; +$solarized-dark-kp: #859900; +$solarized-dark-kr: #268bd2; +$solarized-dark-kt: #dc322f; +$solarized-dark-ld: #93a1a1; +$solarized-dark-m: #2aa198; +$solarized-dark-s: #2aa198; +$solarized-dark-na: #93a1a1; +$solarized-dark-nb: #b58900; +$solarized-dark-nc: #268bd2; +$solarized-dark-no: #cb4b16; +$solarized-dark-nd: #268bd2; +$solarized-dark-ni: #cb4b16; +$solarized-dark-ne: #cb4b16; +$solarized-dark-nf: #268bd2; +$solarized-dark-nl: #93a1a1; +$solarized-dark-nn: #93a1a1; +$solarized-dark-nx: #93a1a1; +$solarized-dark-py: #93a1a1; +$solarized-dark-nt: #268bd2; +$solarized-dark-nv: #268bd2; +$solarized-dark-ow: #859900; +$solarized-dark-w: #93a1a1; +$solarized-dark-mf: #2aa198; +$solarized-dark-mh: #2aa198; +$solarized-dark-mi: #2aa198; +$solarized-dark-mo: #2aa198; +$solarized-dark-sb: #586e75; +$solarized-dark-sc: #2aa198; +$solarized-dark-sd: #93a1a1; +$solarized-dark-s2: #2aa198; +$solarized-dark-se: #cb4b16; +$solarized-dark-sh: #93a1a1; +$solarized-dark-si: #2aa198; +$solarized-dark-sx: #2aa198; +$solarized-dark-sr: #dc322f; +$solarized-dark-s1: #2aa198; +$solarized-dark-ss: #2aa198; +$solarized-dark-bp: #268bd2; +$solarized-dark-vc: #268bd2; +$solarized-dark-vg: #268bd2; +$solarized-dark-vi: #268bd2; +$solarized-dark-il: #2aa198; + .code.solarized-dark { // Line numbers .line-numbers, .diff-line-num { - background-color: #002b36; + background-color: $solarized-dark-line-bg; } .diff-line-num, .diff-line-num a { - color: rgba(255, 255, 255, 0.3); + color: $solarized-dark-line-color; } // Code itself pre.code, .diff-line-num { - border-color: #113b46; + border-color: $solarized-dark-pre-border; } &, pre.code, .line_holder .line_content { - background-color: #002b36; - color: #93a1a1; + background-color: $solarized-dark-pre-bg; + color: $solarized-dark-pre-color; } // Diff line @@ -32,18 +117,18 @@ td.diff-line-num.hll:not(.empty-cell), td.line_content.hll:not(.empty-cell) { - background-color: #174652; - border-color: darken(#174652, 15%); + background-color: $solarized-dark-hll-bg; + border-color: darken($solarized-dark-hll-bg, 15%); } .diff-line-num.new, .line_content.new { - @include diff_background(rgba(133, 153, 0, 0.15), rgba(133, 153, 0, 0.25), #113b46); + @include diff_background($solarized-dark-new-bg, $solarized-dark-new-idiff, $solarized-dark-border); } .diff-line-num.old, .line_content.old { - @include diff_background(rgba(220, 50, 47, 0.3), rgba(220, 50, 47, 0.25), #113b46); + @include diff_background($solarized-dark-old-bg, $solarized-dark-old-idiff, $solarized-dark-border); } .line_content.match { @@ -53,12 +138,12 @@ // highlight line via anchor pre .hll { - background-color: #174652 !important; + background-color: $solarized-dark-hll-bg !important; } // Search result highlight span.highlight_word { - background-color: #094554 !important; + background-color: $solarized-dark-highlight !important; } /* Solarized Dark @@ -79,72 +164,72 @@ green #859900 operators, other keywords */ - .c { color: #586e75; } /* Comment */ - .err { color: #93a1a1; } /* Error */ - .g { color: #93a1a1; } /* Generic */ - .k { color: #859900; } /* Keyword */ - .l { color: #93a1a1; } /* Literal */ - .n { color: #93a1a1; } /* Name */ - .o { color: #859900; } /* Operator */ - .x { color: #cb4b16; } /* Other */ - .p { color: #93a1a1; } /* Punctuation */ - .cm { color: #586e75; } /* Comment.Multiline */ - .cp { color: #859900; } /* Comment.Preproc */ - .c1 { color: #586e75; } /* Comment.Single */ - .cs { color: #859900; } /* Comment.Special */ - .gd { color: #2aa198; } /* Generic.Deleted */ - .ge { color: #93a1a1; font-style: italic; } /* Generic.Emph */ - .gr { color: #dc322f; } /* Generic.Error */ - .gh { color: #cb4b16; } /* Generic.Heading */ - .gi { color: #859900; } /* Generic.Inserted */ - .go { color: #93a1a1; } /* Generic.Output */ - .gp { color: #93a1a1; } /* Generic.Prompt */ - .gs { color: #93a1a1; font-weight: bold; } /* Generic.Strong */ - .gu { color: #cb4b16; } /* Generic.Subheading */ - .gt { color: #93a1a1; } /* Generic.Traceback */ - .kc { color: #cb4b16; } /* Keyword.Constant */ - .kd { color: #268bd2; } /* Keyword.Declaration */ - .kn { color: #859900; } /* Keyword.Namespace */ - .kp { color: #859900; } /* Keyword.Pseudo */ - .kr { color: #268bd2; } /* Keyword.Reserved */ - .kt { color: #dc322f; } /* Keyword.Type */ - .ld { color: #93a1a1; } /* Literal.Date */ - .m { color: #2aa198; } /* Literal.Number */ - .s { color: #2aa198; } /* Literal.String */ - .na { color: #93a1a1; } /* Name.Attribute */ - .nb { color: #b58900; } /* Name.Builtin */ - .nc { color: #268bd2; } /* Name.Class */ - .no { color: #cb4b16; } /* Name.Constant */ - .nd { color: #268bd2; } /* Name.Decorator */ - .ni { color: #cb4b16; } /* Name.Entity */ - .ne { color: #cb4b16; } /* Name.Exception */ - .nf { color: #268bd2; } /* Name.Function */ - .nl { color: #93a1a1; } /* Name.Label */ - .nn { color: #93a1a1; } /* Name.Namespace */ - .nx { color: #93a1a1; } /* Name.Other */ - .py { color: #93a1a1; } /* Name.Property */ - .nt { color: #268bd2; } /* Name.Tag */ - .nv { color: #268bd2; } /* Name.Variable */ - .ow { color: #859900; } /* Operator.Word */ - .w { color: #93a1a1; } /* Text.Whitespace */ - .mf { color: #2aa198; } /* Literal.Number.Float */ - .mh { color: #2aa198; } /* Literal.Number.Hex */ - .mi { color: #2aa198; } /* Literal.Number.Integer */ - .mo { color: #2aa198; } /* Literal.Number.Oct */ - .sb { color: #586e75; } /* Literal.String.Backtick */ - .sc { color: #2aa198; } /* Literal.String.Char */ - .sd { color: #93a1a1; } /* Literal.String.Doc */ - .s2 { color: #2aa198; } /* Literal.String.Double */ - .se { color: #cb4b16; } /* Literal.String.Escape */ - .sh { color: #93a1a1; } /* Literal.String.Heredoc */ - .si { color: #2aa198; } /* Literal.String.Interpol */ - .sx { color: #2aa198; } /* Literal.String.Other */ - .sr { color: #dc322f; } /* Literal.String.Regex */ - .s1 { color: #2aa198; } /* Literal.String.Single */ - .ss { color: #2aa198; } /* Literal.String.Symbol */ - .bp { color: #268bd2; } /* Name.Builtin.Pseudo */ - .vc { color: #268bd2; } /* Name.Variable.Class */ - .vg { color: #268bd2; } /* Name.Variable.Global */ - .vi { color: #268bd2; } /* Name.Variable.Instance */ - .il { color: #2aa198; } /* Literal.Number.Integer.Long */ + .c { color: $solarized-dark-c; } /* Comment */ + .err { color: $solarized-dark-err; } /* Error */ + .g { color: $solarized-dark-g; } /* Generic */ + .k { color: $solarized-dark-k; } /* Keyword */ + .l { color: $solarized-dark-l; } /* Literal */ + .n { color: $solarized-dark-n; } /* Name */ + .o { color: $solarized-dark-o; } /* Operator */ + .x { color: $solarized-dark-x; } /* Other */ + .p { color: $solarized-dark-p; } /* Punctuation */ + .cm { color: $solarized-dark-cm; } /* Comment.Multiline */ + .cp { color: $solarized-dark-cp; } /* Comment.Preproc */ + .c1 { color: $solarized-dark-c1; } /* Comment.Single */ + .cs { color: $solarized-dark-cs; } /* Comment.Special */ + .gd { color: $solarized-dark-gd; } /* Generic.Deleted */ + .ge { color: $solarized-dark-ge; font-style: italic; } /* Generic.Emph */ + .gr { color: $solarized-dark-gr; } /* Generic.Error */ + .gh { color: $solarized-dark-gh; } /* Generic.Heading */ + .gi { color: $solarized-dark-gi; } /* Generic.Inserted */ + .go { color: $solarized-dark-go; } /* Generic.Output */ + .gp { color: $solarized-dark-gp; } /* Generic.Prompt */ + .gs { color: $solarized-dark-gs; font-weight: bold; } /* Generic.Strong */ + .gu { color: $solarized-dark-gu; } /* Generic.Subheading */ + .gt { color: $solarized-dark-gt; } /* Generic.Traceback */ + .kc { color: $solarized-dark-kc; } /* Keyword.Constant */ + .kd { color: $solarized-dark-kd; } /* Keyword.Declaration */ + .kn { color: $solarized-dark-kn; } /* Keyword.Namespace */ + .kp { color: $solarized-dark-kp; } /* Keyword.Pseudo */ + .kr { color: $solarized-dark-kr; } /* Keyword.Reserved */ + .kt { color: $solarized-dark-kt; } /* Keyword.Type */ + .ld { color: $solarized-dark-ld; } /* Literal.Date */ + .m { color: $solarized-dark-m; } /* Literal.Number */ + .s { color: $solarized-dark-s; } /* Literal.String */ + .na { color: $solarized-dark-na; } /* Name.Attribute */ + .nb { color: $solarized-dark-nb; } /* Name.Builtin */ + .nc { color: $solarized-dark-nc; } /* Name.Class */ + .no { color: $solarized-dark-no; } /* Name.Constant */ + .nd { color: $solarized-dark-nd; } /* Name.Decorator */ + .ni { color: $solarized-dark-ni; } /* Name.Entity */ + .ne { color: $solarized-dark-ne; } /* Name.Exception */ + .nf { color: $solarized-dark-nf; } /* Name.Function */ + .nl { color: $solarized-dark-nl; } /* Name.Label */ + .nn { color: $solarized-dark-nn; } /* Name.Namespace */ + .nx { color: $solarized-dark-nx; } /* Name.Other */ + .py { color: $solarized-dark-py; } /* Name.Property */ + .nt { color: $solarized-dark-nt; } /* Name.Tag */ + .nv { color: $solarized-dark-nv; } /* Name.Variable */ + .ow { color: $solarized-dark-ow; } /* Operator.Word */ + .w { color: $solarized-dark-w; } /* Text.Whitespace */ + .mf { color: $solarized-dark-mf; } /* Literal.Number.Float */ + .mh { color: $solarized-dark-mh; } /* Literal.Number.Hex */ + .mi { color: $solarized-dark-mi; } /* Literal.Number.Integer */ + .mo { color: $solarized-dark-mo; } /* Literal.Number.Oct */ + .sb { color: $solarized-dark-sb; } /* Literal.String.Backtick */ + .sc { color: $solarized-dark-sc; } /* Literal.String.Char */ + .sd { color: $solarized-dark-sd; } /* Literal.String.Doc */ + .s2 { color: $solarized-dark-s2; } /* Literal.String.Double */ + .se { color: $solarized-dark-se; } /* Literal.String.Escape */ + .sh { color: $solarized-dark-sh; } /* Literal.String.Heredoc */ + .si { color: $solarized-dark-si; } /* Literal.String.Interpol */ + .sx { color: $solarized-dark-sx; } /* Literal.String.Other */ + .sr { color: $solarized-dark-sr; } /* Literal.String.Regex */ + .s1 { color: $solarized-dark-s1; } /* Literal.String.Single */ + .ss { color: $solarized-dark-ss; } /* Literal.String.Symbol */ + .bp { color: $solarized-dark-bp; } /* Name.Builtin.Pseudo */ + .vc { color: $solarized-dark-vc; } /* Name.Variable.Class */ + .vg { color: $solarized-dark-vg; } /* Name.Variable.Global */ + .vi { color: $solarized-dark-vi; } /* Name.Variable.Instance */ + .il { color: $solarized-dark-il; } /* Literal.Number.Integer.Long */ } diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index faff353ded7..499a1c108b8 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -1,15 +1,99 @@ /* https://gist.github.com/qguv/7936275 */ +/* +* Solarized light syntax colors +*/ +$solarized-light-matchline-bg: rgba(255, 255, 255, 0.4); +$solarized-light-new-bg: rgba(133, 153, 0, 0.2); +$solarized-light-new-idiff: rgba(133, 153, 0, 0.25); +$solarized-light-old-bg: rgba(220, 50, 47, 0.2); +$solarized-light-old-idiff: rgba(220, 50, 47, 0.25); +$solarized-light-border: #c5d0d4; +$solarized-light-pre-bg: #002b36; +$solarized-light-pre-bg: #fdf6e3; +$solarized-light-pre-color: #586e75; +$solarized-light-line-bg: #fdf6e3; +$solarized-light-highlight: #eee8d5; +$solarized-light-hll-bg: #ddd8c5; +$solarized-light-c: #93a1a1; +$solarized-light-err: #586e75; +$solarized-light-g: #586e75; +$solarized-light-k: #859900; +$solarized-light-l: #586e75; +$solarized-light-n: #586e75; +$solarized-light-o: #859900; +$solarized-light-x: #cb4b16; +$solarized-light-p: #586e75; +$solarized-light-cm: #93a1a1; +$solarized-light-cp: #859900; +$solarized-light-c1: #93a1a1; +$solarized-light-cs: #859900; +$solarized-light-gd: #2aa198; +$solarized-light-ge: #586e75; +$solarized-light-gr: #dc322f; +$solarized-light-gh: #cb4b16; +$solarized-light-gi: #859900; +$solarized-light-go: #586e75; +$solarized-light-gp: #586e75; +$solarized-light-gs: #586e75; +$solarized-light-gu: #cb4b16; +$solarized-light-gt: #586e75; +$solarized-light-kc: #cb4b16; +$solarized-light-kd: #268bd2; +$solarized-light-kn: #859900; +$solarized-light-kp: #859900; +$solarized-light-kr: #268bd2; +$solarized-light-kt: #dc322f; +$solarized-light-ld: #586e75; +$solarized-light-m: #2aa198; +$solarized-light-s: #2aa198; +$solarized-light-na: #586e75; +$solarized-light-nb: #b58900; +$solarized-light-nc: #268bd2; +$solarized-light-no: #cb4b16; +$solarized-light-nd: #268bd2; +$solarized-light-ni: #cb4b16; +$solarized-light-ne: #cb4b16; +$solarized-light-nf: #268bd2; +$solarized-light-nl: #586e75; +$solarized-light-nn: #586e75; +$solarized-light-nx: #586e75; +$solarized-light-py: #586e75; +$solarized-light-nt: #268bd2; +$solarized-light-nv: #268bd2; +$solarized-light-ow: #859900; +$solarized-light-w: #586e75; +$solarized-light-mf: #2aa198; +$solarized-light-mh: #2aa198; +$solarized-light-mi: #2aa198; +$solarized-light-mo: #2aa198; +$solarized-light-sb: #93a1a1; +$solarized-light-sc: #2aa198; +$solarized-light-sd: #586e75; +$solarized-light-s2: #2aa198; +$solarized-light-se: #cb4b16; +$solarized-light-sh: #586e75; +$solarized-light-si: #2aa198; +$solarized-light-sx: #2aa198; +$solarized-light-sr: #dc322f; +$solarized-light-s1: #2aa198; +$solarized-light-ss: #2aa198; +$solarized-light-bp: #268bd2; +$solarized-light-vc: #268bd2; +$solarized-light-vg: #268bd2; +$solarized-light-vi: #268bd2; +$solarized-light-il: #2aa198; + @mixin matchLine { color: $black-transparent; - background: rgba(255, 255, 255, 0.4); + background: $solarized-light-matchline-bg; } .code.solarized-light { // Line numbers .line-numbers, .diff-line-num { - background-color: #fdf6e3; + background-color: $solarized-light-line-bg; } .diff-line-num, @@ -20,14 +104,14 @@ // Code itself pre.code, .diff-line-num { - border-color: #c5d0d4; + border-color: $solarized-light-border; } &, pre.code, .line_holder .line_content { - background-color: #fdf6e3; - color: #586e75; + background-color: $solarized-light-pre-bg; + color: $solarized-light-pre-color; } // Diff line @@ -38,18 +122,19 @@ td.diff-line-num.hll:not(.empty-cell), td.line_content.hll:not(.empty-cell) { - background-color: #ddd8c5; - border-color: darken(#ddd8c5, 15%); + background-color: $solarized-light-hll-bg; + border-color: darken($solarized-light-hll-bg, 15%); } .diff-line-num.new, .line_content.new { - @include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.25), #c5d0d4); + @include diff_background($solarized-light-new-bg, + $solarized-light-new-idiff, $solarized-light-border); } .diff-line-num.old, .line_content.old { - @include diff_background(rgba(220, 50, 47, 0.2), rgba(220, 50, 47, 0.25), #c5d0d4); + @include diff_background($solarized-light-old-bg, $solarized-light-old-idiff, $solarized-light-border); } .line_content.match { @@ -59,12 +144,12 @@ // highlight line via anchor pre .hll { - background-color: #ddd8c5 !important; + background-color: $solarized-light-hll-bg !important; } // Search result highlight span.highlight_word { - background-color: #eee8d5 !important; + background-color: $solarized-light-highlight !important; } /* Solarized Light @@ -85,72 +170,72 @@ green #859900 operators, other keywords */ - .c { color: #93a1a1; } /* Comment */ - .err { color: #586e75; } /* Error */ - .g { color: #586e75; } /* Generic */ - .k { color: #859900; } /* Keyword */ - .l { color: #586e75; } /* Literal */ - .n { color: #586e75; } /* Name */ - .o { color: #859900; } /* Operator */ - .x { color: #cb4b16; } /* Other */ - .p { color: #586e75; } /* Punctuation */ - .cm { color: #93a1a1; } /* Comment.Multiline */ - .cp { color: #859900; } /* Comment.Preproc */ - .c1 { color: #93a1a1; } /* Comment.Single */ - .cs { color: #859900; } /* Comment.Special */ - .gd { color: #2aa198; } /* Generic.Deleted */ - .ge { color: #586e75; font-style: italic; } /* Generic.Emph */ - .gr { color: #dc322f; } /* Generic.Error */ - .gh { color: #cb4b16; } /* Generic.Heading */ - .gi { color: #859900; } /* Generic.Inserted */ - .go { color: #586e75; } /* Generic.Output */ - .gp { color: #586e75; } /* Generic.Prompt */ - .gs { color: #586e75; font-weight: bold; } /* Generic.Strong */ - .gu { color: #cb4b16; } /* Generic.Subheading */ - .gt { color: #586e75; } /* Generic.Traceback */ - .kc { color: #cb4b16; } /* Keyword.Constant */ - .kd { color: #268bd2; } /* Keyword.Declaration */ - .kn { color: #859900; } /* Keyword.Namespace */ - .kp { color: #859900; } /* Keyword.Pseudo */ - .kr { color: #268bd2; } /* Keyword.Reserved */ - .kt { color: #dc322f; } /* Keyword.Type */ - .ld { color: #586e75; } /* Literal.Date */ - .m { color: #2aa198; } /* Literal.Number */ - .s { color: #2aa198; } /* Literal.String */ - .na { color: #586e75; } /* Name.Attribute */ - .nb { color: #b58900; } /* Name.Builtin */ - .nc { color: #268bd2; } /* Name.Class */ - .no { color: #cb4b16; } /* Name.Constant */ - .nd { color: #268bd2; } /* Name.Decorator */ - .ni { color: #cb4b16; } /* Name.Entity */ - .ne { color: #cb4b16; } /* Name.Exception */ - .nf { color: #268bd2; } /* Name.Function */ - .nl { color: #586e75; } /* Name.Label */ - .nn { color: #586e75; } /* Name.Namespace */ - .nx { color: #586e75; } /* Name.Other */ - .py { color: #586e75; } /* Name.Property */ - .nt { color: #268bd2; } /* Name.Tag */ - .nv { color: #268bd2; } /* Name.Variable */ - .ow { color: #859900; } /* Operator.Word */ - .w { color: #586e75; } /* Text.Whitespace */ - .mf { color: #2aa198; } /* Literal.Number.Float */ - .mh { color: #2aa198; } /* Literal.Number.Hex */ - .mi { color: #2aa198; } /* Literal.Number.Integer */ - .mo { color: #2aa198; } /* Literal.Number.Oct */ - .sb { color: #93a1a1; } /* Literal.String.Backtick */ - .sc { color: #2aa198; } /* Literal.String.Char */ - .sd { color: #586e75; } /* Literal.String.Doc */ - .s2 { color: #2aa198; } /* Literal.String.Double */ - .se { color: #cb4b16; } /* Literal.String.Escape */ - .sh { color: #586e75; } /* Literal.String.Heredoc */ - .si { color: #2aa198; } /* Literal.String.Interpol */ - .sx { color: #2aa198; } /* Literal.String.Other */ - .sr { color: #dc322f; } /* Literal.String.Regex */ - .s1 { color: #2aa198; } /* Literal.String.Single */ - .ss { color: #2aa198; } /* Literal.String.Symbol */ - .bp { color: #268bd2; } /* Name.Builtin.Pseudo */ - .vc { color: #268bd2; } /* Name.Variable.Class */ - .vg { color: #268bd2; } /* Name.Variable.Global */ - .vi { color: #268bd2; } /* Name.Variable.Instance */ - .il { color: #2aa198; } /* Literal.Number.Integer.Long */ + .c { color: $solarized-light-c; } /* Comment */ + .err { color: $solarized-light-err; } /* Error */ + .g { color: $solarized-light-g; } /* Generic */ + .k { color: $solarized-light-k; } /* Keyword */ + .l { color: $solarized-light-l; } /* Literal */ + .n { color: $solarized-light-n; } /* Name */ + .o { color: $solarized-light-o; } /* Operator */ + .x { color: $solarized-light-x; } /* Other */ + .p { color: $solarized-light-p; } /* Punctuation */ + .cm { color: $solarized-light-cm; } /* Comment.Multiline */ + .cp { color: $solarized-light-cp; } /* Comment.Preproc */ + .c1 { color: $solarized-light-c1; } /* Comment.Single */ + .cs { color: $solarized-light-cs; } /* Comment.Special */ + .gd { color: $solarized-light-gd; } /* Generic.Deleted */ + .ge { color: $solarized-light-ge; font-style: italic; } /* Generic.Emph */ + .gr { color: $solarized-light-gr; } /* Generic.Error */ + .gh { color: $solarized-light-gh; } /* Generic.Heading */ + .gi { color: $solarized-light-gi; } /* Generic.Inserted */ + .go { color: $solarized-light-go; } /* Generic.Output */ + .gp { color: $solarized-light-gp; } /* Generic.Prompt */ + .gs { color: $solarized-light-gs; font-weight: bold; } /* Generic.Strong */ + .gu { color: $solarized-light-gu; } /* Generic.Subheading */ + .gt { color: $solarized-light-gt; } /* Generic.Traceback */ + .kc { color: $solarized-light-kc; } /* Keyword.Constant */ + .kd { color: $solarized-light-kd; } /* Keyword.Declaration */ + .kn { color: $solarized-light-kn; } /* Keyword.Namespace */ + .kp { color: $solarized-light-kp; } /* Keyword.Pseudo */ + .kr { color: $solarized-light-kr; } /* Keyword.Reserved */ + .kt { color: $solarized-light-kt; } /* Keyword.Type */ + .ld { color: $solarized-light-ld; } /* Literal.Date */ + .m { color: $solarized-light-m; } /* Literal.Number */ + .s { color: $solarized-light-s; } /* Literal.String */ + .na { color: $solarized-light-na; } /* Name.Attribute */ + .nb { color: $solarized-light-nb; } /* Name.Builtin */ + .nc { color: $solarized-light-nc; } /* Name.Class */ + .no { color: $solarized-light-no; } /* Name.Constant */ + .nd { color: $solarized-light-nd; } /* Name.Decorator */ + .ni { color: $solarized-light-ni; } /* Name.Entity */ + .ne { color: $solarized-light-ne; } /* Name.Exception */ + .nf { color: $solarized-light-nf; } /* Name.Function */ + .nl { color: $solarized-light-nl; } /* Name.Label */ + .nn { color: $solarized-light-nn; } /* Name.Namespace */ + .nx { color: $solarized-light-nx; } /* Name.Other */ + .py { color: $solarized-light-py; } /* Name.Property */ + .nt { color: $solarized-light-nt; } /* Name.Tag */ + .nv { color: $solarized-light-nv; } /* Name.Variable */ + .ow { color: $solarized-light-ow; } /* Operator.Word */ + .w { color: $solarized-light-w; } /* Text.Whitespace */ + .mf { color: $solarized-light-mf; } /* Literal.Number.Float */ + .mh { color: $solarized-light-mh; } /* Literal.Number.Hex */ + .mi { color: $solarized-light-mi; } /* Literal.Number.Integer */ + .mo { color: $solarized-light-mo; } /* Literal.Number.Oct */ + .sb { color: $solarized-light-sb; } /* Literal.String.Backtick */ + .sc { color: $solarized-light-sc; } /* Literal.String.Char */ + .sd { color: $solarized-light-sd; } /* Literal.String.Doc */ + .s2 { color: $solarized-light-s2; } /* Literal.String.Double */ + .se { color: $solarized-light-se; } /* Literal.String.Escape */ + .sh { color: $solarized-light-sh; } /* Literal.String.Heredoc */ + .si { color: $solarized-light-si; } /* Literal.String.Interpol */ + .sx { color: $solarized-light-sx; } /* Literal.String.Other */ + .sr { color: $solarized-light-sr; } /* Literal.String.Regex */ + .s1 { color: $solarized-light-s1; } /* Literal.String.Single */ + .ss { color: $solarized-light-ss; } /* Literal.String.Symbol */ + .bp { color: $solarized-light-bp; } /* Name.Builtin.Pseudo */ + .vc { color: $solarized-light-vc; } /* Name.Variable.Class */ + .vg { color: $solarized-light-vg; } /* Name.Variable.Global */ + .vi { color: $solarized-light-vi; } /* Name.Variable.Instance */ + .il { color: $solarized-light-il; } /* Literal.Number.Integer.Long */ } diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index d5367d5f3f0..1adab3ffd94 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -1,5 +1,72 @@ /* https://github.com/aahan/pygments-github-style */ +/* +* White Syntax Colors +*/ +$white-code-color: #333; +$white-highlight: #fafe3d; +$white-pre-hll-bg: #f8eec7; +$white-hll-bg: #f8f8f8; +$white-c: #998; +$white-err: #a61717; +$white-err-bg: #e3d2d2; +$white-cm: #998; +$white-cp: #999; +$white-c1: #998; +$white-cs: #999; +$white-gd: $black; +$white-gd-bg: #fdd; +$white-gd-x: $black; +$white-gd-x-bg: #faa; +$white-gr: #a00; +$white-gh: #999; +$white-gi: $black; +$white-gi-bg: #dfd; +$white-gi-x: $black; +$white-gi-x-bg: #afa; +$white-go: #888; +$white-gp: #555; +$white-gu: #800080; +$white-gt: #a00; +$white-kt: #458; +$white-m: #099; +$white-s: #d14; +$white-n: #333; +$white-na: teal; +$white-nb: #0086b3; +$white-nc: #458; +$white-no: teal; +$white-ni: purple; +$white-ne: #900; +$white-nf: #900; +$white-nn: #555; +$white-nt: navy; +$white-nv: teal; +$white-w: #bbb; +$white-mf: #099; +$white-mh: #099; +$white-mi: #099; +$white-mo: #099; +$white-sb: #d14; +$white-sc: #d14; +$white-sd: #d14; +$white-s2: #d14; +$white-se: #d14; +$white-sh: #d14; +$white-si: #d14; +$white-sx: #d14; +$white-sr: #009926; +$white-s1: #d14; +$white-ss: #990073; +$white-bp: #999; +$white-vc: teal; +$white-vg: teal; +$white-vi: teal; +$white-il: #099; +$white-gc-color: #999; +$white-gc-bg: #eaf2f5; + + @mixin matchLine { color: $black-transparent; background-color: $match-line; @@ -26,8 +93,8 @@ &, pre.code, .line_holder .line_content { - background-color: #fff; - color: #333; + background-color: $white-light; + color: $white-code-color; } // Diff line @@ -83,75 +150,75 @@ // highlight line via anchor pre .hll { - background-color: #f8eec7 !important; + background-color: $white-pre-hll-bg !important; } // Search result highlight span.highlight_word { - background-color: #fafe3d !important; + background-color: $white-highlight !important; } - .hll { background-color: #f8f8f8; } - .c { color: #998; font-style: italic; } - .err { color: #a61717; background-color: #e3d2d2; } + .hll { background-color: $white-hll-bg; } + .c { color: $white-c; font-style: italic; } + .err { color: $white-err; background-color: $white-err-bg; } .k { font-weight: bold; } .o { font-weight: bold; } - .cm { color: #998; font-style: italic; } - .cp { color: #999; font-weight: bold; } - .c1 { color: #998; font-style: italic; } - .cs { color: #999; font-weight: bold; font-style: italic; } - .gd { color: #000; background-color: #fdd; } - .gd .x { color: #000; background-color: #faa; } + .cm { color: $white-cm; font-style: italic; } + .cp { color: $white-cp; font-weight: bold; } + .c1 { color: $white-c1; font-style: italic; } + .cs { color: $white-cs; font-weight: bold; font-style: italic; } + .gd { color: $white-gd; background-color: $white-gd-bg; } + .gd .x { color: $white-gd-x; background-color: $white-gd-x-bg; } .ge { font-style: italic; } - .gr { color: #a00; } - .gh { color: #999; } - .gi { color: #000; background-color: #dfd; } - .gi .x { color: #000; background-color: #afa; } - .go { color: #888; } - .gp { color: #555; } + .gr { color: $white-gr; } + .gh { color: $white-gh; } + .gi { color: $white-gi; background-color: $white-gi-bg; } + .gi .x { color: $white-gi-x; background-color: $white-gi-x-bg; } + .go { color: $white-go; } + .gp { color: $white-gp; } .gs { font-weight: bold; } - .gu { color: #800080; font-weight: bold; } - .gt { color: #a00; } + .gu { color: $white-gu; font-weight: bold; } + .gt { color: $white-gt; } .kc { font-weight: bold; } .kd { font-weight: bold; } .kn { font-weight: bold; } .kp { font-weight: bold; } .kr { font-weight: bold; } - .kt { color: #458; font-weight: bold; } - .m { color: #099; } - .s { color: #d14; } - .n { color: #333; } - .na { color: teal; } - .nb { color: #0086b3; } - .nc { color: #458; font-weight: bold; } - .no { color: teal; } - .ni { color: purple; } - .ne { color: #900; font-weight: bold; } - .nf { color: #900; font-weight: bold; } - .nn { color: #555; } - .nt { color: navy; } - .nv { color: teal; } + .kt { color: $white-kt; font-weight: bold; } + .m { color: $white-m; } + .s { color: $white-s; } + .n { color: $white-n; } + .na { color: $white-na; } + .nb { color: $white-nb; } + .nc { color: $white-nc; font-weight: bold; } + .no { color: $white-no; } + .ni { color: $white-ni; } + .ne { color: $white-ne; font-weight: bold; } + .nf { color: $white-nf; font-weight: bold; } + .nn { color: $white-nn; } + .nt { color: $white-nt; } + .nv { color: $white-nv; } .ow { font-weight: bold; } - .w { color: #bbb; } - .mf { color: #099; } - .mh { color: #099; } - .mi { color: #099; } - .mo { color: #099; } - .sb { color: #d14; } - .sc { color: #d14; } - .sd { color: #d14; } - .s2 { color: #d14; } - .se { color: #d14; } - .sh { color: #d14; } - .si { color: #d14; } - .sx { color: #d14; } - .sr { color: #009926; } - .s1 { color: #d14; } - .ss { color: #990073; } - .bp { color: #999; } - .vc { color: teal; } - .vg { color: teal; } - .vi { color: teal; } - .il { color: #099; } - .gc { color: #999; background-color: #eaf2f5; } + .w { color: $white-w; } + .mf { color: $white-mf; } + .mh { color: $white-mh; } + .mi { color: $white-mi; } + .mo { color: $white-mo; } + .sb { color: $white-sb; } + .sc { color: $white-sc; } + .sd { color: $white-sd; } + .s2 { color: $white-s2; } + .se { color: $white-se; } + .sh { color: $white-sh; } + .si { color: $white-si; } + .sx { color: $white-sx; } + .sr { color: $white-sr; } + .s1 { color: $white-s1; } + .ss { color: $white-ss; } + .bp { color: $white-bp; } + .vc { color: $white-vc; } + .vg { color: $white-vg; } + .vi { color: $white-vi; } + .il { color: $white-il; } + .gc { color: $white-gc-color; background-color: $white-gc-bg; } } diff --git a/app/assets/stylesheets/mailers/devise.scss b/app/assets/stylesheets/mailers/devise.scss index b2bce482fde..9f613710cf4 100644 --- a/app/assets/stylesheets/mailers/devise.scss +++ b/app/assets/stylesheets/mailers/devise.scss @@ -1,3 +1,5 @@ +@import "framework/variables"; + // NOTE: This stylesheet is for the exclusive use of the `devise_mailer` layout // used for Devise email templates, and _should not_ be included in any // application stylesheets. @@ -46,7 +48,7 @@ table { &#body { background-color: $message-background-color; - border: 1px solid #000; + border: 1px solid $black; border-radius: 4px; margin: 0 auto; width: 600px; diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss index 8d1a6020ca4..024b4df6bd0 100644 --- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss +++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss @@ -10,8 +10,72 @@ // preference): plain class selectors, type (element name) selectors, or // explicit child selectors. +/* +* Highlighted Diff Email Syntax Colors +*/ +$highlighted-highlight-word: #fafe3d; +$highlighted-hll-bg: #f8f8f8; +$highlighted-c: #998; +$highlighted-err: #a61717; +$highlighted-err-bg: #e3d2d2; +$highlighted-cm: #998; +$highlighted-cp: #999; +$highlighted-c1: #998; +$highlighted-cs: #999; +$highlighted-gd: #000; +$highlighted-gd-bg: #fdd; +$highlighted-gd-x: #000; +$highlighted-gd-x-bg: #faa; +$highlighted-gr: #a00; +$highlighted-gh: #999; +$highlighted-gi: #000; +$highlighted-gi-bg: #dfd; +$highlighted-gi-x: #000; +$highlighted-gi-x-bg: #afa; +$highlighted-go: #888; +$highlighted-gp: #555; +$highlighted-gu: #800080; +$highlighted-gt: #a00; +$highlighted-kt: #458; +$highlighted-m: #099; +$highlighted-s: #d14; +$highlighted-n: #333; +$highlighted-na: teal; +$highlighted-nb: #0086b3; +$highlighted-nc: #458; +$highlighted-no: teal; +$highlighted-ni: purple; +$highlighted-ne: #900; +$highlighted-nf: #900; +$highlighted-nn: #555; +$highlighted-nt: navy; +$highlighted-nv: teal; +$highlighted-w: #bbb; +$highlighted-mf: #099; +$highlighted-mh: #099; +$highlighted-mi: #099; +$highlighted-mo: #099; +$highlighted-sb: #d14; +$highlighted-sc: #d14; +$highlighted-sd: #d14; +$highlighted-s2: #d14; +$highlighted-se: #d14; +$highlighted-sh: #d14; +$highlighted-si: #d14; +$highlighted-sx: #d14; +$highlighted-sr: #009926; +$highlighted-s1: #d14; +$highlighted-ss: #990073; +$highlighted-bp: #999; +$highlighted-vc: teal; +$highlighted-vg: teal; +$highlighted-vi: teal; +$highlighted-il: #099; +$highlighted-gc: #999; +$highlighted-gc-bg: #eaf2f5; + .code { - background-color: #fff; + background-color: $white-light; font-family: monospace; font-size: $code_font_size; -premailer-cellpadding: 0; @@ -75,69 +139,69 @@ pre { } span.highlight_word { - background-color: #fafe3d !important; + background-color: $highlighted-highlight-word !important; } -.hll { background-color: #f8f8f8; } -.c { color: #998; font-style: italic; } -.err { color: #a61717; background-color: #e3d2d2; } +.hll { background-color: $highlighted-hll-bg; } +.c { color: $highlighted-c; font-style: italic; } +.err { color: $highlighted-err; background-color: $highlighted-err-bg; } .k { font-weight: bold; } .o { font-weight: bold; } -.cm { color: #998; font-style: italic; } -.cp { color: #999; font-weight: bold; } -.c1 { color: #998; font-style: italic; } -.cs { color: #999; font-weight: bold; font-style: italic; } -.gd { color: #000; background-color: #fdd; } -.gd .x { color: #000; background-color: #faa; } +.cm { color: $highlighted-cm; font-style: italic; } +.cp { color: $highlighted-cp; font-weight: bold; } +.c1 { color: $highlighted-c1; font-style: italic; } +.cs { color: $highlighted-cs; font-weight: bold; font-style: italic; } +.gd { color: $highlighted-gd; background-color: $highlighted-gd-bg; } +.gd .x { color: $highlighted-gd; background-color: $highlighted-gd-x-bg; } .ge { font-style: italic; } -.gr { color: #a00; } -.gh { color: #999; } -.gi { color: #000; background-color: #dfd; } -.gi .x { color: #000; background-color: #afa; } -.go { color: #888; } -.gp { color: #555; } +.gr { color: $highlighted-gr; } +.gh { color: $highlighted-gh; } +.gi { color: $highlighted-gi; background-color: $highlighted-gi-bg; } +.gi .x { color: $highlighted-gi; background-color: $highlighted-gi-x-bg; } +.go { color: $highlighted-go; } +.gp { color: $highlighted-gp; } .gs { font-weight: bold; } -.gu { color: #800080; font-weight: bold; } -.gt { color: #a00; } +.gu { color: $highlighted-gu; font-weight: bold; } +.gt { color: $highlighted-gt; } .kc { font-weight: bold; } .kd { font-weight: bold; } .kn { font-weight: bold; } .kp { font-weight: bold; } .kr { font-weight: bold; } -.kt { color: #458; font-weight: bold; } -.m { color: #099; } -.s { color: #d14; } -.n { color: #333; } -.na { color: teal; } -.nb { color: #0086b3; } -.nc { color: #458; font-weight: bold; } -.no { color: teal; } -.ni { color: purple; } -.ne { color: #900; font-weight: bold; } -.nf { color: #900; font-weight: bold; } -.nn { color: #555; } -.nt { color: navy; } -.nv { color: teal; } +.kt { color: $highlighted-kt; font-weight: bold; } +.m { color: $highlighted-m; } +.s { color: $highlighted-s; } +.n { color: $highlighted-n; } +.na { color: $highlighted-na; } +.nb { color: $highlighted-nb; } +.nc { color: $highlighted-nc; font-weight: bold; } +.no { color: $highlighted-no; } +.ni { color: $highlighted-ni; } +.ne { color: $highlighted-ne; font-weight: bold; } +.nf { color: $highlighted-nf; font-weight: bold; } +.nn { color: $highlighted-nn; } +.nt { color: $highlighted-nt; } +.nv { color: $highlighted-nv; } .ow { font-weight: bold; } -.w { color: #bbb; } -.mf { color: #099; } -.mh { color: #099; } -.mi { color: #099; } -.mo { color: #099; } -.sb { color: #d14; } -.sc { color: #d14; } -.sd { color: #d14; } -.s2 { color: #d14; } -.se { color: #d14; } -.sh { color: #d14; } -.si { color: #d14; } -.sx { color: #d14; } -.sr { color: #009926; } -.s1 { color: #d14; } -.ss { color: #990073; } -.bp { color: #999; } -.vc { color: teal; } -.vg { color: teal; } -.vi { color: teal; } -.il { color: #099; } -.gc { color: #999; background-color: #eaf2f5; } +.w { color: $highlighted-w; } +.mf { color: $highlighted-mf; } +.mh { color: $highlighted-mh; } +.mi { color: $highlighted-mi; } +.mo { color: $highlighted-mo; } +.sb { color: $highlighted-sb; } +.sc { color: $highlighted-sc; } +.sd { color: $highlighted-sd; } +.s2 { color: $highlighted-s2; } +.se { color: $highlighted-se; } +.sh { color: $highlighted-sh; } +.si { color: $highlighted-si; } +.sx { color: $highlighted-sx; } +.sr { color: $highlighted-sr; } +.s1 { color: $highlighted-s1; } +.ss { color: $highlighted-ss; } +.bp { color: $highlighted-bp; } +.vc { color: $highlighted-vc; } +.vg { color: $highlighted-vg; } +.vi { color: $highlighted-vi; } +.il { color: $highlighted-il; } +.gc { color: $highlighted-gc; background-color: $highlighted-gc-bg; } diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss index ced8c4a9907..ddc382362f7 100644 --- a/app/assets/stylesheets/notify.scss +++ b/app/assets/stylesheets/notify.scss @@ -1,3 +1,5 @@ +@import "framework/variables"; + img { max-width: 100%; height: auto; @@ -5,12 +7,12 @@ img { p.details { font-style: italic; - color: #777; + color: $notify-details; } .footer > p { font-size: small; - color: #777; + color: $notify-footer; } pre.commit-message { @@ -21,10 +23,10 @@ pre.commit-message { text-decoration: none; > .new-file { - color: #090; + color: $notify-new-file; } > .deleted-file { - color: #b00; + color: $notify-deleted-file; } } diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss index 14812e171fd..291372b88e3 100644 --- a/app/assets/stylesheets/pages/admin.scss +++ b/app/assets/stylesheets/pages/admin.scss @@ -31,7 +31,7 @@ .form-actions { padding-left: 130px; - background: #fff; + background: $white-light; } .visibility-levels { diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index 486ad16ea26..dce5c31f282 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -15,7 +15,7 @@ background-color: $award-emoji-menu-bg; border: 1px solid $award-emoji-menu-border; border-radius: $border-radius-base; - box-shadow: 0 6px 12px rgba(0,0,0,.175); + box-shadow: 0 6px 12px $award-emoji-menu-shadow; pointer-events: none; opacity: 0; transform: scale(.2); diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 82f36f24867..0d9cf679e7c 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -145,7 +145,7 @@ .board-blank-state { height: calc(100% - 49px); padding: $gl-padding; - background-color: #fff; + background-color: $white-light; } .board-blank-state-list { @@ -191,9 +191,9 @@ .card { position: relative; padding: 10px $gl-padding; - background: #fff; + background: $white-light; border-radius: $border-radius-default; - box-shadow: 0 1px 2px rgba(186, 186, 186, 0.5); + box-shadow: 0 1px 2px $issue-boards-card-shadow; list-style: none; &:not(:last-child) { diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 48f11eb2552..842c0434bf2 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -1,7 +1,7 @@ .build-page { pre.trace { - background: #111; - color: #fff; + background: $builds-trace-bg; + color: $white-light; font-family: $monospace_font; white-space: pre-wrap; overflow: auto; diff --git a/app/assets/stylesheets/pages/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss index 87c453a7a27..d1cd1e5d848 100644 --- a/app/assets/stylesheets/pages/ci_projects.scss +++ b/app/assets/stylesheets/pages/ci_projects.scss @@ -1,7 +1,7 @@ .ci-body { .project-title { margin: 0; - color: #444; + color: $common-gray-dark; font-size: 20px; line-height: 1.5; } diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index ddc9d0e2b1a..bf656d0e28e 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -5,7 +5,7 @@ .commit-author, .commit-committer { display: block; - color: #999; + color: $commit-committer-color; font-weight: normal; font-style: italic; } @@ -113,17 +113,17 @@ overflow: hidden; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987 .max-width-marker { width: 72ch; - color: rgba(0, 0, 0, 0.0); + color: $commit-max-width-marker-color; font-family: inherit; left: $left; height: 100%; - border-right: 1px solid mix($input-border, white); + border-right: 1px solid mix($input-border, $white-light); position: absolute; z-index: 1; } > textarea { - background-color: rgba(0, 0, 0, 0.0); + background-color: $commit-message-text-area-bg; font-family: inherit; padding-left: $left; position: relative; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 83ffa0e1d39..c29b5fdea78 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -8,8 +8,8 @@ .commit-header { padding: 5px 10px; background-color: $background-color; - border-top: 1px solid #eee; - border-bottom: 1px solid #eee; + border-top: 1px solid $gray-darker; + border-bottom: 1px solid $gray-darker; font-size: 14px; &:first-child { @@ -94,7 +94,7 @@ } &:not(:last-child) { - border-bottom: 1px solid #eee; + border-bottom: 1px solid $gray-darker; } a, @@ -201,7 +201,7 @@ .bar { position: absolute; height: 4px; - background-color: #ccc; + background-color: $divergence-graph-bar-bg; } .bar-behind { @@ -218,7 +218,7 @@ padding-top: 6px; padding-bottom: 0; font-size: 12px; - color: #333; + color: $gl-title-color; display: block; } @@ -239,6 +239,6 @@ height: 18px; margin: 5px 0 0; float: left; - background-color: #ccc; + background-color: $divergence-graph-separator-bg; } } diff --git a/app/assets/stylesheets/pages/confirmation.scss b/app/assets/stylesheets/pages/confirmation.scss index 81e5cee240d..8aab5e8231d 100644 --- a/app/assets/stylesheets/pages/confirmation.scss +++ b/app/assets/stylesheets/pages/confirmation.scss @@ -1,6 +1,6 @@ .well-confirmation { margin-bottom: 20px; - border-bottom: 1px solid #eee; + border-bottom: 1px solid $gray-darker; > h1, h2, diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 0b4930ec98f..e7a2c91003f 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -141,9 +141,9 @@ .dismiss-icon { position: absolute; - right: $cycle-analytics-box-padding; + right: $cycle-analytics-dismiss-icon-color; cursor: pointer; - color: #b2b2b2; + color: $cycle-analytics-dismiss-icon-color; } .svg-container { diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss index 016bab104eb..4421ed6a0b9 100644 --- a/app/assets/stylesheets/pages/dashboard.scss +++ b/app/assets/stylesheets/pages/dashboard.scss @@ -32,7 +32,7 @@ margin-bottom: 15px; i { - color: #888; + color: $dashboard-project-access-icon-color; } } diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 0f0c0abe7ae..80baebd5ea3 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -1,16 +1,16 @@ .detail-page-header { padding: $gl-padding-top 0; border-bottom: 1px solid $border-color; - color: #5c5d5e; + color: $gl-text-color-dark; font-size: 16px; line-height: 34px; .author { - color: #5c5d5e; + color: $gl-text-color-dark; } .identifier { - color: #5c5d5e; + color: $gl-text-color-dark; } .issue_created_ago, diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 99fdea15218..737f6e0f4be 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -14,7 +14,7 @@ background: $background-color; border-bottom: 1px solid $border-color; padding: 10px 16px; - color: #555; + color: $gl-diff-text-color; z-index: 10; border-radius: 3px 3px 0 0; @@ -24,7 +24,7 @@ display: block; .file-mode { - color: #777; + color: $file-mode-changed; } } @@ -49,8 +49,8 @@ .diff-content { overflow: auto; overflow-y: hidden; - background: #fff; - color: #333; + background: $white-light; + color: $gl-title-color; border-radius: 0 0 3px 3px; .unfold { @@ -59,7 +59,7 @@ .file-mode-changed { padding: 10px; - color: #777; + color: $file-mode-changed; } .suppressed-container { @@ -172,7 +172,7 @@ } .image { - background: #ddd; + background: $diff-image-bg; text-align: center; padding: 30px; @@ -182,13 +182,13 @@ .frame { display: inline-block; - background-color: #fff; + background-color: $white-light; line-height: 0; img { - border: 1px solid #fff; - background-image: linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%, #e5e5e5 100%), - linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%, #e5e5e5 100%); + border: 1px solid $white-light; + background-image: linear-gradient(45deg, $diff-image-img-bg 25%, transparent 25%, transparent 75%, $diff-image-img-bg 75%, $diff-image-img-bg 100%), + linear-gradient(45deg, $diff-image-img-bg 25%, transparent 25%, transparent 75%, $diff-image-img-bg 75%, $diff-image-img-bg 100%); background-size: 10px 10px; background-position: 0 0, 5px 5px; max-width: 100%; @@ -206,7 +206,7 @@ .image-info { font-size: 12px; margin: 5px 0 0; - color: grey; + color: $diff-image-info-color; } .view.swipe { @@ -220,7 +220,7 @@ .swipe-wrap { overflow: hidden; - border-left: 1px solid #999; + border-left: 1px solid $diff-swipe-border; position: absolute; display: block; top: 13px; @@ -350,7 +350,7 @@ .view-modes { padding: 10px; text-align: center; - background: #eee; + background: $gray-darker; ul, li { @@ -361,8 +361,8 @@ } li { - color: grey; - border-left: 1px solid #c1c1c1; + color: $diff-view-modes-color; + border-left: 1px solid $diff-view-modes-border; padding: 0 12px 0 16px; cursor: pointer; @@ -380,7 +380,7 @@ } cursor: default; - color: #333; + color: $gl-title-color; } &.disabled { diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 778126bcfb7..6cde9c592de 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -14,10 +14,10 @@ } .cancel-btn { - color: #b94a48; + color: $editor-cancel-color; &:hover { - color: #b94a48; + color: $editor-cancel-color; } } diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 3004959ff7b..dc67d411c71 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -62,7 +62,7 @@ border: none; background: $gray-light; border-radius: 0; - color: #777; + color: $events-pre-color; margin: 0 20px; overflow: hidden; } @@ -80,7 +80,7 @@ } .event-note-icon { - color: #777; + color: $events-pre-color; float: left; font-size: $gl-font-size; line-height: 16px; @@ -91,7 +91,7 @@ .event_icon { position: relative; float: right; - border: 1px solid #eee; + border: 1px solid $gray-darker; padding: 5px; border-radius: 5px; background: $gray-light; @@ -170,7 +170,7 @@ .event-body { margin: 0; - border-left: 2px solid #ddd; + border-left: 2px solid $events-body-border; padding-left: 10px; } @@ -186,4 +186,3 @@ display: none; } } - diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss index f7f9a9bb770..84da9180f93 100644 --- a/app/assets/stylesheets/pages/graph.scss +++ b/app/assets/stylesheets/pages/graph.scss @@ -2,15 +2,15 @@ border: 1px solid $border-color; .controls { - color: #888; + color: $project-network-controls-color; font-size: 14px; padding: 5px; border-bottom: 1px solid $border-color; - background: #eee; + background: $gray-darker; } .network-graph { - background: #fff; + background: $white-light; height: 500px; overflow-y: scroll; overflow-x: hidden; @@ -20,15 +20,14 @@ .graphs { .graph-author-email { float: right; - color: #777; + color: $graph-author-email-color; } .graph-additions { - color: #4a2; + color: $gl-text-green; } .graph-deletions { - color: #d12f19; + color: $gl-text-red; } } - diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index a48b4c65db8..e2e644dc23b 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -9,7 +9,7 @@ li { line-height: 24px; - color: #888; + color: $document-index-color; a { margin-right: 3px; @@ -20,7 +20,7 @@ .shortcut-mappings { font-size: 12px; - color: #555; + color: $help-shortcut-mapping-color; tbody:first-child tr:first-child { padding-top: 0; @@ -29,7 +29,7 @@ th { padding-top: 15px; line-height: 1.5; - color: #333; + color: $help-shortcut-header-color; text-align: left; } @@ -42,7 +42,7 @@ .shortcut { padding-right: 10px; - color: #999; + color: $help-shortcut-color; text-align: right; white-space: nowrap; } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 7aad99eee4e..90587b9425b 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -233,7 +233,7 @@ width: 100%; text-align: center; padding-bottom: 10px; - color: #999; + color: $issuable-sidebar-color; &:hover { color: $gl-gray; @@ -249,12 +249,12 @@ } .avatar:hover { - border-color: #999; + border-color: $issuable-avatar-hover-border; } .btn-clipboard { border: none; - color: #999; + color: $issuable-clipboard-color; &:hover { background: transparent; diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index eb171195309..3b47f99df2c 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -82,18 +82,18 @@ ul.related-merge-requests > li { .merge-request, .issue { &.today { - background: #f3fff2; - border-color: #e1e8d5; + background: $issues-today-bg; + border-color: $issues-today-border; } &.closed { background: $gray-light; - border-color: #e5e5e5; + border-color: $issues-border; } &.merged { background: $gray-light; - border-color: #e5e5e5; + border-color: $issues-border; } } diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index e39ce19f846..b1ccd644450 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -198,7 +198,7 @@ } .label-remove { - border-left: 1px solid rgba(0, 0, 0, .1); + border-left: 1px solid $label-remove-border; z-index: 3; } diff --git a/app/assets/stylesheets/pages/lint.scss b/app/assets/stylesheets/pages/lint.scss index 8290519dc25..8d30bd64278 100644 --- a/app/assets/stylesheets/pages/lint.scss +++ b/app/assets/stylesheets/pages/lint.scss @@ -1,11 +1,11 @@ .ci-body { .incorrect-syntax { font-size: 19px; - color: red; + color: $lint-incorrect-color; } .correct-syntax { font-size: 19px; - color: #47a447; + color: $lint-correct-color; } } diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index 54c89d75e94..dd27a06fcd2 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -18,7 +18,7 @@ p { font-size: 18px; - color: #888; + color: $login-brand-holder-color; } h1:first-child { @@ -174,7 +174,7 @@ .form-control { &:active, &:focus { - background-color: #fff; + background-color: $white-light; } } @@ -195,7 +195,7 @@ h2 { margin-top: 0; font-size: 14px; - color: #a00; + color: $login-devise-error-color; } } } @@ -254,4 +254,3 @@ } } } - diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index 19ab198c2e7..7a90713dd3f 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -1,3 +1,5 @@ +// Disabled to use the color map for creating color schemes +// scss-lint:disable ColorVariable $colors: ( white_header_head_neutral : #e1fad7, white_line_head_neutral : #effdec, @@ -98,6 +100,7 @@ $colors: ( solarized_dark_header_not_chosen : rgba(#839496, .25), solarized_dark_line_not_chosen : rgba(#839496, .15) ); +// scss-lint:enable ColorVariable @mixin color-scheme($color) { @@ -228,14 +231,15 @@ $colors: ( position: absolute; right: 10px; padding: 0; - color: #fff; + outline: none; + color: $white-light; width: 75px; // static width to make 2 buttons have same width height: 19px; } } .btn-success .fa-spinner { - color: #fff; + color: $white-light; } .editor-wrap { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index da1187af41c..1c6fe7afe14 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -86,7 +86,7 @@ } .normal { - color: #5c5d5e; + color: $gl-text-color-dark; } .js-deployment-link { @@ -143,7 +143,7 @@ } .mr-widget-footer { - border-top: 1px solid #eee; + border-top: 1px solid $gray-darker; } .ci-coverage { diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 65775c45e5b..ff092d53845 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -132,7 +132,7 @@ font-size: 15px; .md-area { - background-color: #fff; + background-color: $white-light; } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 56a798a7b6d..16b099c09eb 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -3,9 +3,9 @@ */ @-webkit-keyframes targe3-note { - from { background: #fffff0; } - 50% { background: #ffffd3; } - to { background: #fffff0; } + from { background: $note-targe3-outside; } + 50% { background: $note-targe3-inside; } + to { background: $note-targe3-outside; } } ul.notes { @@ -305,7 +305,7 @@ ul.notes { &.notes_line2 { text-align: center; padding: 10px 0; - border-left: 1px solid #ddd !important; + border-left: 1px solid $note-line2-border !important; } &.notes_content { @@ -471,7 +471,7 @@ ul.notes { .add-diff-note { margin-top: -4px; border-radius: 40px; - background: #fff; + background: $white-light; padding: 4px; font-size: 16px; color: $gl-link-color; @@ -484,7 +484,7 @@ ul.notes { &:hover { background: $gl-info; - color: #fff; + color: $white-light; @include show-add-diff-note; } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 1cf7587dbb4..62862c72b3b 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -8,7 +8,7 @@ .no-ssh-key-message, .project-limit-message { - background-color: #f28d35; + background-color: $project-limit-message-bg; margin-bottom: 0; } @@ -76,7 +76,7 @@ &.static-namespace { height: 35px; border-radius: 3px; - border: 1px solid #e5e5e5; + border: 1px solid $border-color; } &+ .select2 a { @@ -225,7 +225,7 @@ left: 0; margin-top: -6px; border-width: 7px 5px 7px 0; - border-right-color: #dce0e5; + border-right-color: $count-arrow-border; pointer-events: none; } @@ -240,7 +240,7 @@ left: 1px; margin-top: -9px; border-width: 10px 7px 10px 0; - border-right-color: #fff; + border-right-color: $white-light; pointer-events: none; } } @@ -248,7 +248,7 @@ .count { @include btn-gray; display: inline-block; - background: white; + background: $white-light; border-radius: 2px; border-width: 1px; border-style: solid; @@ -270,7 +270,7 @@ } &:hover { - background: #fff; + background: $white-light; } } } @@ -302,7 +302,7 @@ .option-descr { margin-left: 29px; - color: #54565b; + color: $project-option-descr-color; } } } @@ -310,7 +310,7 @@ .save-project-loader { margin-top: 50px; margin-bottom: 50px; - color: #555; + color: $save-project-loader-color; } .transfer-project .select2-container { @@ -373,7 +373,7 @@ a.deploy-project-label { > li + li::before { padding: 0 3px; - color: #999; + color: $project-breadcrumb-color; } a { @@ -549,20 +549,20 @@ a.deploy-project-label { } pre.light-well { - border-color: #f1f1f1; + border-color: $well-light-border; } .git-empty { margin: 0 7px 7px; h5 { - color: #5c5d5e; + color: $gl-text-color-dark; } .light-well { border-radius: 2px; - color: #5b6169; + color: $well-light-text-color; font-size: 13px; line-height: 1.6em; } @@ -716,7 +716,7 @@ pre.light-well { .form-control { @extend .monospace; - background: #fff; + background: $white-light; font-size: 14px; margin-left: -1px; cursor: auto; @@ -726,17 +726,17 @@ pre.light-well { .cannot-be-merged, .cannot-be-merged:hover { - color: #e62958; + color: $error-exclamation-point; margin-top: 2px; } .private-forks-notice .private-fork-icon { i:nth-child(1) { - color: #2aa056; + color: $project-private-forks-notice-odd; } i:nth-child(2) { - color: #fff; + color: $white-light; } } diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss index 7b3878c91df..9b6ff237557 100644 --- a/app/assets/stylesheets/pages/runners.scss +++ b/app/assets/stylesheets/pages/runners.scss @@ -1,27 +1,27 @@ .runner-state { padding: 6px 12px; margin-right: 10px; - color: #fff; + color: $white-light; &.runner-state-shared { - background: #32b186; + background: $runner-state-shared-bg; } &.runner-state-specific { - background: #3498db; + background: $runner-state-specific-bg; } } .runner-status-online { - color: green; + color: $runner-status-online-color; } .runner-status-offline { - color: gray; + color: $runner-status-offline-color; } .runner-status-paused { - color: red; + color: $runner-status-paused-color; } .runner { diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss index 779db77da60..dfa4d033fb8 100644 --- a/app/assets/stylesheets/pages/stat_graph.scss +++ b/app/assets/stylesheets/pages/stat_graph.scss @@ -1,16 +1,16 @@ .tint-box { - background: #f3f3f3; + background: $stat-graph-common-bg; position: relative; margin-bottom: 10px; } .area { - fill: #1db34f; + fill: $stat-graph-area-fill; fill-opacity: 0.5; } .axis { - fill: #aaa; + fill: $stat-graph-axis-fill; font-size: 10px; } @@ -44,19 +44,19 @@ .person .spark { display: block; - background: #f3f3f3; + background: $stat-graph-common-bg; width: 100%; } .person .area-contributor { - fill: #f17f49; + fill: $stat-graph-orange-fill; } } .selection rect { - fill: #333; + fill: $stat-graph-selection-fill; fill-opacity: 0.1; - stroke: #333; + stroke: $stat-graph-selection-stroke; stroke-width: 1px; stroke-opacity: 0.4; shape-rendering: crispedges; diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 4c258bae1f4..5084b466722 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -2,7 +2,7 @@ .ci-status { padding: 2px 7px; margin-right: 10px; - border: 1px solid #eee; + border: 1px solid $gray-darker; white-space: nowrap; border-radius: 4px; diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index b3aef2fdd32..508b30f3947 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -11,7 +11,7 @@ background: $todo-alert-blue; margin-left: -17px; font-size: 11px; - color: white; + color: $white-light; padding: 3px; padding-top: 1px; padding-bottom: 1px; @@ -81,7 +81,7 @@ word-wrap: break-word; .md { - color: #7f8fa4; + color: $gl-grayish-blue; font-size: $gl-font-size; .label { @@ -90,7 +90,7 @@ } p { - color: #5c5d5e; + color: $gl-text-color-dark; } } @@ -102,7 +102,7 @@ border: none; background: $gray-light; border-radius: 0; - color: #777; + color: $todo-body-pre-color; margin: 0 20px; overflow: hidden; } @@ -146,7 +146,7 @@ .todo-body { margin: 0; - border-left: 2px solid #ddd; + border-left: 2px solid $todo-body-border; padding-left: 10px; } } diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss index e73cecc92be..8c87bc3cafd 100644 --- a/app/assets/stylesheets/pages/ui_dev_kit.scss +++ b/app/assets/stylesheets/pages/ui_dev_kit.scss @@ -7,11 +7,11 @@ .example { &::before { content: "Example"; - color: #bbb; + color: $ui-dev-kit-example-color; } padding: 15px; - border: 1px dashed #ddd; + border: 1px dashed $ui-dev-kit-example-border; margin-bottom: 15px; } } diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss index 3fa7fa3d7e3..9f9d630978a 100644 --- a/app/assets/stylesheets/pages/xterm.scss +++ b/app/assets/stylesheets/pages/xterm.scss @@ -20,6 +20,266 @@ $l-cyan: #8abeb7; $l-white: $ci-text-color; + /* + * xterm colors + */ + $xterm-fg-0: $black; + $xterm-fg-1: #800000; + $xterm-fg-2: #008000; + $xterm-fg-3: #808000; + $xterm-fg-4: #000080; + $xterm-fg-5: #800080; + $xterm-fg-6: #008080; + $xterm-fg-7: #c0c0c0; + $xterm-fg-8: #808080; + $xterm-fg-9: #f00; + $xterm-fg-10: #0f0; + $xterm-fg-11: #ff0; + $xterm-fg-12: #00f; + $xterm-fg-13: #f0f; + $xterm-fg-14: #0ff; + $xterm-fg-15: $white-light; + $xterm-fg-16: $black; + $xterm-fg-17: #00005f; + $xterm-fg-18: #000087; + $xterm-fg-19: #0000af; + $xterm-fg-20: #0000d7; + $xterm-fg-21: #00f; + $xterm-fg-22: #005f00; + $xterm-fg-23: #005f5f; + $xterm-fg-24: #005f87; + $xterm-fg-25: #005faf; + $xterm-fg-26: #005fd7; + $xterm-fg-27: #005fff; + $xterm-fg-28: #008700; + $xterm-fg-29: #00875f; + $xterm-fg-30: #008787; + $xterm-fg-31: #0087af; + $xterm-fg-32: #0087d7; + $xterm-fg-33: #0087ff; + $xterm-fg-34: #00af00; + $xterm-fg-35: #00af5f; + $xterm-fg-36: #00af87; + $xterm-fg-37: #00afaf; + $xterm-fg-38: #00afd7; + $xterm-fg-39: #00afff; + $xterm-fg-40: #00d700; + $xterm-fg-41: #00d75f; + $xterm-fg-42: #00d787; + $xterm-fg-43: #00d7af; + $xterm-fg-44: #00d7d7; + $xterm-fg-45: #00d7ff; + $xterm-fg-46: #0f0; + $xterm-fg-47: #00ff5f; + $xterm-fg-48: #00ff87; + $xterm-fg-49: #00ffaf; + $xterm-fg-50: #00ffd7; + $xterm-fg-51: #0ff; + $xterm-fg-52: #5f0000; + $xterm-fg-53: #5f005f; + $xterm-fg-54: #5f0087; + $xterm-fg-55: #5f00af; + $xterm-fg-56: #5f00d7; + $xterm-fg-57: #5f00ff; + $xterm-fg-58: #5f5f00; + $xterm-fg-59: #5f5f5f; + $xterm-fg-60: #5f5f87; + $xterm-fg-61: #5f5faf; + $xterm-fg-62: #5f5fd7; + $xterm-fg-63: #5f5fff; + $xterm-fg-64: #5f8700; + $xterm-fg-65: #5f875f; + $xterm-fg-66: #5f8787; + $xterm-fg-67: #5f87af; + $xterm-fg-68: #5f87d7; + $xterm-fg-69: #5f87ff; + $xterm-fg-70: #5faf00; + $xterm-fg-71: #5faf5f; + $xterm-fg-72: #5faf87; + $xterm-fg-73: #5fafaf; + $xterm-fg-74: #5fafd7; + $xterm-fg-75: #5fafff; + $xterm-fg-76: #5fd700; + $xterm-fg-77: #5fd75f; + $xterm-fg-78: #5fd787; + $xterm-fg-79: #5fd7af; + $xterm-fg-80: #5fd7d7; + $xterm-fg-81: #5fd7ff; + $xterm-fg-82: #5fff00; + $xterm-fg-83: #5fff5f; + $xterm-fg-84: #5fff87; + $xterm-fg-85: #5fffaf; + $xterm-fg-86: #5fffd7; + $xterm-fg-87: #5fffff; + $xterm-fg-88: #870000; + $xterm-fg-89: #87005f; + $xterm-fg-90: #870087; + $xterm-fg-91: #8700af; + $xterm-fg-92: #8700d7; + $xterm-fg-93: #8700ff; + $xterm-fg-94: #875f00; + $xterm-fg-95: #875f5f; + $xterm-fg-96: #875f87; + $xterm-fg-97: #875faf; + $xterm-fg-98: #875fd7; + $xterm-fg-99: #875fff; + $xterm-fg-100: #878700; + $xterm-fg-101: #87875f; + $xterm-fg-102: #878787; + $xterm-fg-103: #8787af; + $xterm-fg-104: #8787d7; + $xterm-fg-105: #8787ff; + $xterm-fg-106: #87af00; + $xterm-fg-107: #87af5f; + $xterm-fg-108: #87af87; + $xterm-fg-109: #87afaf; + $xterm-fg-110: #87afd7; + $xterm-fg-111: #87afff; + $xterm-fg-112: #87d700; + $xterm-fg-113: #87d75f; + $xterm-fg-114: #87d787; + $xterm-fg-115: #87d7af; + $xterm-fg-116: #87d7d7; + $xterm-fg-117: #87d7ff; + $xterm-fg-118: #87ff00; + $xterm-fg-119: #87ff5f; + $xterm-fg-120: #87ff87; + $xterm-fg-121: #87ffaf; + $xterm-fg-122: #87ffd7; + $xterm-fg-123: #87ffff; + $xterm-fg-124: #af0000; + $xterm-fg-125: #af005f; + $xterm-fg-126: #af0087; + $xterm-fg-127: #af00af; + $xterm-fg-128: #af00d7; + $xterm-fg-129: #af00ff; + $xterm-fg-130: #af5f00; + $xterm-fg-131: #af5f5f; + $xterm-fg-132: #af5f87; + $xterm-fg-133: #af5faf; + $xterm-fg-134: #af5fd7; + $xterm-fg-135: #af5fff; + $xterm-fg-136: #af8700; + $xterm-fg-137: #af875f; + $xterm-fg-138: #af8787; + $xterm-fg-139: #af87af; + $xterm-fg-140: #af87d7; + $xterm-fg-141: #af87ff; + $xterm-fg-142: #afaf00; + $xterm-fg-143: #afaf5f; + $xterm-fg-144: #afaf87; + $xterm-fg-145: #afafaf; + $xterm-fg-146: #afafd7; + $xterm-fg-147: #afafff; + $xterm-fg-148: #afd700; + $xterm-fg-149: #afd75f; + $xterm-fg-150: #afd787; + $xterm-fg-151: #afd7af; + $xterm-fg-152: #afd7d7; + $xterm-fg-153: #afd7ff; + $xterm-fg-154: #afff00; + $xterm-fg-155: #afff5f; + $xterm-fg-156: #afff87; + $xterm-fg-157: #afffaf; + $xterm-fg-158: #afffd7; + $xterm-fg-159: #afffff; + $xterm-fg-160: #d70000; + $xterm-fg-161: #d7005f; + $xterm-fg-162: #d70087; + $xterm-fg-163: #d700af; + $xterm-fg-164: #d700d7; + $xterm-fg-165: #d700ff; + $xterm-fg-166: #d75f00; + $xterm-fg-167: #d75f5f; + $xterm-fg-168: #d75f87; + $xterm-fg-169: #d75faf; + $xterm-fg-170: #d75fd7; + $xterm-fg-171: #d75fff; + $xterm-fg-172: #d78700; + $xterm-fg-173: #d7875f; + $xterm-fg-174: #d78787; + $xterm-fg-175: #d787af; + $xterm-fg-176: #d787d7; + $xterm-fg-177: #d787ff; + $xterm-fg-178: #d7af00; + $xterm-fg-179: #d7af5f; + $xterm-fg-180: #d7af87; + $xterm-fg-181: #d7afaf; + $xterm-fg-182: #d7afd7; + $xterm-fg-183: #d7afff; + $xterm-fg-184: #d7d700; + $xterm-fg-185: #d7d75f; + $xterm-fg-186: #d7d787; + $xterm-fg-187: #d7d7af; + $xterm-fg-188: #d7d7d7; + $xterm-fg-189: #d7d7ff; + $xterm-fg-190: #d7ff00; + $xterm-fg-191: #d7ff5f; + $xterm-fg-192: #d7ff87; + $xterm-fg-193: #d7ffaf; + $xterm-fg-194: #d7ffd7; + $xterm-fg-195: #d7ffff; + $xterm-fg-196: #f00; + $xterm-fg-197: #ff005f; + $xterm-fg-198: #ff0087; + $xterm-fg-199: #ff00af; + $xterm-fg-200: #ff00d7; + $xterm-fg-201: #f0f; + $xterm-fg-202: #ff5f00; + $xterm-fg-203: #ff5f5f; + $xterm-fg-204: #ff5f87; + $xterm-fg-205: #ff5faf; + $xterm-fg-206: #ff5fd7; + $xterm-fg-207: #ff5fff; + $xterm-fg-208: #ff8700; + $xterm-fg-209: #ff875f; + $xterm-fg-210: #ff8787; + $xterm-fg-211: #ff87af; + $xterm-fg-212: #ff87d7; + $xterm-fg-213: #ff87ff; + $xterm-fg-214: #ffaf00; + $xterm-fg-215: #ffaf5f; + $xterm-fg-216: #ffaf87; + $xterm-fg-217: #ffafaf; + $xterm-fg-218: #ffafd7; + $xterm-fg-219: #ffafff; + $xterm-fg-220: #ffd700; + $xterm-fg-221: #ffd75f; + $xterm-fg-222: #ffd787; + $xterm-fg-223: #ffd7af; + $xterm-fg-224: #ffd7d7; + $xterm-fg-225: #ffd7ff; + $xterm-fg-226: #ff0; + $xterm-fg-227: #ffff5f; + $xterm-fg-228: #ffff87; + $xterm-fg-229: #ffffaf; + $xterm-fg-230: #ffffd7; + $xterm-fg-231: $white-light; + $xterm-fg-232: #080808; + $xterm-fg-233: #121212; + $xterm-fg-234: #1c1c1c; + $xterm-fg-235: #262626; + $xterm-fg-236: #303030; + $xterm-fg-237: #3a3a3a; + $xterm-fg-238: #444; + $xterm-fg-239: #4e4e4e; + $xterm-fg-240: #585858; + $xterm-fg-241: #626262; + $xterm-fg-242: #6c6c6c; + $xterm-fg-243: #767676; + $xterm-fg-244: #808080; + $xterm-fg-245: #8a8a8a; + $xterm-fg-246: #949494; + $xterm-fg-247: #9e9e9e; + $xterm-fg-248: #a8a8a8; + $xterm-fg-249: #b2b2b2; + $xterm-fg-250: #bcbcbc; + $xterm-fg-251: #c6c6c6; + $xterm-fg-252: #d0d0d0; + $xterm-fg-253: #dadada; + $xterm-fg-254: #e4e4e4; + $xterm-fg-255: #eee; + .term-bold { font-weight: bold; } @@ -169,1026 +429,1026 @@ } .xterm-fg-0 { - color: #000; + color: $xterm-fg-0; } .xterm-fg-1 { - color: #800000; + color: $xterm-fg-1; } .xterm-fg-2 { - color: #008000; + color: $xterm-fg-2; } .xterm-fg-3 { - color: #808000; + color: $xterm-fg-3; } .xterm-fg-4 { - color: #000080; + color: $xterm-fg-4; } .xterm-fg-5 { - color: #800080; + color: $xterm-fg-5; } .xterm-fg-6 { - color: #008080; + color: $xterm-fg-6; } .xterm-fg-7 { - color: #c0c0c0; + color: $xterm-fg-7; } .xterm-fg-8 { - color: #808080; + color: $xterm-fg-8; } .xterm-fg-9 { - color: #f00; + color: $xterm-fg-9; } .xterm-fg-10 { - color: #0f0; + color: $xterm-fg-10; } .xterm-fg-11 { - color: #ff0; + color: $xterm-fg-11; } .xterm-fg-12 { - color: #00f; + color: $xterm-fg-12; } .xterm-fg-13 { - color: #f0f; + color: $xterm-fg-13; } .xterm-fg-14 { - color: #0ff; + color: $xterm-fg-14; } .xterm-fg-15 { - color: #fff; + color: $white-light; } .xterm-fg-16 { - color: #000; + color: $black; } .xterm-fg-17 { - color: #00005f; + color: $xterm-fg-17; } .xterm-fg-18 { - color: #000087; + color: $xterm-fg-18; } .xterm-fg-19 { - color: #0000af; + color: $xterm-fg-19; } .xterm-fg-20 { - color: #0000d7; + color: $xterm-fg-20; } .xterm-fg-21 { - color: #00f; + color: $xterm-fg-21; } .xterm-fg-22 { - color: #005f00; + color: $xterm-fg-22; } .xterm-fg-23 { - color: #005f5f; + color: $xterm-fg-23; } .xterm-fg-24 { - color: #005f87; + color: $xterm-fg-24; } .xterm-fg-25 { - color: #005faf; + color: $xterm-fg-25; } .xterm-fg-26 { - color: #005fd7; + color: $xterm-fg-26; } .xterm-fg-27 { - color: #005fff; + color: $xterm-fg-27; } .xterm-fg-28 { - color: #008700; + color: $xterm-fg-28; } .xterm-fg-29 { - color: #00875f; + color: $xterm-fg-29; } .xterm-fg-30 { - color: #008787; + color: $xterm-fg-30; } .xterm-fg-31 { - color: #0087af; + color: $xterm-fg-31; } .xterm-fg-32 { - color: #0087d7; + color: $xterm-fg-32; } .xterm-fg-33 { - color: #0087ff; + color: $xterm-fg-33; } .xterm-fg-34 { - color: #00af00; + color: $xterm-fg-34; } .xterm-fg-35 { - color: #00af5f; + color: $xterm-fg-35; } .xterm-fg-36 { - color: #00af87; + color: $xterm-fg-36; } .xterm-fg-37 { - color: #00afaf; + color: $xterm-fg-37; } .xterm-fg-38 { - color: #00afd7; + color: $xterm-fg-38; } .xterm-fg-39 { - color: #00afff; + color: $xterm-fg-39; } .xterm-fg-40 { - color: #00d700; + color: $xterm-fg-40; } .xterm-fg-41 { - color: #00d75f; + color: $xterm-fg-41; } .xterm-fg-42 { - color: #00d787; + color: $xterm-fg-42; } .xterm-fg-43 { - color: #00d7af; + color: $xterm-fg-43; } .xterm-fg-44 { - color: #00d7d7; + color: $xterm-fg-44; } .xterm-fg-45 { - color: #00d7ff; + color: $xterm-fg-45; } .xterm-fg-46 { - color: #0f0; + color: $xterm-fg-46; } .xterm-fg-47 { - color: #00ff5f; + color: $xterm-fg-47; } .xterm-fg-48 { - color: #00ff87; + color: $xterm-fg-48; } .xterm-fg-49 { - color: #00ffaf; + color: $xterm-fg-49; } .xterm-fg-50 { - color: #00ffd7; + color: $xterm-fg-50; } .xterm-fg-51 { - color: #0ff; + color: $xterm-fg-51; } .xterm-fg-52 { - color: #5f0000; + color: $xterm-fg-52; } .xterm-fg-53 { - color: #5f005f; + color: $xterm-fg-53; } .xterm-fg-54 { - color: #5f0087; + color: $xterm-fg-54; } .xterm-fg-55 { - color: #5f00af; + color: $xterm-fg-55; } .xterm-fg-56 { - color: #5f00d7; + color: $xterm-fg-56; } .xterm-fg-57 { - color: #5f00ff; + color: $xterm-fg-57; } .xterm-fg-58 { - color: #5f5f00; + color: $xterm-fg-58; } .xterm-fg-59 { - color: #5f5f5f; + color: $xterm-fg-59; } .xterm-fg-60 { - color: #5f5f87; + color: $xterm-fg-60; } .xterm-fg-61 { - color: #5f5faf; + color: $xterm-fg-61; } .xterm-fg-62 { - color: #5f5fd7; + color: $xterm-fg-62; } .xterm-fg-63 { - color: #5f5fff; + color: $xterm-fg-63; } .xterm-fg-64 { - color: #5f8700; + color: $xterm-fg-64; } .xterm-fg-65 { - color: #5f875f; + color: $xterm-fg-65; } .xterm-fg-66 { - color: #5f8787; + color: $xterm-fg-66; } .xterm-fg-67 { - color: #5f87af; + color: $xterm-fg-67; } .xterm-fg-68 { - color: #5f87d7; + color: $xterm-fg-68; } .xterm-fg-69 { - color: #5f87ff; + color: $xterm-fg-69; } .xterm-fg-70 { - color: #5faf00; + color: $xterm-fg-70; } .xterm-fg-71 { - color: #5faf5f; + color: $xterm-fg-71; } .xterm-fg-72 { - color: #5faf87; + color: $xterm-fg-72; } .xterm-fg-73 { - color: #5fafaf; + color: $xterm-fg-73; } .xterm-fg-74 { - color: #5fafd7; + color: $xterm-fg-74; } .xterm-fg-75 { - color: #5fafff; + color: $xterm-fg-75; } .xterm-fg-76 { - color: #5fd700; + color: $xterm-fg-76; } .xterm-fg-77 { - color: #5fd75f; + color: $xterm-fg-77; } .xterm-fg-78 { - color: #5fd787; + color: $xterm-fg-78; } .xterm-fg-79 { - color: #5fd7af; + color: $xterm-fg-79; } .xterm-fg-80 { - color: #5fd7d7; + color: $xterm-fg-80; } .xterm-fg-81 { - color: #5fd7ff; + color: $xterm-fg-81; } .xterm-fg-82 { - color: #5fff00; + color: $xterm-fg-82; } .xterm-fg-83 { - color: #5fff5f; + color: $xterm-fg-83; } .xterm-fg-84 { - color: #5fff87; + color: $xterm-fg-84; } .xterm-fg-85 { - color: #5fffaf; + color: $xterm-fg-85; } .xterm-fg-86 { - color: #5fffd7; + color: $xterm-fg-86; } .xterm-fg-87 { - color: #5fffff; + color: $xterm-fg-87; } .xterm-fg-88 { - color: #870000; + color: $xterm-fg-88; } .xterm-fg-89 { - color: #87005f; + color: $xterm-fg-89; } .xterm-fg-90 { - color: #870087; + color: $xterm-fg-90; } .xterm-fg-91 { - color: #8700af; + color: $xterm-fg-91; } .xterm-fg-92 { - color: #8700d7; + color: $xterm-fg-92; } .xterm-fg-93 { - color: #8700ff; + color: $xterm-fg-93; } .xterm-fg-94 { - color: #875f00; + color: $xterm-fg-94; } .xterm-fg-95 { - color: #875f5f; + color: $xterm-fg-95; } .xterm-fg-96 { - color: #875f87; + color: $xterm-fg-96; } .xterm-fg-97 { - color: #875faf; + color: $xterm-fg-97; } .xterm-fg-98 { - color: #875fd7; + color: $xterm-fg-98; } .xterm-fg-99 { - color: #875fff; + color: $xterm-fg-99; } .xterm-fg-100 { - color: #878700; + color: $xterm-fg-100; } .xterm-fg-101 { - color: #87875f; + color: $xterm-fg-101; } .xterm-fg-102 { - color: #878787; + color: $xterm-fg-102; } .xterm-fg-103 { - color: #8787af; + color: $xterm-fg-103; } .xterm-fg-104 { - color: #8787d7; + color: $xterm-fg-104; } .xterm-fg-105 { - color: #8787ff; + color: $xterm-fg-105; } .xterm-fg-106 { - color: #87af00; + color: $xterm-fg-106; } .xterm-fg-107 { - color: #87af5f; + color: $xterm-fg-107; } .xterm-fg-108 { - color: #87af87; + color: $xterm-fg-108; } .xterm-fg-109 { - color: #87afaf; + color: $xterm-fg-109; } .xterm-fg-110 { - color: #87afd7; + color: $xterm-fg-110; } .xterm-fg-111 { - color: #87afff; + color: $xterm-fg-111; } .xterm-fg-112 { - color: #87d700; + color: $xterm-fg-112; } .xterm-fg-113 { - color: #87d75f; + color: $xterm-fg-113; } .xterm-fg-114 { - color: #87d787; + color: $xterm-fg-114; } .xterm-fg-115 { - color: #87d7af; + color: $xterm-fg-115; } .xterm-fg-116 { - color: #87d7d7; + color: $xterm-fg-116; } .xterm-fg-117 { - color: #87d7ff; + color: $xterm-fg-117; } .xterm-fg-118 { - color: #87ff00; + color: $xterm-fg-118; } .xterm-fg-119 { - color: #87ff5f; + color: $xterm-fg-119; } .xterm-fg-120 { - color: #87ff87; + color: $xterm-fg-120; } .xterm-fg-121 { - color: #87ffaf; + color: $xterm-fg-121; } .xterm-fg-122 { - color: #87ffd7; + color: $xterm-fg-122; } .xterm-fg-123 { - color: #87ffff; + color: $xterm-fg-123; } .xterm-fg-124 { - color: #af0000; + color: $xterm-fg-124; } .xterm-fg-125 { - color: #af005f; + color: $xterm-fg-125; } .xterm-fg-126 { - color: #af0087; + color: $xterm-fg-126; } .xterm-fg-127 { - color: #af00af; + color: $xterm-fg-127; } .xterm-fg-128 { - color: #af00d7; + color: $xterm-fg-128; } .xterm-fg-129 { - color: #af00ff; + color: $xterm-fg-129; } .xterm-fg-130 { - color: #af5f00; + color: $xterm-fg-130; } .xterm-fg-131 { - color: #af5f5f; + color: $xterm-fg-131; } .xterm-fg-132 { - color: #af5f87; + color: $xterm-fg-132; } .xterm-fg-133 { - color: #af5faf; + color: $xterm-fg-133; } .xterm-fg-134 { - color: #af5fd7; + color: $xterm-fg-134; } .xterm-fg-135 { - color: #af5fff; + color: $xterm-fg-135; } .xterm-fg-136 { - color: #af8700; + color: $xterm-fg-136; } .xterm-fg-137 { - color: #af875f; + color: $xterm-fg-137; } .xterm-fg-138 { - color: #af8787; + color: $xterm-fg-138; } .xterm-fg-139 { - color: #af87af; + color: $xterm-fg-139; } .xterm-fg-140 { - color: #af87d7; + color: $xterm-fg-140; } .xterm-fg-141 { - color: #af87ff; + color: $xterm-fg-141; } .xterm-fg-142 { - color: #afaf00; + color: $xterm-fg-142; } .xterm-fg-143 { - color: #afaf5f; + color: $xterm-fg-143; } .xterm-fg-144 { - color: #afaf87; + color: $xterm-fg-144; } .xterm-fg-145 { - color: #afafaf; + color: $xterm-fg-145; } .xterm-fg-146 { - color: #afafd7; + color: $xterm-fg-146; } .xterm-fg-147 { - color: #afafff; + color: $xterm-fg-147; } .xterm-fg-148 { - color: #afd700; + color: $xterm-fg-148; } .xterm-fg-149 { - color: #afd75f; + color: $xterm-fg-149; } .xterm-fg-150 { - color: #afd787; + color: $xterm-fg-150; } .xterm-fg-151 { - color: #afd7af; + color: $xterm-fg-151; } .xterm-fg-152 { - color: #afd7d7; + color: $xterm-fg-152; } .xterm-fg-153 { - color: #afd7ff; + color: $xterm-fg-153; } .xterm-fg-154 { - color: #afff00; + color: $xterm-fg-154; } .xterm-fg-155 { - color: #afff5f; + color: $xterm-fg-155; } .xterm-fg-156 { - color: #afff87; + color: $xterm-fg-156; } .xterm-fg-157 { - color: #afffaf; + color: $xterm-fg-157; } .xterm-fg-158 { - color: #afffd7; + color: $xterm-fg-158; } .xterm-fg-159 { - color: #afffff; + color: $xterm-fg-159; } .xterm-fg-160 { - color: #d70000; + color: $xterm-fg-160; } .xterm-fg-161 { - color: #d7005f; + color: $xterm-fg-161; } .xterm-fg-162 { - color: #d70087; + color: $xterm-fg-162; } .xterm-fg-163 { - color: #d700af; + color: $xterm-fg-163; } .xterm-fg-164 { - color: #d700d7; + color: $xterm-fg-164; } .xterm-fg-165 { - color: #d700ff; + color: $xterm-fg-165; } .xterm-fg-166 { - color: #d75f00; + color: $xterm-fg-166; } .xterm-fg-167 { - color: #d75f5f; + color: $xterm-fg-167; } .xterm-fg-168 { - color: #d75f87; + color: $xterm-fg-168; } .xterm-fg-169 { - color: #d75faf; + color: $xterm-fg-169; } .xterm-fg-170 { - color: #d75fd7; + color: $xterm-fg-170; } .xterm-fg-171 { - color: #d75fff; + color: $xterm-fg-171; } .xterm-fg-172 { - color: #d78700; + color: $xterm-fg-172; } .xterm-fg-173 { - color: #d7875f; + color: $xterm-fg-173; } .xterm-fg-174 { - color: #d78787; + color: $xterm-fg-174; } .xterm-fg-175 { - color: #d787af; + color: $xterm-fg-175; } .xterm-fg-176 { - color: #d787d7; + color: $xterm-fg-176; } .xterm-fg-177 { - color: #d787ff; + color: $xterm-fg-177; } .xterm-fg-178 { - color: #d7af00; + color: $xterm-fg-178; } .xterm-fg-179 { - color: #d7af5f; + color: $xterm-fg-179; } .xterm-fg-180 { - color: #d7af87; + color: $xterm-fg-180; } .xterm-fg-181 { - color: #d7afaf; + color: $xterm-fg-181; } .xterm-fg-182 { - color: #d7afd7; + color: $xterm-fg-182; } .xterm-fg-183 { - color: #d7afff; + color: $xterm-fg-183; } .xterm-fg-184 { - color: #d7d700; + color: $xterm-fg-184; } .xterm-fg-185 { - color: #d7d75f; + color: $xterm-fg-185; } .xterm-fg-186 { - color: #d7d787; + color: $xterm-fg-186; } .xterm-fg-187 { - color: #d7d7af; + color: $xterm-fg-187; } .xterm-fg-188 { - color: #d7d7d7; + color: $xterm-fg-188; } .xterm-fg-189 { - color: #d7d7ff; + color: $xterm-fg-189; } .xterm-fg-190 { - color: #d7ff00; + color: $xterm-fg-190; } .xterm-fg-191 { - color: #d7ff5f; + color: $xterm-fg-191; } .xterm-fg-192 { - color: #d7ff87; + color: $xterm-fg-192; } .xterm-fg-193 { - color: #d7ffaf; + color: $xterm-fg-193; } .xterm-fg-194 { - color: #d7ffd7; + color: $xterm-fg-194; } .xterm-fg-195 { - color: #d7ffff; + color: $xterm-fg-195; } .xterm-fg-196 { - color: #f00; + color: $xterm-fg-196; } .xterm-fg-197 { - color: #ff005f; + color: $xterm-fg-197; } .xterm-fg-198 { - color: #ff0087; + color: $xterm-fg-198; } .xterm-fg-199 { - color: #ff00af; + color: $xterm-fg-199; } .xterm-fg-200 { - color: #ff00d7; + color: $xterm-fg-200; } .xterm-fg-201 { - color: #f0f; + color: $xterm-fg-201; } .xterm-fg-202 { - color: #ff5f00; + color: $xterm-fg-202; } .xterm-fg-203 { - color: #ff5f5f; + color: $xterm-fg-203; } .xterm-fg-204 { - color: #ff5f87; + color: $xterm-fg-204; } .xterm-fg-205 { - color: #ff5faf; + color: $xterm-fg-205; } .xterm-fg-206 { - color: #ff5fd7; + color: $xterm-fg-206; } .xterm-fg-207 { - color: #ff5fff; + color: $xterm-fg-207; } .xterm-fg-208 { - color: #ff8700; + color: $xterm-fg-208; } .xterm-fg-209 { - color: #ff875f; + color: $xterm-fg-209; } .xterm-fg-210 { - color: #ff8787; + color: $xterm-fg-210; } .xterm-fg-211 { - color: #ff87af; + color: $xterm-fg-211; } .xterm-fg-212 { - color: #ff87d7; + color: $xterm-fg-212; } .xterm-fg-213 { - color: #ff87ff; + color: $xterm-fg-213; } .xterm-fg-214 { - color: #ffaf00; + color: $xterm-fg-214; } .xterm-fg-215 { - color: #ffaf5f; + color: $xterm-fg-215; } .xterm-fg-216 { - color: #ffaf87; + color: $xterm-fg-216; } .xterm-fg-217 { - color: #ffafaf; + color: $xterm-fg-217; } .xterm-fg-218 { - color: #ffafd7; + color: $xterm-fg-218; } .xterm-fg-219 { - color: #ffafff; + color: $xterm-fg-219; } .xterm-fg-220 { - color: #ffd700; + color: $xterm-fg-220; } .xterm-fg-221 { - color: #ffd75f; + color: $xterm-fg-221; } .xterm-fg-222 { - color: #ffd787; + color: $xterm-fg-222; } .xterm-fg-223 { - color: #ffd7af; + color: $xterm-fg-223; } .xterm-fg-224 { - color: #ffd7d7; + color: $xterm-fg-224; } .xterm-fg-225 { - color: #ffd7ff; + color: $xterm-fg-225; } .xterm-fg-226 { - color: #ff0; + color: $xterm-fg-226; } .xterm-fg-227 { - color: #ffff5f; + color: $xterm-fg-227; } .xterm-fg-228 { - color: #ffff87; + color: $xterm-fg-228; } .xterm-fg-229 { - color: #ffffaf; + color: $xterm-fg-229; } .xterm-fg-230 { - color: #ffffd7; + color: $xterm-fg-230; } .xterm-fg-231 { - color: #fff; + color: $xterm-fg-231; } .xterm-fg-232 { - color: #080808; + color: $xterm-fg-232; } .xterm-fg-233 { - color: #121212; + color: $xterm-fg-233; } .xterm-fg-234 { - color: #1c1c1c; + color: $xterm-fg-234; } .xterm-fg-235 { - color: #262626; + color: $xterm-fg-235; } .xterm-fg-236 { - color: #303030; + color: $xterm-fg-236; } .xterm-fg-237 { - color: #3a3a3a; + color: $xterm-fg-237; } .xterm-fg-238 { - color: #444; + color: $xterm-fg-238; } .xterm-fg-239 { - color: #4e4e4e; + color: $xterm-fg-239; } .xterm-fg-240 { - color: #585858; + color: $xterm-fg-240; } .xterm-fg-241 { - color: #626262; + color: $xterm-fg-241; } .xterm-fg-242 { - color: #6c6c6c; + color: $xterm-fg-242; } .xterm-fg-243 { - color: #767676; + color: $xterm-fg-243; } .xterm-fg-244 { - color: #808080; + color: $xterm-fg-244; } .xterm-fg-245 { - color: #8a8a8a; + color: $xterm-fg-245; } .xterm-fg-246 { - color: #949494; + color: $xterm-fg-246; } .xterm-fg-247 { - color: #9e9e9e; + color: $xterm-fg-247; } .xterm-fg-248 { - color: #a8a8a8; + color: $xterm-fg-248; } .xterm-fg-249 { - color: #b2b2b2; + color: $xterm-fg-249; } .xterm-fg-250 { - color: #bcbcbc; + color: $xterm-fg-250; } .xterm-fg-251 { - color: #c6c6c6; + color: $xterm-fg-251; } .xterm-fg-252 { - color: #d0d0d0; + color: $xterm-fg-252; } .xterm-fg-253 { - color: #dadada; + color: $xterm-fg-253; } .xterm-fg-254 { - color: #e4e4e4; + color: $xterm-fg-254; } .xterm-fg-255 { - color: #eee; + color: $xterm-fg-255; } } diff --git a/changelogs/unreleased/23500-enable-colorvariable.yml b/changelogs/unreleased/23500-enable-colorvariable.yml new file mode 100644 index 00000000000..98e22a934b8 --- /dev/null +++ b/changelogs/unreleased/23500-enable-colorvariable.yml @@ -0,0 +1,4 @@ +--- +title: Enable ColorVariable in scss-lint +merge_request: +author: Sam Rose -- cgit v1.2.1 From 215b9f733c097bc1eac7ca3b37302c3594904a28 Mon Sep 17 00:00:00 2001 From: DJ Mountney <david@twkie.net> Date: Thu, 1 Dec 2016 17:24:29 -0800 Subject: Add changelog entry for seeding the runner request token --- changelogs/unreleased/seed-runner-token.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/seed-runner-token.yml diff --git a/changelogs/unreleased/seed-runner-token.yml b/changelogs/unreleased/seed-runner-token.yml new file mode 100644 index 00000000000..e8153be043a --- /dev/null +++ b/changelogs/unreleased/seed-runner-token.yml @@ -0,0 +1,5 @@ +--- +title: Add support for setting the GitLab Runners Registration Token during initial + database seeding +merge_request: 6642 +author: -- cgit v1.2.1 From ebef1a8441b451e2718860538c6e6261ea616bf8 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Fri, 2 Dec 2016 08:55:53 +0100 Subject: Remove change to MWBS in code review guideline [ci skip] --- doc/development/code_review.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 52ee633c43a..e1fb8102b67 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -70,7 +70,7 @@ experience, refactors the existing code). Then: - After a round of line notes, it can be helpful to post a summary note such as "LGTM :thumbsup:", or "Just a couple things to address." - Avoid accepting a merge request before the build succeeds. Of course, "Merge - When Pipeline Succeeds" is fine. + When Build Succeeds" (MWBS) is fine. - If you set the MR to "Merge When Build Succeeds", you should take over subsequent revisions for anything that would be spotted after that. -- cgit v1.2.1 From 8235f8301571a9b55a14b94b2ed687e53cbc2f4e Mon Sep 17 00:00:00 2001 From: Jacopo <beschi.jacopo@gmail.com> Date: Sun, 6 Nov 2016 19:41:23 +0100 Subject: Fixed Wrong Tab Selected When Loggin Fails And Multiple Login Tabs Exists When ldap is enabled and use "Standard" authentication method, if authentication fails the correct tab remain selected. This is done by saving into localStorage when the active tab changes and by always selecting that tab when the page is loaded. --- .eslintrc | 3 +- app/assets/javascripts/dispatcher.js.es6 | 1 + app/assets/javascripts/signin_tabs_memoizer.js.es6 | 49 ++++++++++++++++++++ app/views/devise/sessions/new.html.haml | 1 + .../15081-wrong-login-tab-ldap-frontend.yml | 4 ++ spec/javascripts/fixtures/signin_tabs.html.haml | 5 ++ spec/javascripts/signin_tabs_memoizer_spec.js.es6 | 53 ++++++++++++++++++++++ 7 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/signin_tabs_memoizer.js.es6 create mode 100644 changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml create mode 100644 spec/javascripts/fixtures/signin_tabs.html.haml create mode 100644 spec/javascripts/signin_tabs_memoizer_spec.js.es6 diff --git a/.eslintrc b/.eslintrc index b80dcec9d1d..e13f76b213c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,7 +8,8 @@ "globals": { "_": false, "gl": false, - "gon": false + "gon": false, + "localStorage": false }, "plugins": [ "filenames" diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 16df4b0b005..fb366e2eb88 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -24,6 +24,7 @@ switch (page) { case 'sessions:new': new UsernameValidator(); + new ActiveTabMemoizer(); break; case 'projects:boards:show': case 'projects:boards:index': diff --git a/app/assets/javascripts/signin_tabs_memoizer.js.es6 b/app/assets/javascripts/signin_tabs_memoizer.js.es6 new file mode 100644 index 00000000000..d811d1cd53a --- /dev/null +++ b/app/assets/javascripts/signin_tabs_memoizer.js.es6 @@ -0,0 +1,49 @@ +/* eslint no-param-reassign: ["error", { "props": false }]*/ +/* eslint no-new: "off" */ +((global) => { + /** + * Memorize the last selected tab after reloading a page. + * Does that setting the current selected tab in the localStorage + */ + class ActiveTabMemoizer { + constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) { + this.currentTabKey = currentTabKey; + this.tabSelector = tabSelector; + this.bootstrap(); + } + + bootstrap() { + const tabs = document.querySelectorAll(this.tabSelector); + if (tabs.length > 0) { + tabs[0].addEventListener('click', (e) => { + if (e.target && e.target.nodeName === 'A') { + const anchorName = e.target.getAttribute('href'); + this.saveData(anchorName); + } + }); + } + + this.showTab(); + } + + showTab() { + const anchorName = this.readData(); + if (anchorName) { + const tab = document.querySelector(`${this.tabSelector} a[href="${anchorName}"]`); + if (tab) { + tab.click(); + } + } + } + + saveData(val) { + localStorage.setItem(this.currentTabKey, val); + } + + readData() { + return localStorage.getItem(this.currentTabKey); + } + } + + global.ActiveTabMemoizer = ActiveTabMemoizer; +})(window); diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index fa8e7979461..af87129e49e 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -1,4 +1,5 @@ - page_title "Sign in" + %div - if form_based_providers.any? = render 'devise/shared/tabs_ldap' diff --git a/changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml b/changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml new file mode 100644 index 00000000000..19c76b5b437 --- /dev/null +++ b/changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml @@ -0,0 +1,4 @@ +--- +title: Fix wrong tab selected when loggin fails and multiple login tabs exists +merge_request: 7314 +author: Jacopo Beschi @jacopo-beschi diff --git a/spec/javascripts/fixtures/signin_tabs.html.haml b/spec/javascripts/fixtures/signin_tabs.html.haml new file mode 100644 index 00000000000..12b8d423cbe --- /dev/null +++ b/spec/javascripts/fixtures/signin_tabs.html.haml @@ -0,0 +1,5 @@ +%ul.nav-tabs + %li + %a.active{ id: 'standard', href: '#standard'} Standard + %li + %a{ id: 'ldap', href: '#ldap'} Ldap diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js.es6 b/spec/javascripts/signin_tabs_memoizer_spec.js.es6 new file mode 100644 index 00000000000..9a9fb22255b --- /dev/null +++ b/spec/javascripts/signin_tabs_memoizer_spec.js.es6 @@ -0,0 +1,53 @@ +/*= require signin_tabs_memoizer */ + +((global) => { + describe('SigninTabsMemoizer', () => { + const fixtureTemplate = 'signin_tabs.html'; + const tabSelector = 'ul.nav-tabs'; + const currentTabKey = 'current_signin_tab'; + let memo; + + function createMemoizer() { + memo = new global.ActiveTabMemoizer({ + currentTabKey, + tabSelector, + }); + return memo; + } + + fixture.preload(fixtureTemplate); + + beforeEach(() => { + fixture.load(fixtureTemplate); + }); + + it('does nothing if no tab was previously selected', () => { + createMemoizer(); + + expect(document.querySelector('li a.active').getAttribute('id')).toEqual('standard'); + }); + + it('shows last selected tab on boot', () => { + createMemoizer().saveData('#ldap'); + const fakeTab = { + click: () => {}, + }; + spyOn(document, 'querySelector').and.returnValue(fakeTab); + spyOn(fakeTab, 'click'); + + memo.bootstrap(); + + // verify that triggers click on the last selected tab + expect(document.querySelector).toHaveBeenCalledWith(`${tabSelector} a[href="#ldap"]`); + expect(fakeTab.click).toHaveBeenCalled(); + }); + + it('saves last selected tab on change', () => { + createMemoizer(); + + document.getElementById('standard').click(); + + expect(memo.readData()).toEqual('#standard'); + }); + }); +})(window); -- cgit v1.2.1 From be797097df258f306dca1cf821b7ed9b46e7919f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Fri, 2 Dec 2016 11:17:48 +0100 Subject: Add scaffold for each class of core CI status --- lib/gitlab/ci/status/core/canceled.rb | 8 ++++++++ lib/gitlab/ci/status/core/created.rb | 8 ++++++++ lib/gitlab/ci/status/core/failed.rb | 8 ++++++++ lib/gitlab/ci/status/core/pending.rb | 8 ++++++++ lib/gitlab/ci/status/core/running.rb | 8 ++++++++ lib/gitlab/ci/status/core/skipped.rb | 8 ++++++++ lib/gitlab/ci/status/core/success.rb | 8 ++++++++ 7 files changed, 56 insertions(+) create mode 100644 lib/gitlab/ci/status/core/canceled.rb create mode 100644 lib/gitlab/ci/status/core/created.rb create mode 100644 lib/gitlab/ci/status/core/failed.rb create mode 100644 lib/gitlab/ci/status/core/pending.rb create mode 100644 lib/gitlab/ci/status/core/running.rb create mode 100644 lib/gitlab/ci/status/core/skipped.rb create mode 100644 lib/gitlab/ci/status/core/success.rb diff --git a/lib/gitlab/ci/status/core/canceled.rb b/lib/gitlab/ci/status/core/canceled.rb new file mode 100644 index 00000000000..3dcddd6e3ef --- /dev/null +++ b/lib/gitlab/ci/status/core/canceled.rb @@ -0,0 +1,8 @@ +module Gitlab::Ci + module Status + module Core + class Canceled + end + end + end +end diff --git a/lib/gitlab/ci/status/core/created.rb b/lib/gitlab/ci/status/core/created.rb new file mode 100644 index 00000000000..590f14d6b57 --- /dev/null +++ b/lib/gitlab/ci/status/core/created.rb @@ -0,0 +1,8 @@ +module Gitlab::Ci + module Status + module Core + class Created + end + end + end +end diff --git a/lib/gitlab/ci/status/core/failed.rb b/lib/gitlab/ci/status/core/failed.rb new file mode 100644 index 00000000000..d5af40b53cb --- /dev/null +++ b/lib/gitlab/ci/status/core/failed.rb @@ -0,0 +1,8 @@ +module Gitlab::Ci + module Status + module Core + class Failed + end + end + end +end diff --git a/lib/gitlab/ci/status/core/pending.rb b/lib/gitlab/ci/status/core/pending.rb new file mode 100644 index 00000000000..ef57886234e --- /dev/null +++ b/lib/gitlab/ci/status/core/pending.rb @@ -0,0 +1,8 @@ +module Gitlab::Ci + module Status + module Core + class Pending + end + end + end +end diff --git a/lib/gitlab/ci/status/core/running.rb b/lib/gitlab/ci/status/core/running.rb new file mode 100644 index 00000000000..0b027f4dc9c --- /dev/null +++ b/lib/gitlab/ci/status/core/running.rb @@ -0,0 +1,8 @@ +module Gitlab::Ci + module Status + module Core + class Running + end + end + end +end diff --git a/lib/gitlab/ci/status/core/skipped.rb b/lib/gitlab/ci/status/core/skipped.rb new file mode 100644 index 00000000000..b8b07a69156 --- /dev/null +++ b/lib/gitlab/ci/status/core/skipped.rb @@ -0,0 +1,8 @@ +module Gitlab::Ci + module Status + module Core + class Skipped + end + end + end +end diff --git a/lib/gitlab/ci/status/core/success.rb b/lib/gitlab/ci/status/core/success.rb new file mode 100644 index 00000000000..511a34901f8 --- /dev/null +++ b/lib/gitlab/ci/status/core/success.rb @@ -0,0 +1,8 @@ +module Gitlab::Ci + module Status + module Core + class Success + end + end + end +end -- cgit v1.2.1 From 09b35c537c74ae307e42d1fc0979bc4dbbdc52d8 Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Fri, 2 Dec 2016 18:18:02 +0800 Subject: Speed up project snippet security request specs Part of https://gitlab.com/gitlab-org/gitlab-ce/issues/24899 See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7779 and https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7813 --- .../project/snippet/internal_access_spec.rb | 124 +++++++--------- .../project/snippet/private_access_spec.rb | 86 +++++------ .../security/project/snippet/public_access_spec.rb | 162 ++++++++++----------- 3 files changed, 168 insertions(+), 204 deletions(-) diff --git a/spec/features/security/project/snippet/internal_access_spec.rb b/spec/features/security/project/snippet/internal_access_spec.rb index 49deacc5c74..2659b3ee3ec 100644 --- a/spec/features/security/project/snippet/internal_access_spec.rb +++ b/spec/features/security/project/snippet/internal_access_spec.rb @@ -5,76 +5,64 @@ describe "Internal Project Snippets Access", feature: true do let(:project) { create(:empty_project, :internal) } - let(:owner) { project.owner } - let(:master) { create(:user) } - let(:developer) { create(:user) } - let(:reporter) { create(:user) } - let(:guest) { create(:user) } - let(:internal_snippet) { create(:project_snippet, :internal, project: project, author: owner) } - let(:private_snippet) { create(:project_snippet, :private, project: project, author: owner) } - - before do - project.team << [master, :master] - project.team << [developer, :developer] - project.team << [reporter, :reporter] - project.team << [guest, :guest] - end + let(:internal_snippet) { create(:project_snippet, :internal, project: project, author: project.owner) } + let(:private_snippet) { create(:project_snippet, :private, project: project, author: project.owner) } describe "GET /:project_path/snippets" do subject { namespace_project_snippets_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/snippets/new" do subject { new_namespace_project_snippet_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/snippets/:id" do context "for an internal snippet" do subject { namespace_project_snippet_path(project.namespace, project, internal_snippet) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end context "for a private snippet" do subject { namespace_project_snippet_path(project.namespace, project, private_snippet) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end end @@ -82,29 +70,29 @@ describe "Internal Project Snippets Access", feature: true do context "for an internal snippet" do subject { raw_namespace_project_snippet_path(project.namespace, project, internal_snippet) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end context "for a private snippet" do subject { raw_namespace_project_snippet_path(project.namespace, project, private_snippet) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end end end diff --git a/spec/features/security/project/snippet/private_access_spec.rb b/spec/features/security/project/snippet/private_access_spec.rb index a1bfc076d99..6eb9f163bd5 100644 --- a/spec/features/security/project/snippet/private_access_spec.rb +++ b/spec/features/security/project/snippet/private_access_spec.rb @@ -5,73 +5,61 @@ describe "Private Project Snippets Access", feature: true do let(:project) { create(:empty_project, :private) } - let(:owner) { project.owner } - let(:master) { create(:user) } - let(:developer) { create(:user) } - let(:reporter) { create(:user) } - let(:guest) { create(:user) } - let(:private_snippet) { create(:project_snippet, :private, project: project, author: owner) } - - before do - project.team << [master, :master] - project.team << [developer, :developer] - project.team << [reporter, :reporter] - project.team << [guest, :guest] - end + let(:private_snippet) { create(:project_snippet, :private, project: project, author: project.owner) } describe "GET /:project_path/snippets" do subject { namespace_project_snippets_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/snippets/new" do subject { new_namespace_project_snippet_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/snippets/:id for a private snippet" do subject { namespace_project_snippet_path(project.namespace, project, private_snippet) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/snippets/:id/raw for a private snippet" do subject { raw_namespace_project_snippet_path(project.namespace, project, private_snippet) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end end diff --git a/spec/features/security/project/snippet/public_access_spec.rb b/spec/features/security/project/snippet/public_access_spec.rb index 30bcd87ef04..f3329d0bc96 100644 --- a/spec/features/security/project/snippet/public_access_spec.rb +++ b/spec/features/security/project/snippet/public_access_spec.rb @@ -5,91 +5,79 @@ describe "Public Project Snippets Access", feature: true do let(:project) { create(:empty_project, :public) } - let(:owner) { project.owner } - let(:master) { create(:user) } - let(:developer) { create(:user) } - let(:reporter) { create(:user) } - let(:guest) { create(:user) } - let(:public_snippet) { create(:project_snippet, :public, project: project, author: owner) } - let(:internal_snippet) { create(:project_snippet, :internal, project: project, author: owner) } - let(:private_snippet) { create(:project_snippet, :private, project: project, author: owner) } - - before do - project.team << [master, :master] - project.team << [developer, :developer] - project.team << [reporter, :reporter] - project.team << [guest, :guest] - end + let(:public_snippet) { create(:project_snippet, :public, project: project, author: project.owner) } + let(:internal_snippet) { create(:project_snippet, :internal, project: project, author: project.owner) } + let(:private_snippet) { create(:project_snippet, :private, project: project, author: project.owner) } describe "GET /:project_path/snippets" do subject { namespace_project_snippets_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :external } - it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:external) } + it { is_expected.to be_allowed_for(:visitor) } end describe "GET /:project_path/snippets/new" do subject { new_namespace_project_snippet_path(project.namespace, project) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/snippets/:id" do context "for a public snippet" do subject { namespace_project_snippet_path(project.namespace, project, public_snippet) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :external } - it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:external) } + it { is_expected.to be_allowed_for(:visitor) } end context "for an internal snippet" do subject { namespace_project_snippet_path(project.namespace, project, internal_snippet) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end context "for a private snippet" do subject { namespace_project_snippet_path(project.namespace, project, private_snippet) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end end @@ -97,43 +85,43 @@ describe "Public Project Snippets Access", feature: true do context "for a public snippet" do subject { raw_namespace_project_snippet_path(project.namespace, project, public_snippet) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :external } - it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:external) } + it { is_expected.to be_allowed_for(:visitor) } end context "for an internal snippet" do subject { raw_namespace_project_snippet_path(project.namespace, project, internal_snippet) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end context "for a private snippet" do subject { raw_namespace_project_snippet_path(project.namespace, project, private_snippet) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end end end -- cgit v1.2.1 From 614fcfc4df6226f84c5b7e10864df45b24e72e46 Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Fri, 2 Dec 2016 18:31:57 +0800 Subject: Simplify `branch_name` in bin/changelog The `--short` option has existed since at least 1.8.1: https://git-scm.com/docs/git-symbolic-ref/1.8.1 --- bin/changelog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/changelog b/bin/changelog index b6586ebb6aa..e07b1ad237a 100755 --- a/bin/changelog +++ b/bin/changelog @@ -158,7 +158,7 @@ class ChangelogEntry end def branch_name - @branch_name ||= %x{git symbolic-ref HEAD}.strip.sub(%r{\Arefs/heads/}, '') + @branch_name ||= %x{git symbolic-ref --short HEAD}.strip end end -- cgit v1.2.1 From 5df3e8b81b4115f44b1ee56a3531dca6d912756c Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Fri, 2 Dec 2016 12:14:10 +0100 Subject: Add initial implmentation for core success status --- lib/gitlab/ci/status/core/success.rb | 7 +++++++ spec/lib/gitlab/ci/status/core/success_spec.rb | 11 +++++++++++ 2 files changed, 18 insertions(+) create mode 100644 spec/lib/gitlab/ci/status/core/success_spec.rb diff --git a/lib/gitlab/ci/status/core/success.rb b/lib/gitlab/ci/status/core/success.rb index 511a34901f8..7bae4f612cc 100644 --- a/lib/gitlab/ci/status/core/success.rb +++ b/lib/gitlab/ci/status/core/success.rb @@ -2,6 +2,13 @@ module Gitlab::Ci module Status module Core class Success + def label + 'passed' + end + + def icon + 'success' + end end end end diff --git a/spec/lib/gitlab/ci/status/core/success_spec.rb b/spec/lib/gitlab/ci/status/core/success_spec.rb new file mode 100644 index 00000000000..80587956336 --- /dev/null +++ b/spec/lib/gitlab/ci/status/core/success_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Core::Success do + describe '#label' do + it { expect(subject.label).to eq 'passed' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'success' } + end +end -- cgit v1.2.1 From d4ed5b2e0c7fc94309499a0a268c543a82e00e9b Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Fri, 2 Dec 2016 12:38:07 +0100 Subject: Add abstract base class for core CI status --- lib/gitlab/ci/status/core/base.rb | 41 ++++++++++++++++++++++++++ lib/gitlab/ci/status/core/success.rb | 2 +- spec/lib/gitlab/ci/status/core/success_spec.rb | 2 ++ 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 lib/gitlab/ci/status/core/base.rb diff --git a/lib/gitlab/ci/status/core/base.rb b/lib/gitlab/ci/status/core/base.rb new file mode 100644 index 00000000000..96d09dcbfc5 --- /dev/null +++ b/lib/gitlab/ci/status/core/base.rb @@ -0,0 +1,41 @@ +module Gitlab::Ci + module Status + module Core + # Base abstract class fore core status + # + class Base + def initialize(subject) + @subject = subject + end + + def icon + raise NotImplementedError + end + + def label + raise NotImplementedError + end + + def has_details? + raise NotImplementedError + end + + def details_path + raise NotImplementedError + end + + def has_action? + raise NotImplementedError + end + + def action_icon + raise NotImplementedError + end + + def action_path + raise NotImplementedError + end + end + end + end +end diff --git a/lib/gitlab/ci/status/core/success.rb b/lib/gitlab/ci/status/core/success.rb index 7bae4f612cc..e32a5228619 100644 --- a/lib/gitlab/ci/status/core/success.rb +++ b/lib/gitlab/ci/status/core/success.rb @@ -1,7 +1,7 @@ module Gitlab::Ci module Status module Core - class Success + class Success < Core::Base def label 'passed' end diff --git a/spec/lib/gitlab/ci/status/core/success_spec.rb b/spec/lib/gitlab/ci/status/core/success_spec.rb index 80587956336..93a656a46cd 100644 --- a/spec/lib/gitlab/ci/status/core/success_spec.rb +++ b/spec/lib/gitlab/ci/status/core/success_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Gitlab::Ci::Status::Core::Success do + subject { described_class.new(double('subject')) } + describe '#label' do it { expect(subject.label).to eq 'passed' } end -- cgit v1.2.1 From c9f65322f81588c663e6799cef963b02424054a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Fri, 2 Dec 2016 12:36:05 +0100 Subject: Patch upgrade documentation should be consulted on stable branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [ci skip] Signed-off-by: Rémy Coutable <remy@rymai.me> --- doc/update/patch_versions.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index e98c40ca4c0..685972cfb41 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -1,7 +1,10 @@ # Universal update guide for patch versions -*Make sure you view this [upgrade guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/patch_versions.md) from the `master` branch for the most up to date instructions.* -For example from 7.14.0 to 7.14.3, also see the [semantic versioning specification](http://semver.org/). +## Select Version to Install + +Make sure you view [this update guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/patch_versions.md) from the tag (version) of GitLab you would like to install. +In most cases this should be the highest numbered production tag (without rc in it). +You can select the tag in the version dropdown in the top left corner of GitLab (below the menu bar). ### 0. Backup -- cgit v1.2.1 From f5d7a61760d8f60c27b8838db826468768154733 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Fri, 2 Dec 2016 14:03:30 +0200 Subject: Fixes ActionView::Template::Error: undefined method `text?` for nil:NilClass --- ...view-template-error-undefined-method-text-for-nil-nilclass.yml | 5 +++++ lib/gitlab/diff/file_collection/merge_request_diff.rb | 2 +- spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb | 8 ++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/25251-actionview-template-error-undefined-method-text-for-nil-nilclass.yml diff --git a/changelogs/unreleased/25251-actionview-template-error-undefined-method-text-for-nil-nilclass.yml b/changelogs/unreleased/25251-actionview-template-error-undefined-method-text-for-nil-nilclass.yml new file mode 100644 index 00000000000..7f1c417bc77 --- /dev/null +++ b/changelogs/unreleased/25251-actionview-template-error-undefined-method-text-for-nil-nilclass.yml @@ -0,0 +1,5 @@ +--- +title: 'Fixes "ActionView::Template::Error: undefined method `text?` for nil:NilClass" + on MR pages' +merge_request: +author: diff --git a/lib/gitlab/diff/file_collection/merge_request_diff.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb index 26bb0bc16f5..56530448f36 100644 --- a/lib/gitlab/diff/file_collection/merge_request_diff.rb +++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb @@ -61,7 +61,7 @@ module Gitlab end def cacheable?(diff_file) - @merge_request_diff.present? && diff_file.blob.text? + @merge_request_diff.present? && diff_file.blob && diff_file.blob.text? end def cache_key diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb index c863a5f04cc..2a680f03476 100644 --- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb +++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb @@ -10,4 +10,12 @@ describe Gitlab::Diff::FileCollection::MergeRequestDiff do described_class.new(merge_request.merge_request_diff, diff_options: nil).diff_files end + + it 'does not hightlight file if blob is not accessable' do + allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(nil) + + expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines) + + described_class.new(merge_request.merge_request_diff, diff_options: nil).diff_files + end end -- cgit v1.2.1 From 943b3d0e0007d2f48a64ffdef6bf0ff0fcb774f2 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Fri, 2 Dec 2016 13:08:21 +0100 Subject: Implement the rest of core CI statuses with specs --- lib/gitlab/ci/status/core/base.rb | 4 ++++ lib/gitlab/ci/status/core/canceled.rb | 9 ++++++++- lib/gitlab/ci/status/core/created.rb | 9 ++++++++- lib/gitlab/ci/status/core/failed.rb | 9 ++++++++- lib/gitlab/ci/status/core/pending.rb | 9 ++++++++- lib/gitlab/ci/status/core/running.rb | 9 ++++++++- lib/gitlab/ci/status/core/skipped.rb | 9 ++++++++- lib/gitlab/ci/status/core/success.rb | 2 +- spec/lib/gitlab/ci/status/core/canceled_spec.rb | 17 +++++++++++++++++ spec/lib/gitlab/ci/status/core/created_spec.rb | 17 +++++++++++++++++ spec/lib/gitlab/ci/status/core/failed_spec.rb | 17 +++++++++++++++++ spec/lib/gitlab/ci/status/core/pending_spec.rb | 17 +++++++++++++++++ spec/lib/gitlab/ci/status/core/running_spec.rb | 17 +++++++++++++++++ spec/lib/gitlab/ci/status/core/skipped_spec.rb | 17 +++++++++++++++++ spec/lib/gitlab/ci/status/core/success_spec.rb | 6 +++++- 15 files changed, 160 insertions(+), 8 deletions(-) create mode 100644 spec/lib/gitlab/ci/status/core/canceled_spec.rb create mode 100644 spec/lib/gitlab/ci/status/core/created_spec.rb create mode 100644 spec/lib/gitlab/ci/status/core/failed_spec.rb create mode 100644 spec/lib/gitlab/ci/status/core/pending_spec.rb create mode 100644 spec/lib/gitlab/ci/status/core/running_spec.rb create mode 100644 spec/lib/gitlab/ci/status/core/skipped_spec.rb diff --git a/lib/gitlab/ci/status/core/base.rb b/lib/gitlab/ci/status/core/base.rb index 96d09dcbfc5..f7687c49ffd 100644 --- a/lib/gitlab/ci/status/core/base.rb +++ b/lib/gitlab/ci/status/core/base.rb @@ -16,6 +16,10 @@ module Gitlab::Ci raise NotImplementedError end + def title + "#{@subject.class.name.demodulize}: #{label}" + end + def has_details? raise NotImplementedError end diff --git a/lib/gitlab/ci/status/core/canceled.rb b/lib/gitlab/ci/status/core/canceled.rb index 3dcddd6e3ef..5e06b946a99 100644 --- a/lib/gitlab/ci/status/core/canceled.rb +++ b/lib/gitlab/ci/status/core/canceled.rb @@ -1,7 +1,14 @@ module Gitlab::Ci module Status module Core - class Canceled + class Canceled < Core::Base + def label + 'canceled' + end + + def icon + 'icon_status_canceled' + end end end end diff --git a/lib/gitlab/ci/status/core/created.rb b/lib/gitlab/ci/status/core/created.rb index 590f14d6b57..c116f9a97f1 100644 --- a/lib/gitlab/ci/status/core/created.rb +++ b/lib/gitlab/ci/status/core/created.rb @@ -1,7 +1,14 @@ module Gitlab::Ci module Status module Core - class Created + class Created < Core::Base + def label + 'created' + end + + def icon + 'icon_status_created' + end end end end diff --git a/lib/gitlab/ci/status/core/failed.rb b/lib/gitlab/ci/status/core/failed.rb index d5af40b53cb..467ef71e819 100644 --- a/lib/gitlab/ci/status/core/failed.rb +++ b/lib/gitlab/ci/status/core/failed.rb @@ -1,7 +1,14 @@ module Gitlab::Ci module Status module Core - class Failed + class Failed < Core::Base + def label + 'failed' + end + + def icon + 'icon_status_failed' + end end end end diff --git a/lib/gitlab/ci/status/core/pending.rb b/lib/gitlab/ci/status/core/pending.rb index ef57886234e..05c9e41091b 100644 --- a/lib/gitlab/ci/status/core/pending.rb +++ b/lib/gitlab/ci/status/core/pending.rb @@ -1,7 +1,14 @@ module Gitlab::Ci module Status module Core - class Pending + class Pending < Core::Base + def label + 'pending' + end + + def icon + 'icon_status_pending' + end end end end diff --git a/lib/gitlab/ci/status/core/running.rb b/lib/gitlab/ci/status/core/running.rb index 0b027f4dc9c..01f0c57ef5f 100644 --- a/lib/gitlab/ci/status/core/running.rb +++ b/lib/gitlab/ci/status/core/running.rb @@ -1,7 +1,14 @@ module Gitlab::Ci module Status module Core - class Running + class Running < Core::Base + def label + 'running' + end + + def icon + 'icon_status_running' + end end end end diff --git a/lib/gitlab/ci/status/core/skipped.rb b/lib/gitlab/ci/status/core/skipped.rb index b8b07a69156..e791341b7e0 100644 --- a/lib/gitlab/ci/status/core/skipped.rb +++ b/lib/gitlab/ci/status/core/skipped.rb @@ -1,7 +1,14 @@ module Gitlab::Ci module Status module Core - class Skipped + class Skipped < Core::Base + def label + 'skipped' + end + + def icon + 'icon_status_skipped' + end end end end diff --git a/lib/gitlab/ci/status/core/success.rb b/lib/gitlab/ci/status/core/success.rb index e32a5228619..bcfe7e63a6c 100644 --- a/lib/gitlab/ci/status/core/success.rb +++ b/lib/gitlab/ci/status/core/success.rb @@ -7,7 +7,7 @@ module Gitlab::Ci end def icon - 'success' + 'icon_status_success' end end end diff --git a/spec/lib/gitlab/ci/status/core/canceled_spec.rb b/spec/lib/gitlab/ci/status/core/canceled_spec.rb new file mode 100644 index 00000000000..d03b7a18aa0 --- /dev/null +++ b/spec/lib/gitlab/ci/status/core/canceled_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Core::Canceled do + subject { described_class.new(double('subject')) } + + describe '#label' do + it { expect(subject.label).to eq 'canceled' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_canceled' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: canceled' } + end +end diff --git a/spec/lib/gitlab/ci/status/core/created_spec.rb b/spec/lib/gitlab/ci/status/core/created_spec.rb new file mode 100644 index 00000000000..5a6d7523e83 --- /dev/null +++ b/spec/lib/gitlab/ci/status/core/created_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Core::Created do + subject { described_class.new(double('subject')) } + + describe '#label' do + it { expect(subject.label).to eq 'created' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_created' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: created' } + end +end diff --git a/spec/lib/gitlab/ci/status/core/failed_spec.rb b/spec/lib/gitlab/ci/status/core/failed_spec.rb new file mode 100644 index 00000000000..0b7e03f1684 --- /dev/null +++ b/spec/lib/gitlab/ci/status/core/failed_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Core::Failed do + subject { described_class.new(double('subject')) } + + describe '#label' do + it { expect(subject.label).to eq 'failed' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_failed' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: failed' } + end +end diff --git a/spec/lib/gitlab/ci/status/core/pending_spec.rb b/spec/lib/gitlab/ci/status/core/pending_spec.rb new file mode 100644 index 00000000000..95f2255e66d --- /dev/null +++ b/spec/lib/gitlab/ci/status/core/pending_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Core::Pending do + subject { described_class.new(double('subject')) } + + describe '#label' do + it { expect(subject.label).to eq 'pending' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_pending' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: pending' } + end +end diff --git a/spec/lib/gitlab/ci/status/core/running_spec.rb b/spec/lib/gitlab/ci/status/core/running_spec.rb new file mode 100644 index 00000000000..648de678618 --- /dev/null +++ b/spec/lib/gitlab/ci/status/core/running_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Core::Running do + subject { described_class.new(double('subject')) } + + describe '#label' do + it { expect(subject.label).to eq 'running' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_running' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: running' } + end +end diff --git a/spec/lib/gitlab/ci/status/core/skipped_spec.rb b/spec/lib/gitlab/ci/status/core/skipped_spec.rb new file mode 100644 index 00000000000..dc2b56a1e34 --- /dev/null +++ b/spec/lib/gitlab/ci/status/core/skipped_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Core::Skipped do + subject { described_class.new(double('subject')) } + + describe '#label' do + it { expect(subject.label).to eq 'skipped' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_skipped' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: skipped' } + end +end diff --git a/spec/lib/gitlab/ci/status/core/success_spec.rb b/spec/lib/gitlab/ci/status/core/success_spec.rb index 93a656a46cd..fc67e58adb5 100644 --- a/spec/lib/gitlab/ci/status/core/success_spec.rb +++ b/spec/lib/gitlab/ci/status/core/success_spec.rb @@ -8,6 +8,10 @@ describe Gitlab::Ci::Status::Core::Success do end describe '#icon' do - it { expect(subject.icon).to eq 'success' } + it { expect(subject.icon).to eq 'icon_status_success' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: passed' } end end -- cgit v1.2.1 From 63e2d6528e3213742656758fc065aa55601885c8 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" <lbennett@gitlab.com> Date: Fri, 25 Nov 2016 15:23:47 +0000 Subject: Added element extensions spec for .matches and .closest --- spec/javascripts/extensions/element_spec.js.es6 | 38 +++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 spec/javascripts/extensions/element_spec.js.es6 diff --git a/spec/javascripts/extensions/element_spec.js.es6 b/spec/javascripts/extensions/element_spec.js.es6 new file mode 100644 index 00000000000..c5b86d35204 --- /dev/null +++ b/spec/javascripts/extensions/element_spec.js.es6 @@ -0,0 +1,38 @@ +/*= require extensions/element */ + +(() => { + describe('Element extensions', function () { + beforeEach(() => { + this.element = document.createElement('ul'); + }); + + describe('matches', () => { + it('returns true if element matches the selector', () => { + expect(this.element.matches('ul')).toBeTruthy(); + }); + + it("returns false if element doesn't match the selector", () => { + expect(this.element.matches('.not-an-element')).toBeFalsy(); + }); + }); + + describe('closest', () => { + beforeEach(() => { + this.childElement = document.createElement('li'); + this.element.appendChild(this.childElement); + }); + + it('returns the closest parent that matches the selector', () => { + expect(this.childElement.closest('ul').toString()).toBe(this.element.toString()); + }); + + it('returns itself if it matches the selector', () => { + expect(this.childElement.closest('li').toString()).toBe(this.childElement.toString()); + }); + + it('returns undefined if nothing matches the selector', () => { + expect(this.childElement.closest('.no-an-element')).toBeFalsy(); + }); + }); + }); +})(); -- cgit v1.2.1 From 119757ac9c2e968a4e97bd3a7f7d7a783456da83 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Fri, 2 Dec 2016 13:27:26 +0100 Subject: Add scaffold for remaining statuses-related classes --- lib/gitlab/ci/status/extended/base.rb | 11 ++++++++++ lib/gitlab/ci/status/extended/pipeline/common.rb | 24 ++++++++++++++++++++++ .../extended/pipeline/success_with_warnings.rb | 23 +++++++++++++++++++++ lib/gitlab/ci/status/factory.rb | 6 ++++++ spec/lib/gitlab/ci/status/extended/base_spec.rb | 0 .../ci/status/extended/pipeline/common_spec.rb | 0 .../pipeline/success_with_warnings_spec.rb | 0 spec/lib/gitlab/ci/status/factory_spec.rb | 0 8 files changed, 64 insertions(+) create mode 100644 lib/gitlab/ci/status/extended/base.rb create mode 100644 lib/gitlab/ci/status/extended/pipeline/common.rb create mode 100644 lib/gitlab/ci/status/extended/pipeline/success_with_warnings.rb create mode 100644 lib/gitlab/ci/status/factory.rb create mode 100644 spec/lib/gitlab/ci/status/extended/base_spec.rb create mode 100644 spec/lib/gitlab/ci/status/extended/pipeline/common_spec.rb create mode 100644 spec/lib/gitlab/ci/status/extended/pipeline/success_with_warnings_spec.rb create mode 100644 spec/lib/gitlab/ci/status/factory_spec.rb diff --git a/lib/gitlab/ci/status/extended/base.rb b/lib/gitlab/ci/status/extended/base.rb new file mode 100644 index 00000000000..1d7819c6891 --- /dev/null +++ b/lib/gitlab/ci/status/extended/base.rb @@ -0,0 +1,11 @@ +module Gitlab::Ci + module Status + module Extended + module Base + def matches?(_subject) + raise NotImplementedError + end + end + end + end +end diff --git a/lib/gitlab/ci/status/extended/pipeline/common.rb b/lib/gitlab/ci/status/extended/pipeline/common.rb new file mode 100644 index 00000000000..75d392fab6c --- /dev/null +++ b/lib/gitlab/ci/status/extended/pipeline/common.rb @@ -0,0 +1,24 @@ +module Gitlab::Ci + module Status + module Extended + module Pipeline + module Common + def initialize(pipeline) + @pipeline = pipeline + end + + def has_details? + true + end + + def details_path + end + + def has_action? + false + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/extended/pipeline/success_with_warnings.rb b/lib/gitlab/ci/status/extended/pipeline/success_with_warnings.rb new file mode 100644 index 00000000000..5e92bb97eec --- /dev/null +++ b/lib/gitlab/ci/status/extended/pipeline/success_with_warnings.rb @@ -0,0 +1,23 @@ +module Gitlab::Ci + module Status + module Extended + module Pipeline + class SuccessWithWarnings < SimpleDelegator + extend Status::Extended::Base + + def label + 'passed with warnings' + end + + def icon + 'icon_status_warning' + end + + def self.matches?(pipeline) + pipeline.success? && pipeline.has_warnings? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb new file mode 100644 index 00000000000..212cd1a1687 --- /dev/null +++ b/lib/gitlab/ci/status/factory.rb @@ -0,0 +1,6 @@ +module Gitlab::Ci + module Status + class Factory + end + end +end diff --git a/spec/lib/gitlab/ci/status/extended/base_spec.rb b/spec/lib/gitlab/ci/status/extended/base_spec.rb new file mode 100644 index 00000000000..e69de29bb2d diff --git a/spec/lib/gitlab/ci/status/extended/pipeline/common_spec.rb b/spec/lib/gitlab/ci/status/extended/pipeline/common_spec.rb new file mode 100644 index 00000000000..e69de29bb2d diff --git a/spec/lib/gitlab/ci/status/extended/pipeline/success_with_warnings_spec.rb b/spec/lib/gitlab/ci/status/extended/pipeline/success_with_warnings_spec.rb new file mode 100644 index 00000000000..e69de29bb2d diff --git a/spec/lib/gitlab/ci/status/factory_spec.rb b/spec/lib/gitlab/ci/status/factory_spec.rb new file mode 100644 index 00000000000..e69de29bb2d -- cgit v1.2.1 From a527fab1f5d565c804bd5de5300dfcbb88376f8d Mon Sep 17 00:00:00 2001 From: Andrew Smith <espadav8@gmail.com> Date: Thu, 1 Dec 2016 23:11:29 +1000 Subject: Test all values for `enabled_git_access_protocol` --- spec/models/user_spec.rb | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 475f4419d58..2244803f90c 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -576,15 +576,20 @@ describe User, models: true do end end - context 'when current_application_settings.enabled_git_access_protocol does not contain SSH' do - before do - stub_application_setting(enabled_git_access_protocol: 'HTTP') - end - - it "doesn't require user to have SSH key" do - user = build(:user) - - expect(user.require_ssh_key?).to be_falsey + describe '#require_ssh_key?' do + protocol_and_expectation = { + 'http' => false, + 'ssh' => true, + '' => true, + } + + protocol_and_expectation.each do |protocol, expected| + it "has correct require_ssh_key?" do + stub_application_setting(enabled_git_access_protocol: protocol) + user = build(:user) + + expect(user.require_ssh_key?).to eq(expected) + end end end end -- cgit v1.2.1 From 0c7168b98d69dc4071873a2787e4974a4c24ea20 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Fri, 2 Dec 2016 13:56:33 +0100 Subject: Add information about badge test to core statuses --- lib/gitlab/ci/status/core/canceled.rb | 4 ++++ lib/gitlab/ci/status/core/created.rb | 4 ++++ lib/gitlab/ci/status/core/failed.rb | 4 ++++ lib/gitlab/ci/status/core/pending.rb | 4 ++++ lib/gitlab/ci/status/core/running.rb | 4 ++++ lib/gitlab/ci/status/core/skipped.rb | 4 ++++ lib/gitlab/ci/status/core/success.rb | 4 ++++ lib/gitlab/ci/status/extended/pipeline/success_with_warnings.rb | 4 ++++ spec/lib/gitlab/ci/status/core/canceled_spec.rb | 4 ++++ spec/lib/gitlab/ci/status/core/created_spec.rb | 4 ++++ spec/lib/gitlab/ci/status/core/failed_spec.rb | 4 ++++ spec/lib/gitlab/ci/status/core/pending_spec.rb | 4 ++++ spec/lib/gitlab/ci/status/core/running_spec.rb | 4 ++++ spec/lib/gitlab/ci/status/core/skipped_spec.rb | 4 ++++ spec/lib/gitlab/ci/status/core/success_spec.rb | 4 ++++ 15 files changed, 60 insertions(+) diff --git a/lib/gitlab/ci/status/core/canceled.rb b/lib/gitlab/ci/status/core/canceled.rb index 5e06b946a99..a05ac8ee3cc 100644 --- a/lib/gitlab/ci/status/core/canceled.rb +++ b/lib/gitlab/ci/status/core/canceled.rb @@ -2,6 +2,10 @@ module Gitlab::Ci module Status module Core class Canceled < Core::Base + def text + 'canceled' + end + def label 'canceled' end diff --git a/lib/gitlab/ci/status/core/created.rb b/lib/gitlab/ci/status/core/created.rb index c116f9a97f1..ee8bf2e8dac 100644 --- a/lib/gitlab/ci/status/core/created.rb +++ b/lib/gitlab/ci/status/core/created.rb @@ -2,6 +2,10 @@ module Gitlab::Ci module Status module Core class Created < Core::Base + def text + 'created' + end + def label 'created' end diff --git a/lib/gitlab/ci/status/core/failed.rb b/lib/gitlab/ci/status/core/failed.rb index 467ef71e819..ea1615853c0 100644 --- a/lib/gitlab/ci/status/core/failed.rb +++ b/lib/gitlab/ci/status/core/failed.rb @@ -2,6 +2,10 @@ module Gitlab::Ci module Status module Core class Failed < Core::Base + def text + 'failed' + end + def label 'failed' end diff --git a/lib/gitlab/ci/status/core/pending.rb b/lib/gitlab/ci/status/core/pending.rb index 05c9e41091b..95fbb710735 100644 --- a/lib/gitlab/ci/status/core/pending.rb +++ b/lib/gitlab/ci/status/core/pending.rb @@ -2,6 +2,10 @@ module Gitlab::Ci module Status module Core class Pending < Core::Base + def text + 'pending' + end + def label 'pending' end diff --git a/lib/gitlab/ci/status/core/running.rb b/lib/gitlab/ci/status/core/running.rb index 01f0c57ef5f..5580c1a5154 100644 --- a/lib/gitlab/ci/status/core/running.rb +++ b/lib/gitlab/ci/status/core/running.rb @@ -2,6 +2,10 @@ module Gitlab::Ci module Status module Core class Running < Core::Base + def text + 'running' + end + def label 'running' end diff --git a/lib/gitlab/ci/status/core/skipped.rb b/lib/gitlab/ci/status/core/skipped.rb index e791341b7e0..0e8e42f525b 100644 --- a/lib/gitlab/ci/status/core/skipped.rb +++ b/lib/gitlab/ci/status/core/skipped.rb @@ -2,6 +2,10 @@ module Gitlab::Ci module Status module Core class Skipped < Core::Base + def text + 'skipped' + end + def label 'skipped' end diff --git a/lib/gitlab/ci/status/core/success.rb b/lib/gitlab/ci/status/core/success.rb index bcfe7e63a6c..7efafdb615f 100644 --- a/lib/gitlab/ci/status/core/success.rb +++ b/lib/gitlab/ci/status/core/success.rb @@ -2,6 +2,10 @@ module Gitlab::Ci module Status module Core class Success < Core::Base + def text + 'passed' + end + def label 'passed' end diff --git a/lib/gitlab/ci/status/extended/pipeline/success_with_warnings.rb b/lib/gitlab/ci/status/extended/pipeline/success_with_warnings.rb index 5e92bb97eec..8f1d9cf87c7 100644 --- a/lib/gitlab/ci/status/extended/pipeline/success_with_warnings.rb +++ b/lib/gitlab/ci/status/extended/pipeline/success_with_warnings.rb @@ -5,6 +5,10 @@ module Gitlab::Ci class SuccessWithWarnings < SimpleDelegator extend Status::Extended::Base + def text + 'passed' + end + def label 'passed with warnings' end diff --git a/spec/lib/gitlab/ci/status/core/canceled_spec.rb b/spec/lib/gitlab/ci/status/core/canceled_spec.rb index d03b7a18aa0..fd90eb6beda 100644 --- a/spec/lib/gitlab/ci/status/core/canceled_spec.rb +++ b/spec/lib/gitlab/ci/status/core/canceled_spec.rb @@ -3,6 +3,10 @@ require 'spec_helper' describe Gitlab::Ci::Status::Core::Canceled do subject { described_class.new(double('subject')) } + describe '#text' do + it { expect(subject.label).to eq 'canceled' } + end + describe '#label' do it { expect(subject.label).to eq 'canceled' } end diff --git a/spec/lib/gitlab/ci/status/core/created_spec.rb b/spec/lib/gitlab/ci/status/core/created_spec.rb index 5a6d7523e83..a35a3e14929 100644 --- a/spec/lib/gitlab/ci/status/core/created_spec.rb +++ b/spec/lib/gitlab/ci/status/core/created_spec.rb @@ -3,6 +3,10 @@ require 'spec_helper' describe Gitlab::Ci::Status::Core::Created do subject { described_class.new(double('subject')) } + describe '#text' do + it { expect(subject.label).to eq 'created' } + end + describe '#label' do it { expect(subject.label).to eq 'created' } end diff --git a/spec/lib/gitlab/ci/status/core/failed_spec.rb b/spec/lib/gitlab/ci/status/core/failed_spec.rb index 0b7e03f1684..41ce63b3a6f 100644 --- a/spec/lib/gitlab/ci/status/core/failed_spec.rb +++ b/spec/lib/gitlab/ci/status/core/failed_spec.rb @@ -3,6 +3,10 @@ require 'spec_helper' describe Gitlab::Ci::Status::Core::Failed do subject { described_class.new(double('subject')) } + describe '#text' do + it { expect(subject.label).to eq 'failed' } + end + describe '#label' do it { expect(subject.label).to eq 'failed' } end diff --git a/spec/lib/gitlab/ci/status/core/pending_spec.rb b/spec/lib/gitlab/ci/status/core/pending_spec.rb index 95f2255e66d..988d3c0a9e2 100644 --- a/spec/lib/gitlab/ci/status/core/pending_spec.rb +++ b/spec/lib/gitlab/ci/status/core/pending_spec.rb @@ -3,6 +3,10 @@ require 'spec_helper' describe Gitlab::Ci::Status::Core::Pending do subject { described_class.new(double('subject')) } + describe '#text' do + it { expect(subject.label).to eq 'pending' } + end + describe '#label' do it { expect(subject.label).to eq 'pending' } end diff --git a/spec/lib/gitlab/ci/status/core/running_spec.rb b/spec/lib/gitlab/ci/status/core/running_spec.rb index 648de678618..dbb0d37659c 100644 --- a/spec/lib/gitlab/ci/status/core/running_spec.rb +++ b/spec/lib/gitlab/ci/status/core/running_spec.rb @@ -3,6 +3,10 @@ require 'spec_helper' describe Gitlab::Ci::Status::Core::Running do subject { described_class.new(double('subject')) } + describe '#text' do + it { expect(subject.label).to eq 'running' } + end + describe '#label' do it { expect(subject.label).to eq 'running' } end diff --git a/spec/lib/gitlab/ci/status/core/skipped_spec.rb b/spec/lib/gitlab/ci/status/core/skipped_spec.rb index dc2b56a1e34..624348af2d1 100644 --- a/spec/lib/gitlab/ci/status/core/skipped_spec.rb +++ b/spec/lib/gitlab/ci/status/core/skipped_spec.rb @@ -3,6 +3,10 @@ require 'spec_helper' describe Gitlab::Ci::Status::Core::Skipped do subject { described_class.new(double('subject')) } + describe '#text' do + it { expect(subject.label).to eq 'skipped' } + end + describe '#label' do it { expect(subject.label).to eq 'skipped' } end diff --git a/spec/lib/gitlab/ci/status/core/success_spec.rb b/spec/lib/gitlab/ci/status/core/success_spec.rb index fc67e58adb5..c4bc0d5e234 100644 --- a/spec/lib/gitlab/ci/status/core/success_spec.rb +++ b/spec/lib/gitlab/ci/status/core/success_spec.rb @@ -3,6 +3,10 @@ require 'spec_helper' describe Gitlab::Ci::Status::Core::Success do subject { described_class.new(double('subject')) } + describe '#text' do + it { expect(subject.label).to eq 'passed' } + end + describe '#label' do it { expect(subject.label).to eq 'passed' } end -- cgit v1.2.1 From c7c249407e98bf5fc099cd89901e67b000fdf69d Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Fri, 2 Dec 2016 14:21:04 +0100 Subject: Add implementation of common pipeline extended status --- lib/gitlab/ci/status/core/base.rb | 2 ++ lib/gitlab/ci/status/extended/pipeline/common.rb | 7 +++---- .../ci/status/extended/pipeline/common_spec.rb | 23 ++++++++++++++++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/lib/gitlab/ci/status/core/base.rb b/lib/gitlab/ci/status/core/base.rb index f7687c49ffd..d1896a610b7 100644 --- a/lib/gitlab/ci/status/core/base.rb +++ b/lib/gitlab/ci/status/core/base.rb @@ -4,6 +4,8 @@ module Gitlab::Ci # Base abstract class fore core status # class Base + include Gitlab::Routing.url_helpers + def initialize(subject) @subject = subject end diff --git a/lib/gitlab/ci/status/extended/pipeline/common.rb b/lib/gitlab/ci/status/extended/pipeline/common.rb index 75d392fab6c..1b70ba303dc 100644 --- a/lib/gitlab/ci/status/extended/pipeline/common.rb +++ b/lib/gitlab/ci/status/extended/pipeline/common.rb @@ -3,15 +3,14 @@ module Gitlab::Ci module Extended module Pipeline module Common - def initialize(pipeline) - @pipeline = pipeline - end - def has_details? true end def details_path + namespace_project_pipeline_path(@subject.project.namespace, + @subject.project, + @subject) end def has_action? diff --git a/spec/lib/gitlab/ci/status/extended/pipeline/common_spec.rb b/spec/lib/gitlab/ci/status/extended/pipeline/common_spec.rb index e69de29bb2d..32939800c70 100644 --- a/spec/lib/gitlab/ci/status/extended/pipeline/common_spec.rb +++ b/spec/lib/gitlab/ci/status/extended/pipeline/common_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Extended::Pipeline::Common do + let(:pipeline) { create(:ci_pipeline) } + + subject do + Gitlab::Ci::Status::Core::Success + .new(pipeline).extend(described_class) + end + + it 'does not have action' do + expect(subject).not_to have_action + end + + it 'has details' do + expect(subject).to have_details + end + + it 'links to the pipeline details page' do + expect(subject.details_path) + .to include "pipelines/#{pipeline.id}" + end +end -- cgit v1.2.1 From d74801ac6fca7b20e01555ac6339d08b16bba8fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Fri, 2 Dec 2016 14:34:17 +0100 Subject: Document the public Project API and document `GET /projects/:id/users` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- doc/api/projects.md | 48 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/doc/api/projects.md b/doc/api/projects.md index 132be644b59..0bc2a5210aa 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -347,7 +347,8 @@ Parameters: ### Get single project Get a specific project, identified by project ID or NAMESPACE/PROJECT_NAME, which is owned by the authenticated user. -If using namespaced projects call make sure that the NAMESPACE/PROJECT_NAME is URL-encoded, eg. `/api/v3/projects/diaspora%2Fdiaspora` (where `/` is represented by `%2F`). +If using namespaced projects call make sure that the NAMESPACE/PROJECT_NAME is URL-encoded, eg. `/api/v3/projects/diaspora%2Fdiaspora` (where `/` is represented by `%2F`). This endpoint can be accessed without authentication if +the project is publicly accessible. ``` GET /projects/:id @@ -436,10 +437,47 @@ Parameters: } ``` +## Get project users + +Get the users list of a project. + + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `search` | string | no | Search for specific users | + +``` +GET /projects/:id/users +``` + +```json +[ + { + "id": 1, + "username": "john_smith", + "name": "John Smith", + "state": "active", + "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg", + "web_url": "http://localhost:3000/john_smith" + }, + { + "id": 2, + "username": "jack_smith", + "name": "Jack Smith", + "state": "blocked", + "avatar_url": "http://gravatar.com/../e32131cd8.jpeg", + "web_url": "http://localhost:3000/jack_smith" + } +] +``` + ### Get project events -Get the events for the specified project. -Sorted from newest to oldest +Get the events for the specified project sorted from newest to oldest. This +endpoint can be accessed without authentication if the project is publicly +accessible. ``` GET /projects/:id/events @@ -1344,7 +1382,9 @@ Parameter: ## Search for projects by name -Search for projects by name which are accessible to the authenticated user. +Search for projects by name which are accessible to the authenticated user. This +endpoint can be accessed without authentication if the project is publicly +accessible. ``` GET /projects/search/:query -- cgit v1.2.1 From d55ff247569a2bf5c78c80f966a56b28d5c8332f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Fri, 2 Dec 2016 14:37:29 +0100 Subject: Implement extended pipeline - status with warnings --- spec/lib/gitlab/ci/status/extended/base_spec.rb | 12 ++++ .../pipeline/success_with_warnings_spec.rb | 65 ++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/spec/lib/gitlab/ci/status/extended/base_spec.rb b/spec/lib/gitlab/ci/status/extended/base_spec.rb index e69de29bb2d..7cdc68c927f 100644 --- a/spec/lib/gitlab/ci/status/extended/base_spec.rb +++ b/spec/lib/gitlab/ci/status/extended/base_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Extended::Base do + subject do + Class.new.extend(described_class) + end + + it 'requires subclass to implement matcher' do + expect { subject.matches?(double) } + .to raise_error(NotImplementedError) + end +end diff --git a/spec/lib/gitlab/ci/status/extended/pipeline/success_with_warnings_spec.rb b/spec/lib/gitlab/ci/status/extended/pipeline/success_with_warnings_spec.rb index e69de29bb2d..b1a63c5f2f9 100644 --- a/spec/lib/gitlab/ci/status/extended/pipeline/success_with_warnings_spec.rb +++ b/spec/lib/gitlab/ci/status/extended/pipeline/success_with_warnings_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Extended::Pipeline::SuccessWithWarnings do + subject do + described_class.new(double('status')) + end + + describe '#test' do + it { expect(subject.text).to eq 'passed' } + end + + describe '#label' do + it { expect(subject.label).to eq 'passed with warnings' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_warning' } + end + + describe '.matches?' do + context 'when pipeline is successful' do + let(:pipeline) do + create(:ci_pipeline, status: :success) + end + + context 'when pipeline has warnings' do + before do + allow(pipeline).to receive(:has_warnings?).and_return(true) + end + + it 'is a correct match' do + expect(described_class.matches?(pipeline)).to eq true + end + end + + context 'when pipeline does not have warnings' do + it 'does not match' do + expect(described_class.matches?(pipeline)).to eq false + end + end + end + + context 'when pipeline is not successful' do + let(:pipeline) do + create(:ci_pipeline, status: :skipped) + end + + context 'when pipeline has warnings' do + before do + allow(pipeline).to receive(:has_warnings?).and_return(true) + end + + it 'does not match' do + expect(described_class.matches?(pipeline)).to eq false + end + end + + context 'when pipeline does not have warnings' do + it 'does not match' do + expect(described_class.matches?(pipeline)).to eq false + end + end + end + end +end -- cgit v1.2.1 From 454e963196fdbded9ef8c530b0457a8c489e810d Mon Sep 17 00:00:00 2001 From: James Gregory <james.gregory@epigenesys.org.uk> Date: Mon, 28 Nov 2016 17:14:05 +0000 Subject: The admin user projects view now has a clickable group link --- app/views/admin/users/projects.html.haml | 2 +- .../unreleased/feature-admin-user-groups-link.yml | 4 ++++ spec/features/admin/admin_users_spec.rb | 28 ++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/feature-admin-user-groups-link.yml diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml index 84b9ceb23b3..dd6b7303493 100644 --- a/app/views/admin/users/projects.html.haml +++ b/app/views/admin/users/projects.html.haml @@ -7,7 +7,7 @@ %ul.well-list - @user.groups.each do |group| %li - %strong= group.name + %strong= link_to group.name, admin_group_path(group) – access to #{pluralize(group.projects.count, 'project')} diff --git a/changelogs/unreleased/feature-admin-user-groups-link.yml b/changelogs/unreleased/feature-admin-user-groups-link.yml new file mode 100644 index 00000000000..b89c08f82d7 --- /dev/null +++ b/changelogs/unreleased/feature-admin-user-groups-link.yml @@ -0,0 +1,4 @@ +--- +title: The admin user projects view now has a clickable group link +merge_request: 7620 +author: James Gregory diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index cb3191dfdde..e31325ce47b 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -225,4 +225,32 @@ describe "Admin::Users", feature: true do end end end + + describe "GET /admin/users/:id/projects" do + before do + @group = create(:group) + @project = create(:project, group: @group) + @simple_user = create(:user) + @group.add_developer(@simple_user) + + visit projects_admin_user_path(@simple_user) + end + + it "lists group projects" do + within(:css, '.append-bottom-default + .panel') do + expect(page).to have_content 'Group projects' + expect(page).to have_link @group.name, admin_group_path(@group) + end + end + + it 'allows navigation to the group details' do + within(:css, '.append-bottom-default + .panel') do + click_link @group.name + end + within(:css, 'h3.page-title') do + expect(page).to have_content "Group: #{@group.name}" + end + expect(page).to have_content @project.name + end + end end -- cgit v1.2.1 From 76abe0eded9a7b52c5d22ef1cbbecbb9ee700ed4 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 2 Dec 2016 15:26:06 +0000 Subject: Change ref property to commitRef --- .../environments/components/environment_item.js.es6 | 2 +- app/assets/javascripts/vue_common_component/commit.js.es6 | 14 +++++++------- spec/javascripts/vue_common_components/commit_spec.js.es6 | 10 +++++----- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 7ead8a18c2a..77ed13fc5f3 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -451,7 +451,7 @@ <div v-if="!isFolder && hasLastDeploymentKey" class="js-commit-component"> <commit-component :tag="commitTag" - :ref="commitRef" + :commitRef="commitRef" :commit_url="commitUrl" :short_sha="commitShortSha" :title="commitTitle" diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_common_component/commit.js.es6 index 1bc68c1ba2f..3ecb1efd2fc 100644 --- a/app/assets/javascripts/vue_common_component/commit.js.es6 +++ b/app/assets/javascripts/vue_common_component/commit.js.es6 @@ -23,7 +23,7 @@ * name * ref_url */ - ref: { + commitRef: { type: Object, required: false, default: () => ({}), @@ -79,8 +79,8 @@ * * @returns {Boolean} */ - hasRef() { - return this.ref && this.ref.name && this.ref.ref_url; + hasCommitRef() { + return this.commitRef && this.commitRef.name && this.commitRef.ref_url; }, /** @@ -131,15 +131,15 @@ template: ` <div class="branch-commit"> - <div v-if="hasRef" class="icon-container"> + <div v-if="hasCommitRef" class="icon-container"> <i v-if="tag" class="fa fa-tag"></i> <i v-if="!tag" class="fa fa-code-fork"></i> </div> - <a v-if="hasRef" + <a v-if="hasCommitRef" class="monospace branch-name" - :href="ref.ref_url"> - {{ref.name}} + :href="commitRef.ref_url"> + {{commitRef.name}} </a> <div class="icon-container commit-icon commit-icon-container"></div> diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_common_components/commit_spec.js.es6 index b1dbc8bd5fa..8b32b2c5cf7 100644 --- a/spec/javascripts/vue_common_components/commit_spec.js.es6 +++ b/spec/javascripts/vue_common_components/commit_spec.js.es6 @@ -10,7 +10,7 @@ describe('Commit component', () => { el: document.querySelector('.test-commit-container'), propsData: { tag: false, - ref: { + commitRef: { name: 'master', ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', }, @@ -34,7 +34,7 @@ describe('Commit component', () => { props = { tag: true, - ref: { + commitRef: { name: 'master', ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', }, @@ -59,11 +59,11 @@ describe('Commit component', () => { }); it('should render a link to the ref url', () => { - expect(component.$el.querySelector('.branch-name').getAttribute('href')).toEqual(props.ref.ref_url); + expect(component.$el.querySelector('.branch-name').getAttribute('href')).toEqual(props.commitRef.ref_url); }); it('should render the ref name', () => { - expect(component.$el.querySelector('.branch-name').textContent).toContain(props.ref.name); + expect(component.$el.querySelector('.branch-name').textContent).toContain(props.commitRef.name); }); it('should render the commit short sha with a link to the commit url', () => { @@ -103,7 +103,7 @@ describe('Commit component', () => { fixture.set('<div class="test-commit-container"></div>'); props = { tag: false, - ref: { + commitRef: { name: 'master', ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', }, -- cgit v1.2.1 From 21d88cda23e415b5fde98a25956a0e1461f53409 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 2 Dec 2016 15:30:26 +0000 Subject: Adds changelog entry --- .../javascripts/environments/components/environment_item.js.es6 | 2 +- changelogs/unreleased/25264-ref-commit.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/25264-ref-commit.yml diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 77ed13fc5f3..6ed14261fc3 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -451,7 +451,7 @@ <div v-if="!isFolder && hasLastDeploymentKey" class="js-commit-component"> <commit-component :tag="commitTag" - :commitRef="commitRef" + :commit_ref="commitRef" :commit_url="commitUrl" :short_sha="commitShortSha" :title="commitTitle" diff --git a/changelogs/unreleased/25264-ref-commit.yml b/changelogs/unreleased/25264-ref-commit.yml new file mode 100644 index 00000000000..13a33da9801 --- /dev/null +++ b/changelogs/unreleased/25264-ref-commit.yml @@ -0,0 +1,4 @@ +--- +title: Change ref property to commitRef in vue commit component +merge_request: 7901 +author: -- cgit v1.2.1 From f83927ebdeffb6529d864de51279509f18b99bc5 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 2 Dec 2016 15:41:58 +0000 Subject: Change prop name to keep consistency with other props. --- app/assets/javascripts/vue_common_component/commit.js.es6 | 8 ++++---- spec/javascripts/vue_common_components/commit_spec.js.es6 | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_common_component/commit.js.es6 index 3ecb1efd2fc..2ef2959cbf4 100644 --- a/app/assets/javascripts/vue_common_component/commit.js.es6 +++ b/app/assets/javascripts/vue_common_component/commit.js.es6 @@ -23,7 +23,7 @@ * name * ref_url */ - commitRef: { + commit_ref: { type: Object, required: false, default: () => ({}), @@ -80,7 +80,7 @@ * @returns {Boolean} */ hasCommitRef() { - return this.commitRef && this.commitRef.name && this.commitRef.ref_url; + return this.commit_ref && this.commit_ref.name && this.commit_ref.ref_url; }, /** @@ -138,8 +138,8 @@ <a v-if="hasCommitRef" class="monospace branch-name" - :href="commitRef.ref_url"> - {{commitRef.name}} + :href="commit_ref.ref_url"> + {{commit_ref.name}} </a> <div class="icon-container commit-icon commit-icon-container"></div> diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_common_components/commit_spec.js.es6 index 8b32b2c5cf7..d170517dd9b 100644 --- a/spec/javascripts/vue_common_components/commit_spec.js.es6 +++ b/spec/javascripts/vue_common_components/commit_spec.js.es6 @@ -10,7 +10,7 @@ describe('Commit component', () => { el: document.querySelector('.test-commit-container'), propsData: { tag: false, - commitRef: { + commit_ref: { name: 'master', ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', }, @@ -34,7 +34,7 @@ describe('Commit component', () => { props = { tag: true, - commitRef: { + commit_ref: { name: 'master', ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', }, @@ -59,11 +59,11 @@ describe('Commit component', () => { }); it('should render a link to the ref url', () => { - expect(component.$el.querySelector('.branch-name').getAttribute('href')).toEqual(props.commitRef.ref_url); + expect(component.$el.querySelector('.branch-name').getAttribute('href')).toEqual(props.commit_ref.ref_url); }); it('should render the ref name', () => { - expect(component.$el.querySelector('.branch-name').textContent).toContain(props.commitRef.name); + expect(component.$el.querySelector('.branch-name').textContent).toContain(props.commit_ref.name); }); it('should render the commit short sha with a link to the commit url', () => { @@ -103,7 +103,7 @@ describe('Commit component', () => { fixture.set('<div class="test-commit-container"></div>'); props = { tag: false, - commitRef: { + commit_ref: { name: 'master', ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', }, -- cgit v1.2.1 From 89c22ed5af490f250800030ee4342c185dbc5358 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 2 Dec 2016 14:34:08 +0000 Subject: Shows group members in the project members list Closes #24122 --- .../projects/project_members_controller.rb | 20 +++++++++++++++++++- app/views/shared/members/_member.html.haml | 6 +++++- .../group-members-in-project-members-view.yml | 4 ++++ .../features/projects/members/group_members_spec.rb | 21 +++++++++++++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/group-members-in-project-members-view.yml create mode 100644 spec/features/projects/members/group_members_spec.rb diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 699a56ae2f8..ccf5ff35171 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -10,14 +10,32 @@ class Projects::ProjectMembersController < Projects::ApplicationController @project_members = @project.project_members @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project) + @group = @project.group + + if @group + @group_members = @group.group_members + @group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group) + end + if params[:search].present? users = @project.users.search(params[:search]).to_a @project_members = @project_members.where(user_id: users) + if @group_members + users = @group.users.search(params[:search]).to_a + @group_members = @group_members.where(user_id: users) + end + @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) end - @project_members = @project_members.order(access_level: :desc).page(params[:page]) + members_id = @project_members.pluck(:id) + + if @group_members + members_id << @group_members.select{ |member| !@project_members.find_by(user_id: member.user_id) }.select(&:id) + end + + @project_members = Member.where(id: members_id.flatten).order(access_level: :desc).page(params[:page]) @requesters = AccessRequestsFinder.new(@project).execute(current_user) diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 432047a1c4e..bf42c9080a6 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -12,6 +12,10 @@ = link_to user.name, user_path(user) %span.cgray= user.to_reference + - if member.real_source_type == 'Group' + · + %span.cblue=member.group.name + - if user == current_user %span.label.label-success.prepend-left-5 It's you @@ -45,7 +49,7 @@ = time_ago_with_tooltip(member.created_at) - if show_roles .controls.member-controls - - if show_controls + - if show_controls && member.real_source_type == 'Project' - if user != current_user = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{member.id}", disabled: !can_admin_member diff --git a/changelogs/unreleased/group-members-in-project-members-view.yml b/changelogs/unreleased/group-members-in-project-members-view.yml new file mode 100644 index 00000000000..415e2b6b1e2 --- /dev/null +++ b/changelogs/unreleased/group-members-in-project-members-view.yml @@ -0,0 +1,4 @@ +--- +title: Shows group members in project members list +merge_request: +author: diff --git a/spec/features/projects/members/group_members_spec.rb b/spec/features/projects/members/group_members_spec.rb new file mode 100644 index 00000000000..39235a1cd4f --- /dev/null +++ b/spec/features/projects/members/group_members_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +feature 'Projects members', feature: true do + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } + + background do + group.add_owner(user) + login_as(user) + visit namespace_project_project_members_path(project.namespace, project) + end + + it 'shows group members in list' do + expect(page).to have_selector('.group_member') + + page.within first('.content-list .member') do + expect(page).to have_content(group.name) + end + end +end -- cgit v1.2.1 From cc66ec2b73d6fa581f5300957597615ed1b58c55 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 2 Dec 2016 16:48:34 +0000 Subject: Fixed Ruby to be better for performance Fixed controls not showing in groups which fixes tests --- .../projects/project_members_controller.rb | 22 +++++++++++----------- app/views/shared/members/_member.html.haml | 10 +++------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index ccf5ff35171..10bc75b4c5e 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -10,20 +10,20 @@ class Projects::ProjectMembersController < Projects::ApplicationController @project_members = @project.project_members @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project) - @group = @project.group + group = @project.group - if @group - @group_members = @group.group_members - @group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group) + if group + group_members = group.group_members.where.not(user_id: @project_members.select(:user_id)) + group_members = group_members.non_invite unless can?(current_user, :admin_group, @group) end if params[:search].present? - users = @project.users.search(params[:search]).to_a - @project_members = @project_members.where(user_id: users) + user_ids = @project.users.search(params[:search]).select(:id) + @project_members = @project_members.where(user_id: user_ids) - if @group_members - users = @group.users.search(params[:search]).to_a - @group_members = @group_members.where(user_id: users) + if group_members + user_ids = group.users.search(params[:search]).select(:id) + group_members = group_members.where(user_id: user_ids) end @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) @@ -31,8 +31,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController members_id = @project_members.pluck(:id) - if @group_members - members_id << @group_members.select{ |member| !@project_members.find_by(user_id: member.user_id) }.select(&:id) + if group_members + members_id << group_members.pluck(:id) end @project_members = Member.where(id: members_id.flatten).order(access_level: :desc).page(params[:page]) diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index bf42c9080a6..aa5b39151e6 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -12,10 +12,6 @@ = link_to user.name, user_path(user) %span.cgray= user.to_reference - - if member.real_source_type == 'Group' - · - %span.cblue=member.group.name - - if user == current_user %span.label.label-success.prepend-left-5 It's you @@ -24,8 +20,8 @@ %strong Blocked - if source.instance_of?(Group) && !@group - = link_to source, class: "member-group-link prepend-left-5" do - = "· #{source.name}" + · + = link_to source.name, source, class: "member-group-link" .hidden-xs.cgray - if member.request? @@ -49,7 +45,7 @@ = time_ago_with_tooltip(member.created_at) - if show_roles .controls.member-controls - - if show_controls && member.real_source_type == 'Project' + - if show_controls && (member.respond_to?(:group) && @members) || (member.respond_to?(:project) && @project_members) - if user != current_user = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{member.id}", disabled: !can_admin_member -- cgit v1.2.1 From 76d8c2e656da5a7411c2150bf60ad214e0bb0ce1 Mon Sep 17 00:00:00 2001 From: mattl <mattl@gitlab.com> Date: Fri, 2 Dec 2016 12:04:03 -0500 Subject: WIP: Adds a default commit message when adding a README (#25167) --- app/helpers/projects_helper.rb | 16 +--------------- app/views/projects/_readme.html.haml | 2 +- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 898ce6a3af7..9cda3b78761 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -390,21 +390,7 @@ module ProjectsHelper "success" end end - - def new_readme_path - ref = @repository.root_ref if @repository - ref ||= 'master' - - namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'README.md') - end - - def new_license_path - ref = @repository.root_ref if @repository - ref ||= 'master' - - namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'LICENSE') - end - + def readme_cache_key sha = @project.commit.try(:sha) || 'nil' [@project.path_with_namespace, sha, "readme"].join('-') diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml index 369a847e7d4..b6fb08b68e9 100644 --- a/app/views/projects/_readme.html.haml +++ b/app/views/projects/_readme.html.haml @@ -18,5 +18,5 @@ distributed with computer software, forming part of its documentation. %p We recommend you to - = link_to "add a README", new_readme_path, class: 'underlined-link' + = link_to "add a README", add_special_file_path(@project, file_name: 'README.md'), class: 'underlined-link' file to the repository and GitLab will render it here instead of this message. -- cgit v1.2.1 From 3ababa74c45af3a2fb2b34cd9dcd1a9efe319440 Mon Sep 17 00:00:00 2001 From: mattl <mattl@gitlab.com> Date: Fri, 2 Dec 2016 12:13:28 -0500 Subject: Added a changelog entry --- changelogs/unreleased/mr-origin-7855.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/mr-origin-7855.yml diff --git a/changelogs/unreleased/mr-origin-7855.yml b/changelogs/unreleased/mr-origin-7855.yml new file mode 100644 index 00000000000..0fdc6153d55 --- /dev/null +++ b/changelogs/unreleased/mr-origin-7855.yml @@ -0,0 +1,4 @@ +--- +title: Provides a sensible default message when adding a README to a project +merge_request: 7903 +author: -- cgit v1.2.1 From f272ee6eba37548cbd8919139d583a71ffdac8dc Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira <oswluizf@gmail.com> Date: Wed, 2 Nov 2016 21:49:13 -0200 Subject: Add shorthand support to gitlab markdown references --- app/helpers/gitlab_markdown_helper.rb | 2 +- app/helpers/labels_helper.rb | 9 +- app/models/commit.rb | 22 +- app/models/commit_range.rb | 17 +- app/models/concerns/referable.rb | 13 - app/models/issue.rb | 6 +- app/models/label.rb | 15 +- app/models/merge_request.rb | 6 +- app/models/milestone.rb | 13 +- app/models/project.rb | 33 ++- app/models/snippet.rb | 8 +- changelogs/unreleased/glm-shorthand-reference.yml | 4 + lib/banzai/filter/abstract_reference_filter.rb | 23 +- lib/banzai/filter/commit_range_reference_filter.rb | 2 +- lib/banzai/filter/commit_reference_filter.rb | 2 +- lib/banzai/filter/label_reference_filter.rb | 54 +--- lib/banzai/filter/milestone_reference_filter.rb | 20 +- spec/features/issues/move_spec.rb | 2 +- spec/features/merge_requests/create_new_mr_spec.rb | 2 +- spec/helpers/labels_helper_spec.rb | 4 +- .../lib/banzai/filter/abstract_link_filter_spec.rb | 4 +- .../filter/commit_range_reference_filter_spec.rb | 99 +++++-- .../banzai/filter/commit_reference_filter_spec.rb | 78 +++++- .../banzai/filter/issue_reference_filter_spec.rb | 108 +++++++- .../banzai/filter/label_reference_filter_spec.rb | 291 +++++++++++++++++---- .../filter/merge_request_reference_filter_spec.rb | 92 ++++++- .../filter/milestone_reference_filter_spec.rb | 108 +++++++- .../banzai/filter/snippet_reference_filter_spec.rb | 85 +++++- spec/lib/gitlab/gfm/reference_rewriter_spec.rb | 4 +- spec/models/commit_range_spec.rb | 8 +- spec/models/commit_spec.rb | 14 +- spec/models/group_label_spec.rb | 10 + spec/models/issue_spec.rb | 10 +- spec/models/merge_request_spec.rb | 9 +- spec/models/milestone_spec.rb | 14 + spec/models/project_label_spec.rb | 4 +- spec/models/project_spec.rb | 66 ++++- spec/models/snippet_spec.rb | 29 +- spec/services/issues/move_service_spec.rb | 4 +- spec/services/system_note_service_spec.rb | 2 +- 40 files changed, 1010 insertions(+), 286 deletions(-) create mode 100644 changelogs/unreleased/glm-shorthand-reference.yml diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 0772d848289..eb435cc1783 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -174,7 +174,7 @@ module GitlabMarkdownHelper # Returns a String def cross_project_reference(project, entity) if entity.respond_to?(:to_reference) - "#{project.to_reference}#{entity.to_reference}" + entity.to_reference(project) else '' end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 4f180456b16..e5b1e6e8bc7 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -82,12 +82,6 @@ module LabelsHelper span.html_safe end - def render_colored_cross_project_label(label, source_project = nil, tooltip: true) - label_suffix = source_project ? source_project.name_with_namespace : label.project.name_with_namespace - label_suffix = " <i>in #{escape_once(label_suffix)}</i>" - render_colored_label(label, label_suffix, tooltip: tooltip) - end - def suggested_colors [ '#0033CC', @@ -166,6 +160,5 @@ module LabelsHelper end # Required for Banzai::Filter::LabelReferenceFilter - module_function :render_colored_label, :render_colored_cross_project_label, - :text_color_for_bg, :escape_once + module_function :render_colored_label, :text_color_for_bg, :escape_once end diff --git a/app/models/commit.rb b/app/models/commit.rb index 176c524cf7b..248140f421b 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -92,19 +92,11 @@ class Commit end def to_reference(from_project = nil) - if cross_project_reference?(from_project) - project.to_reference + self.class.reference_prefix + self.id - else - self.id - end + commit_reference(from_project, id) end def reference_link_text(from_project = nil) - if cross_project_reference?(from_project) - project.to_reference + self.class.reference_prefix + self.short_id - else - self.short_id - end + commit_reference(from_project, short_id) end def diff_line_count @@ -329,6 +321,16 @@ class Commit private + def commit_reference(from_project, referable_commit_id) + reference = project.to_reference(from_project) + + if reference.present? + "#{reference}#{self.class.reference_prefix}#{referable_commit_id}" + else + referable_commit_id + end + end + def find_author_by_any_email User.find_by_any_email(author_email.downcase) end diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index ac2477fd973..d9af7f6c139 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -90,21 +90,24 @@ class CommitRange alias_method :id, :to_s def to_reference(from_project = nil) - if cross_project_reference?(from_project) - project.to_reference + self.class.reference_prefix + self.id + project_reference = project.to_reference(from_project) + + if project_reference.present? + project_reference + self.class.reference_prefix + self.id else self.id end end def reference_link_text(from_project = nil) - reference = ref_from + notation + ref_to + project_reference = project.to_reference(from_project) + reference = ref_from + notation + ref_to - if cross_project_reference?(from_project) - reference = project.to_reference + self.class.reference_prefix + reference + if project_reference.present? + project_reference + self.class.reference_prefix + reference + else + reference end - - reference end # Return a Hash of parameters for passing to a URL helper diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb index dee940a3f88..8ba009fe04f 100644 --- a/app/models/concerns/referable.rb +++ b/app/models/concerns/referable.rb @@ -72,17 +72,4 @@ module Referable }x end end - - private - - # Check if a reference is being done cross-project - # - # from_project - Refering Project object - def cross_project_reference?(from_project) - if self.is_a?(Project) - self != from_project - else - from_project && self.project && self.project != from_project - end - end end diff --git a/app/models/issue.rb b/app/models/issue.rb index fbf07040301..7fe92051037 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -153,11 +153,7 @@ class Issue < ActiveRecord::Base def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{iid}" - if cross_project_reference?(from_project) - reference = project.to_reference + reference - end - - reference + "#{project.to_reference(from_project)}#{reference}" end def referenced_merge_requests(current_user = nil) diff --git a/app/models/label.rb b/app/models/label.rb index d9287f2dc29..d38c37344c9 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -144,9 +144,10 @@ class Label < ActiveRecord::Base # # Examples: # - # Label.first.to_reference # => "~1" - # Label.first.to_reference(format: :name) # => "~\"bug\"" - # Label.first.to_reference(project1, project2) # => "gitlab-org/gitlab-ce~1" + # Label.first.to_reference # => "~1" + # Label.first.to_reference(format: :name) # => "~\"bug\"" + # Label.first.to_reference(project, same_namespace_project) # => "gitlab-ce~1" + # Label.first.to_reference(project, another_namespace_project) # => "gitlab-org/gitlab-ce~1" # # Returns a String # @@ -154,8 +155,8 @@ class Label < ActiveRecord::Base format_reference = label_format_reference(format) reference = "#{self.class.reference_prefix}#{format_reference}" - if cross_project_reference?(source_project, target_project) - source_project.to_reference + reference + if source_project + "#{source_project.to_reference(target_project)}#{reference}" else reference end @@ -169,10 +170,6 @@ class Label < ActiveRecord::Base private - def cross_project_reference?(source_project, target_project) - source_project && target_project && source_project != target_project - end - def issues_count(user, params = {}) params.merge!(subject_foreign_key => subject.id, label_name: title, scope: 'all') IssuesFinder.new(user, params.with_indifferent_access).execute.count diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index bfb016df46d..4de4a83a041 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -176,11 +176,7 @@ class MergeRequest < ActiveRecord::Base def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{iid}" - if cross_project_reference?(from_project) - reference = project.to_reference + reference - end - - reference + "#{project.to_reference(from_project)}#{reference}" end def first_commit diff --git a/app/models/milestone.rb b/app/models/milestone.rb index c774e69080c..45ca97adad1 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -113,19 +113,16 @@ class Milestone < ActiveRecord::Base # # Examples: # - # Milestone.first.to_reference # => "%1" - # Milestone.first.to_reference(format: :name) # => "%\"goal\"" - # Milestone.first.to_reference(project) # => "gitlab-org/gitlab-ce%1" + # Milestone.first.to_reference # => "%1" + # Milestone.first.to_reference(format: :name) # => "%\"goal\"" + # Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-ce%1" + # Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1" # def to_reference(from_project = nil, format: :iid) format_reference = milestone_format_reference(format) reference = "#{self.class.reference_prefix}#{format_reference}" - if cross_project_reference?(from_project) - project.to_reference + reference - else - reference - end + "#{project.to_reference(from_project)}#{reference}" end def reference_link_text(from_project = nil) diff --git a/app/models/project.rb b/app/models/project.rb index f01cb613b85..9d58aff4033 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -419,7 +419,11 @@ class Project < ActiveRecord::Base def reference_pattern name_pattern = Gitlab::Regex::NAMESPACE_REGEX_STR - %r{(?<project>#{name_pattern}/#{name_pattern})} + + %r{ + ((?<namespace>#{name_pattern})\/)? + (?<project>#{name_pattern}) + }x end def trending @@ -650,8 +654,20 @@ class Project < ActiveRecord::Base end end - def to_reference(_from_project = nil) - path_with_namespace + def to_reference(from_project = nil) + if cross_namespace_reference?(from_project) + path_with_namespace + elsif cross_project_reference?(from_project) + path + end + end + + def to_human_reference(from_project = nil) + if cross_namespace_reference?(from_project) + name_with_namespace + elsif cross_project_reference?(from_project) + name + end end def web_url @@ -1327,10 +1343,21 @@ class Project < ActiveRecord::Base private + # Check if a reference is being done cross-project + # + # from_project - Refering Project object + def cross_project_reference?(from_project) + from_project && self != from_project + end + def pushes_since_gc_redis_key "projects/#{id}/pushes_since_gc" end + def cross_namespace_reference?(from_project) + from_project && namespace != from_project.namespace + end + def default_branch_protected? current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL || current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 8ff4e7ae718..aa2e3a1ff18 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -67,11 +67,11 @@ class Snippet < ActiveRecord::Base def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{id}" - if cross_project_reference?(from_project) - reference = project.to_reference + reference + if project.present? + "#{project.to_reference(from_project)}#{reference}" + else + reference end - - reference end def self.content_types diff --git a/changelogs/unreleased/glm-shorthand-reference.yml b/changelogs/unreleased/glm-shorthand-reference.yml new file mode 100644 index 00000000000..6d60f23c798 --- /dev/null +++ b/changelogs/unreleased/glm-shorthand-reference.yml @@ -0,0 +1,4 @@ +--- +title: Add shorthand support to gitlab markdown references +merge_request: 7255 +author: Oswaldo Ferreira diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 3740d4fb4cd..d904a8bd4ae 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -33,7 +33,7 @@ module Banzai # Returns a String replaced with the return of the block. def self.references_in(text, pattern = object_class.reference_pattern) text.gsub(pattern) do |match| - yield match, $~[object_sym].to_i, $~[:project], $~ + yield match, $~[object_sym].to_i, $~[:project], $~[:namespace], $~ end end @@ -145,8 +145,9 @@ module Banzai # Returns a String with references replaced with links. All links # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling. def object_link_filter(text, pattern, link_content: nil) - references_in(text, pattern) do |match, id, project_ref, matches| - project = project_from_ref_cached(project_ref) + references_in(text, pattern) do |match, id, project_ref, namespace_ref, matches| + project_path = full_project_path(namespace_ref, project_ref) + project = project_from_ref_cached(project_path) if project && object = find_object_cached(project, id) title = object_link_title(object) @@ -217,10 +218,9 @@ module Banzai nodes.each do |node| node.to_html.scan(regex) do - project = $~[:project] || current_project_path + project_path = full_project_path($~[:namespace], $~[:project]) symbol = $~[object_sym] - - refs[project] << symbol if object_class.reference_valid?(symbol) + refs[project_path] << symbol if object_class.reference_valid?(symbol) end end @@ -272,8 +272,19 @@ module Banzai @current_project_path ||= project.path_with_namespace end + def current_project_namespace_path + @current_project_namespace_path ||= project.namespace.path + end + private + def full_project_path(namespace, project_ref) + return current_project_path unless project_ref + + namespace_ref = namespace || current_project_namespace_path + "#{namespace_ref}/#{project_ref}" + end + def project_refs_cache RequestStore[:banzai_project_refs] ||= {} end diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb index 4358bf45549..eaacb9591b1 100644 --- a/lib/banzai/filter/commit_range_reference_filter.rb +++ b/lib/banzai/filter/commit_range_reference_filter.rb @@ -12,7 +12,7 @@ module Banzai def self.references_in(text, pattern = CommitRange.reference_pattern) text.gsub(pattern) do |match| - yield match, $~[:commit_range], $~[:project], $~ + yield match, $~[:commit_range], $~[:project], $~[:namespace], $~ end end diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb index a26dd09c25a..69c06117eda 100644 --- a/lib/banzai/filter/commit_reference_filter.rb +++ b/lib/banzai/filter/commit_reference_filter.rb @@ -12,7 +12,7 @@ module Banzai def self.references_in(text, pattern = Commit.reference_pattern) text.gsub(pattern) do |match| - yield match, $~[:commit], $~[:project], $~ + yield match, $~[:commit], $~[:project], $~[:namespace], $~ end end diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index 9f9a96cdc65..a605dea149e 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -14,16 +14,18 @@ module Banzai def self.references_in(text, pattern = Label.reference_pattern) unescape_html_entities(text).gsub(pattern) do |match| - yield match, $~[:label_id].to_i, $~[:label_name], $~[:project], $~ + yield match, $~[:label_id].to_i, $~[:label_name], $~[:project], $~[:namespace], $~ end end def references_in(text, pattern = Label.reference_pattern) unescape_html_entities(text).gsub(pattern) do |match| - label = find_label($~[:project], $~[:label_id], $~[:label_name]) + namespace, project = $~[:namespace], $~[:project] + project_path = full_project_path(namespace, project) + label = find_label(project_path, $~[:label_id], $~[:label_name]) if label - yield match, label.id, $~[:project], $~ + yield match, label.id, project, namespace, $~ else match end @@ -64,48 +66,12 @@ module Banzai end def object_link_text(object, matches) - if same_group?(object) && namespace_match?(matches) - render_same_project_label(object) - elsif same_project?(object) - render_same_project_label(object) - else - render_cross_project_label(object, matches) - end - end - - def same_group?(object) - object.is_a?(GroupLabel) && object.group == project.group - end - - def namespace_match?(matches) - matches[:project].blank? || matches[:project] == project.path_with_namespace - end - - def same_project?(object) - object.is_a?(ProjectLabel) && object.project == project - end - - def user - context[:current_user] || context[:author] - end - - def project - context[:project] - end - - def render_same_project_label(object) - LabelsHelper.render_colored_label(object) - end - - def render_cross_project_label(object, matches) - source_project = - if matches[:project] - Project.find_with_namespace(matches[:project]) - else - object.project - end + project_path = full_project_path(matches[:namespace], matches[:project]) + project_from_ref = project_from_ref_cached(project_path) + reference = project_from_ref.to_human_reference(project) + label_suffix = " <i>in #{reference}</i>" if reference.present? - LabelsHelper.render_colored_cross_project_label(object, source_project) + LabelsHelper.render_colored_label(object, label_suffix) end def unescape_html_entities(text) diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index 58fff496d00..f12014e191f 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -19,18 +19,20 @@ module Banzai return super(text, pattern) if pattern != Milestone.reference_pattern text.gsub(pattern) do |match| - milestone = find_milestone($~[:project], $~[:milestone_iid], $~[:milestone_name]) + milestone = find_milestone($~[:project], $~[:namespace], $~[:milestone_iid], $~[:milestone_name]) if milestone - yield match, milestone.iid, $~[:project], $~ + yield match, milestone.iid, $~[:project], $~[:namespace], $~ else match end end end - def find_milestone(project_ref, milestone_id, milestone_name) - project = project_from_ref(project_ref) + def find_milestone(project_ref, namespace_ref, milestone_id, milestone_name) + project_path = full_project_path(namespace_ref, project_ref) + project = project_from_ref(project_path) + return unless project milestone_params = milestone_params(milestone_id, milestone_name) @@ -52,11 +54,13 @@ module Banzai end def object_link_text(object, matches) - if context[:project] == object.project - super + milestone_link = escape_once(super) + reference = object.project.to_reference(project) + + if reference.present? + "#{milestone_link} <i>in #{reference}</i>".html_safe else - "#{escape_once(super)} <i>in #{escape_once(object.project.path_with_namespace)}</i>". - html_safe + milestone_link end end diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb index c9bec05a9da..f89b4db9e62 100644 --- a/spec/features/issues/move_spec.rb +++ b/spec/features/issues/move_spec.rb @@ -28,7 +28,7 @@ feature 'issue move to another project' do let(:new_project) { create(:project) } let(:new_project_search) { create(:project) } let(:text) { "Text with #{mr.to_reference}" } - let(:cross_reference) { old_project.to_reference } + let(:cross_reference) { old_project.to_reference(new_project) } background do old_project.team << [user, :reporter] diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb index 702869b6e8b..f1b68a39343 100644 --- a/spec/features/merge_requests/create_new_mr_spec.rb +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -40,7 +40,7 @@ feature 'Create New Merge Request', feature: true, js: true do visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_project_id: private_project.id }) - expect(page).not_to have_content private_project.to_reference + expect(page).not_to have_content private_project.path_with_namespace end end diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb index d30daf47543..7cf535fadae 100644 --- a/spec/helpers/labels_helper_spec.rb +++ b/spec/helpers/labels_helper_spec.rb @@ -7,7 +7,7 @@ describe LabelsHelper do context 'without subject' do it "uses the label's project" do - expect(link_to_label(label)).to match %r{<a href="/#{label.project.to_reference}/issues\?label_name%5B%5D=#{label.name}">.*</a>} + expect(link_to_label(label)).to match %r{<a href="/#{label.project.path_with_namespace}/issues\?label_name%5B%5D=#{label.name}">.*</a>} end end @@ -32,7 +32,7 @@ describe LabelsHelper do ['issue', :issue, 'merge_request', :merge_request].each do |type| context "set to #{type}" do it 'links to correct page' do - expect(link_to_label(label, type: type)).to match %r{<a href="/#{label.project.to_reference}/#{type.to_s.pluralize}\?label_name%5B%5D=#{label.name}">.*</a>} + expect(link_to_label(label, type: type)).to match %r{<a href="/#{label.project.path_with_namespace}/#{type.to_s.pluralize}\?label_name%5B%5D=#{label.name}">.*</a>} end end end diff --git a/spec/lib/banzai/filter/abstract_link_filter_spec.rb b/spec/lib/banzai/filter/abstract_link_filter_spec.rb index 1ee31a603e4..70a87fbc01e 100644 --- a/spec/lib/banzai/filter/abstract_link_filter_spec.rb +++ b/spec/lib/banzai/filter/abstract_link_filter_spec.rb @@ -5,7 +5,7 @@ describe Banzai::Filter::AbstractReferenceFilter do describe '#references_per_project' do it 'returns a Hash containing references grouped per project paths' do - doc = Nokogiri::HTML.fragment("#1 #{project.to_reference}#2") + doc = Nokogiri::HTML.fragment("#1 #{project.path_with_namespace}#2") filter = described_class.new(doc, project: project) expect(filter).to receive(:object_class).exactly(4).times.and_return(Issue) @@ -14,7 +14,7 @@ describe Banzai::Filter::AbstractReferenceFilter do refs = filter.references_per_project expect(refs).to be_an_instance_of(Hash) - expect(refs[project.to_reference]).to eq(Set.new(%w[1 2])) + expect(refs[project.path_with_namespace]).to eq(Set.new(%w[1 2])) end end diff --git a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb index e6c90ad87ee..9703e2315b8 100644 --- a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb @@ -59,9 +59,7 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do it 'ignores invalid commit IDs' do exp = act = "See #{commit1.id.reverse}...#{commit2.id}" - expect(project).to receive(:valid_repo?).and_return(true) - expect(project.repository).to receive(:commit).with(commit1.id.reverse) - expect(project.repository).to receive(:commit).with(commit2.id) + allow(project.repository).to receive(:commit).with(commit1.id.reverse) expect(reference_filter(act).to_html).to eq exp end @@ -100,14 +98,44 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do end end - context 'cross-project reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:project, :public, namespace: namespace) } - let(:reference) { range.to_reference(project) } + context 'cross-project / cross-namespace complete reference' do + let(:project2) { create(:project, :public) } + let(:reference) { "#{project2.path_with_namespace}@#{commit1.id}...#{commit2.id}" } - before do - range.project = project2 + it 'links to a valid reference' do + doc = reference_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 'link has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.css('a').first.text). + to eql("#{project2.path_with_namespace}@#{commit1.short_id}...#{commit2.short_id}") + end + + it 'has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.text).to eql("Fixed (#{project2.path_with_namespace}@#{commit1.short_id}...#{commit2.short_id}.)") + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Fixed #{project2.path_with_namespace}@#{commit1.id.reverse}...#{commit2.id}" + expect(reference_filter(act).to_html).to eq exp + + exp = act = "Fixed #{project2.path_with_namespace}@#{commit1.id}...#{commit2.id.reverse}" + expect(reference_filter(act).to_html).to eq exp end + end + + context 'cross-project / same-namespace complete reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:project, :public, namespace: namespace) } + let(:project2) { create(:project, :public, path: "same-namespace", namespace: namespace) } + let(:reference) { "#{project2.path}@#{commit1.id}...#{commit2.id}" } it 'links to a valid reference' do doc = reference_filter("See #{reference}") @@ -116,24 +144,65 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) end - it 'links with adjacent text' do + it 'link has valid text' do doc = reference_filter("Fixed (#{reference}.)") - exp = Regexp.escape("#{project2.to_reference}@#{range.reference_link_text}") - expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) + expect(doc.css('a').first.text). + to eql("#{project2.path}@#{commit1.short_id}...#{commit2.short_id}") + end + + it 'has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.text).to eql("Fixed (#{project2.path}@#{commit1.short_id}...#{commit2.short_id}.)") end it 'ignores invalid commit IDs on the referenced project' do - exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}" + exp = act = "Fixed #{project2.path}@#{commit1.id.reverse}...#{commit2.id}" expect(reference_filter(act).to_html).to eq exp - exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" + exp = act = "Fixed #{project2.path}@#{commit1.id}...#{commit2.id.reverse}" + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'cross-project shorthand reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:project, :public, namespace: namespace) } + let(:project2) { create(:project, :public, path: "same-namespace", namespace: namespace) } + let(:reference) { "#{project2.path}@#{commit1.id}...#{commit2.id}" } + + it 'links to a valid reference' do + doc = reference_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 'link has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.css('a').first.text). + to eql("#{project2.path}@#{commit1.short_id}...#{commit2.short_id}") + end + + it 'has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.text).to eql("Fixed (#{project2.path}@#{commit1.short_id}...#{commit2.short_id}.)") + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Fixed #{project2.path}@#{commit1.id.reverse}...#{commit2.id}" + expect(reference_filter(act).to_html).to eq exp + + exp = act = "Fixed #{project2.path}@#{commit1.id}...#{commit2.id.reverse}" expect(reference_filter(act).to_html).to eq exp end end context 'cross-project URL reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:namespace) { create(:namespace) } let(:project2) { create(:project, :public, namespace: namespace) } let(:range) { CommitRange.new("#{commit1.id}...master", project) } let(:reference) { urls.namespace_project_compare_url(project2.namespace, project2, from: commit1.id, to: 'master') } diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb index e0f08282551..2e6dcc3a434 100644 --- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb @@ -41,6 +41,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do it 'links with adjacent text' do doc = reference_filter("See (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{commit.short_id}<\/a>\.\)/) end @@ -48,8 +49,6 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do invalid = invalidate_reference(reference) exp = act = "See #{invalid}" - expect(project).to receive(:valid_repo?).and_return(true) - expect(project.repository).to receive(:commit).with(invalid) expect(reference_filter(act).to_html).to eq exp end @@ -95,34 +94,85 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do end end - context 'cross-project reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } + context 'cross-project / cross-namespace complete reference' do + let(:namespace) { create(:namespace) } let(:project2) { create(:project, :public, namespace: namespace) } let(:commit) { project2.commit } - let(:reference) { commit.to_reference(project) } + let(:reference) { "#{project2.path_with_namespace}@#{commit.short_id}" } - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") + it 'link has valid text' do + doc = reference_filter("See (#{reference}.)") - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id) + expect(doc.css('a').first.text).to eql("#{project2.path_with_namespace}@#{commit.short_id}") end - it 'links with adjacent text' do - doc = reference_filter("Fixed (#{reference}.)") + it 'has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.text).to eql("See (#{project2.path_with_namespace}@#{commit.short_id}.)") + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Committed #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'cross-project / same-namespace complete reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, namespace: namespace) } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:commit) { project2.commit } + let(:reference) { "#{project2.path_with_namespace}@#{commit.short_id}" } + + it 'link has valid text' do + doc = reference_filter("See (#{reference}.)") - exp = Regexp.escape(project2.to_reference) - expect(doc.to_html).to match(/\(<a.+>#{exp}@#{commit.short_id}<\/a>\.\)/) + expect(doc.css('a').first.text).to eql("#{project2.path}@#{commit.short_id}") + end + + it 'has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.text).to eql("See (#{project2.path}@#{commit.short_id}.)") end it 'ignores invalid commit IDs on the referenced project' do exp = act = "Committed #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'cross-project shorthand reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, namespace: namespace) } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:commit) { project2.commit } + let(:reference) { "#{project2.path_with_namespace}@#{commit.short_id}" } + + it 'link has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.css('a').first.text).to eql("#{project2.path}@#{commit.short_id}") + end + + it 'has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.text).to eql("See (#{project2.path}@#{commit.short_id}.)") + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Committed #{invalidate_reference(reference)}" + expect(reference_filter(act).to_html).to eq exp end end context 'cross-project URL reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:namespace) { create(:namespace) } let(:project2) { create(:project, :public, namespace: namespace) } let(:commit) { project2.commit } let(:reference) { urls.namespace_project_commit_url(project2.namespace, project2, commit.id) } diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index 8f0b2db3e8e..456dbac0698 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -8,7 +8,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do end let(:project) { create(:empty_project, :public) } - let(:issue) { create(:issue, project: project) } + let(:issue) { create(:issue, project: project) } it 'requires project context' do expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) @@ -24,7 +24,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do context 'internal reference' do it_behaves_like 'a reference containing an element node' - let(:reference) { issue.to_reference } + let(:reference) { "##{issue.iid}" } it 'ignores valid references when using non-default tracker' do allow(project).to receive(:default_issues_tracker?).and_return(false) @@ -42,7 +42,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do it 'links with adjacent text' do doc = reference_filter("Fixed (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + expect(doc.text).to eql("Fixed (#{reference}.)") end it 'ignores invalid issue IDs' do @@ -116,13 +116,56 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do end end - context 'cross-project reference' do + context 'cross-project / cross-namespace complete reference' do it_behaves_like 'a reference containing an element node' - let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:empty_project, :public) } + let(:issue) { create(:issue, project: project2) } + let(:reference) { "#{project2.path_with_namespace}##{issue.iid}" } + + it 'ignores valid references when cross-reference project uses external tracker' do + expect_any_instance_of(described_class).to receive(:find_object). + with(project2, issue.iid). + and_return(nil) + + exp = act = "Issue #{reference}" + expect(reference_filter(act).to_html).to eq exp + end + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq helper.url_for_issue(issue.iid, project2) + end + + it 'link has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.css('a').first.text).to eql("#{project2.path_with_namespace}##{issue.iid}") + end + + it 'has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.text).to eq("Fixed (#{project2.path_with_namespace}##{issue.iid}.)") + end + + it 'ignores invalid issue IDs on the referenced project' do + exp = act = "Fixed #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'cross-project / same-namespace complete reference' do + it_behaves_like 'a reference containing an element node' + + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, :public, namespace: namespace) } let(:project2) { create(:empty_project, :public, namespace: namespace) } let(:issue) { create(:issue, project: project2) } - let(:reference) { issue.to_reference(project) } + let(:reference) { "#{project2.path_with_namespace}##{issue.iid}" } it 'ignores valid references when cross-reference project uses external tracker' do expect_any_instance_of(described_class).to receive(:find_object). @@ -140,9 +183,16 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do to eq helper.url_for_issue(issue.iid, project2) end - it 'links with adjacent text' do + it 'link has valid text' do doc = reference_filter("Fixed (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + + expect(doc.css('a').first.text).to eql("#{project2.path}##{issue.iid}") + end + + it 'has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.text).to eq("Fixed (#{project2.path}##{issue.iid}.)") end it 'ignores invalid issue IDs on the referenced project' do @@ -150,9 +200,47 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do expect(reference_filter(act).to_html).to eq exp end + end + + context 'cross-project shorthand reference' do + it_behaves_like 'a reference containing an element node' - it 'ignores out-of-bounds issue IDs on the referenced project' do - exp = act = "Fixed ##{Gitlab::Database::MAX_INT_VALUE + 1}" + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, :public, namespace: namespace) } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let(:issue) { create(:issue, project: project2) } + let(:reference) { "#{project2.path}##{issue.iid}" } + + it 'ignores valid references when cross-reference project uses external tracker' do + expect_any_instance_of(described_class).to receive(:find_object). + with(project2, issue.iid). + and_return(nil) + + exp = act = "Issue #{reference}" + expect(reference_filter(act).to_html).to eq exp + end + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq helper.url_for_issue(issue.iid, project2) + end + + it 'link has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.css('a').first.text).to eql("#{project2.path}##{issue.iid}") + end + + it 'has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.text).to eq("Fixed (#{project2.path}##{issue.iid}.)") + end + + it 'ignores invalid issue IDs on the referenced project' do + exp = act = "Fixed #{invalidate_reference(reference)}" expect(reference_filter(act).to_html).to eq exp end diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index 9c09f00ae8a..284641fb20a 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -4,7 +4,7 @@ require 'html/pipeline' describe Banzai::Filter::LabelReferenceFilter, lib: true do include FilterSpecHelper - let(:project) { create(:empty_project, :public) } + let(:project) { create(:empty_project, :public, name: 'sample-project') } let(:label) { create(:label, project: project) } let(:reference) { label.to_reference } @@ -48,6 +48,14 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do expect(link).to eq urls.namespace_project_issues_path(project.namespace, project, label_name: label.name) end + context 'project that does not exist referenced' do + let(:result) { reference_filter('aaa/bbb~ccc') } + + it 'does not link reference' do + expect(result.to_html).to eq 'aaa/bbb~ccc' + end + end + describe 'label span element' do it 'includes default classes' do doc = reference_filter("Label #{reference}") @@ -334,14 +342,14 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do end context 'with project reference' do - let(:reference) { project.to_reference + group_label.to_reference(format: :name) } + let(:reference) { "#{project.to_reference}#{group_label.to_reference(format: :name)}" } it 'links to a valid reference' do doc = reference_filter("See #{reference}", project: project) expect(doc.css('a').first.attr('href')).to eq urls. namespace_project_issues_url(project.namespace, project, label_name: group_label.name) - expect(doc.text).to eq 'See gfm references' + expect(doc.text).to eq "See gfm references" end it 'links with adjacent text' do @@ -357,68 +365,247 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do end end - describe 'cross project label references' do - context 'valid project referenced' do - let(:another_project) { create(:empty_project, :public) } - let(:project_name) { another_project.name_with_namespace } - let(:label) { create(:label, project: another_project, color: '#00ff00') } - let(:reference) { label.to_reference(project) } + describe 'cross-project / cross-namespace complete reference' do + let(:project2) { create(:empty_project) } + let(:label) { create(:label, project: project2, color: '#00ff00') } + let(:reference) { "#{project2.path_with_namespace}~#{label.name}" } + let!(:result) { reference_filter("See #{reference}") } - let!(:result) { reference_filter("See #{reference}") } + it 'links to a valid reference' do + expect(result.css('a').first.attr('href')) + .to eq urls.namespace_project_issues_url(project2.namespace, + project2, + label_name: label.name) + end - it 'points to referenced project issues page' do - expect(result.css('a').first.attr('href')) - .to eq urls.namespace_project_issues_url(another_project.namespace, - another_project, - label_name: label.name) - end + it 'has valid color' do + expect(result.css('a span').first.attr('style')).to match /background-color: #00ff00/ + end - it 'has valid color' do - expect(result.css('a span').first.attr('style')) - .to match /background-color: #00ff00/ - end + it 'has valid link text' do + expect(result.css('a').first.text).to eq "#{label.name} in #{project2.name_with_namespace}" + end - it 'contains cross project content' do - expect(result.css('a').first.text).to eq "#{label.name} in #{project_name}" - end + it 'has valid text' do + expect(result.text).to eq "See #{label.name} in #{project2.name_with_namespace}" end - context 'project that does not exist referenced' do - let(:result) { reference_filter('aaa/bbb~ccc') } + it 'ignores invalid IDs on the referenced label' do + exp = act = "See #{invalidate_reference(reference)}" - it 'does not link reference' do - expect(result.to_html).to eq 'aaa/bbb~ccc' - end + expect(reference_filter(act).to_html).to eq exp + end + end + + describe 'cross-project / same-namespace complete reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, namespace: namespace) } + let(:project2) { create(:empty_project, namespace: namespace) } + let(:label) { create(:label, project: project2, color: '#00ff00') } + let(:reference) { "#{project2.path_with_namespace}~#{label.name}" } + let!(:result) { reference_filter("See #{reference}") } + + it 'links to a valid reference' do + expect(result.css('a').first.attr('href')) + .to eq urls.namespace_project_issues_url(project2.namespace, + project2, + label_name: label.name) + end + + it 'has valid color' do + expect(result.css('a span').first.attr('style')).to match /background-color: #00ff00/ + end + + it 'has valid link text' do + expect(result.css('a').first.text).to eq "#{label.name} in #{project2.name}" + end + + it 'has valid text' do + expect(result.text).to eq "See #{label.name} in #{project2.name}" + end + + it 'ignores invalid IDs on the referenced label' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + describe 'cross-project shorthand reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, namespace: namespace) } + let(:project2) { create(:empty_project, namespace: namespace) } + let(:label) { create(:label, project: project2, color: '#00ff00') } + let(:reference) { "#{project2.path}~#{label.name}" } + let!(:result) { reference_filter("See #{reference}") } + + it 'links to a valid reference' do + expect(result.css('a').first.attr('href')) + .to eq urls.namespace_project_issues_url(project2.namespace, + project2, + label_name: label.name) + end + + it 'has valid color' do + expect(result.css('a span').first.attr('style')). + to match /background-color: #00ff00/ + end + + it 'has valid link text' do + expect(result.css('a').first.text).to eq "#{label.name} in #{project2.name}" + end + + it 'has valid text' do + expect(result.text).to eq "See #{label.name} in #{project2.name}" + end + + it 'ignores invalid IDs on the referenced label' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp end end describe 'cross group label references' do - context 'valid project referenced' do - let(:group) { create(:group) } - let(:project) { create(:empty_project, :public, namespace: group) } - let(:another_group) { create(:group) } - let(:another_project) { create(:empty_project, :public, namespace: another_group) } - let(:project_name) { another_project.name_with_namespace } - let(:group_label) { create(:group_label, group: another_group, color: '#00ff00') } - let(:reference) { another_project.to_reference + group_label.to_reference } - - let!(:result) { reference_filter("See #{reference}", project: project) } - - it 'points to referenced project issues page' do - expect(result.css('a').first.attr('href')) - .to eq urls.namespace_project_issues_url(another_project.namespace, - another_project, - label_name: group_label.name) - end + let(:group) { create(:group) } + let(:project) { create(:empty_project, :public, namespace: group) } + let(:another_group) { create(:group) } + let(:another_project) { create(:empty_project, :public, namespace: another_group) } + let(:group_label) { create(:group_label, group: another_group, color: '#00ff00') } + let(:reference) { "#{another_project.path_with_namespace}~#{group_label.name}" } + let!(:result) { reference_filter("See #{reference}", project: project) } - it 'has valid color' do - expect(result.css('a span').first.attr('style')) - .to match /background-color: #00ff00/ - end + it 'points to referenced project issues page' do + expect(result.css('a').first.attr('href')) + .to eq urls.namespace_project_issues_url(another_project.namespace, + another_project, + label_name: group_label.name) + end - it 'contains cross project content' do - expect(result.css('a').first.text).to eq "#{group_label.name} in #{project_name}" - end + it 'has valid color' do + expect(result.css('a span').first.attr('style')). + to match /background-color: #00ff00/ + end + + it 'has valid link text' do + expect(result.css('a').first.text). + to eq "#{group_label.name} in #{another_project.name_with_namespace}" + end + + it 'has valid text' do + expect(result.text). + to eq "See #{group_label.name} in #{another_project.name_with_namespace}" + end + + it 'ignores invalid IDs on the referenced label' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + describe 'cross-project / same-group_label complete reference' do + let(:group) { create(:group) } + let(:project) { create(:empty_project, :public, namespace: group) } + let(:another_project) { create(:empty_project, :public, namespace: group) } + let(:group_label) { create(:group_label, group: group, color: '#00ff00') } + let(:reference) { "#{another_project.path_with_namespace}~#{group_label.name}" } + let!(:result) { reference_filter("See #{reference}", project: project) } + + it 'points to referenced project issues page' do + expect(result.css('a').first.attr('href')). + to eq urls.namespace_project_issues_url(another_project.namespace, + another_project, + label_name: group_label.name) + end + + it 'has valid color' do + expect(result.css('a span').first.attr('style')). + to match /background-color: #00ff00/ + end + + it 'has valid link text' do + expect(result.css('a').first.text). + to eq "#{group_label.name} in #{another_project.name}" + end + + it 'has valid text' do + expect(result.text). + to eq "See #{group_label.name} in #{another_project.name}" + end + + it 'ignores invalid IDs on the referenced label' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + describe 'same project / same group_label complete reference' do + let(:group) { create(:group) } + let(:project) { create(:empty_project, :public, namespace: group) } + let(:group_label) { create(:group_label, group: group, color: '#00ff00') } + let(:reference) { "#{project.path_with_namespace}~#{group_label.name}" } + let!(:result) { reference_filter("See #{reference}", project: project) } + + it 'points to referenced project issues page' do + expect(result.css('a').first.attr('href')) + .to eq urls.namespace_project_issues_url(project.namespace, + project, + label_name: group_label.name) + end + + it 'has valid color' do + expect(result.css('a span').first.attr('style')) + .to match /background-color: #00ff00/ + end + + it 'has valid link text' do + expect(result.css('a').first.text).to eq group_label.name + end + + it 'has valid text' do + expect(result.text).to eq "See #{group_label.name}" + end + + it 'ignores invalid IDs on the referenced label' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + describe 'same project / same group_label shorthand reference' do + let(:group) { create(:group) } + let(:project) { create(:empty_project, :public, namespace: group) } + let(:group_label) { create(:group_label, group: group, color: '#00ff00') } + let(:reference) { "#{project.path}~#{group_label.name}" } + let!(:result) { reference_filter("See #{reference}", project: project) } + + it 'points to referenced project issues page' do + expect(result.css('a').first.attr('href')) + .to eq urls.namespace_project_issues_url(project.namespace, + project, + label_name: group_label.name) + end + + it 'has valid color' do + expect(result.css('a span').first.attr('style')). + to match /background-color: #00ff00/ + end + + it 'has valid link text' do + expect(result.css('a').first.text).to eq group_label.name + end + + it 'has valid text' do + expect(result.text).to eq "See #{group_label.name}" + end + + it 'ignores invalid IDs on the referenced label' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp end end end diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb index 274258a045c..275010c1a2c 100644 --- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do include FilterSpecHelper - let(:project) { create(:project, :public) } + let(:project) { create(:empty_project, :public) } let(:merge) { create(:merge_request, source_project: project) } it 'requires project context' do @@ -86,23 +86,97 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do end end - context 'cross-project reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:project, :public, namespace: namespace) } - let(:merge) { create(:merge_request, source_project: project2) } - let(:reference) { merge.to_reference(project) } + context 'cross-project / cross-namespace complete reference' do + let(:project2) { create(:empty_project, :public) } + let(:merge) { create(:merge_request, source_project: project2) } + let(:reference) { "#{project2.path_with_namespace}!#{merge.iid}" } it 'links to a valid reference' do doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')). to eq urls.namespace_project_merge_request_url(project2.namespace, - project, merge) + project2, merge) end - it 'links with adjacent text' do + it 'link has valid text' do doc = reference_filter("Merge (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + + expect(doc.css('a').first.text).to eq(reference) + end + + it 'has valid text' do + doc = reference_filter("Merge (#{reference}.)") + + expect(doc.text).to eq("Merge (#{reference}.)") + end + + it 'ignores invalid merge IDs on the referenced project' do + exp = act = "Merge #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'cross-project / same-namespace complete reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, :public, namespace: namespace) } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let!(:merge) { create(:merge_request, source_project: project2) } + let(:reference) { "#{project2.path_with_namespace}!#{merge.iid}" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_merge_request_url(project2.namespace, + project2, merge) + end + + it 'link has valid text' do + doc = reference_filter("Merge (#{reference}.)") + + expect(doc.css('a').first.text).to eq("#{project2.path}!#{merge.iid}") + end + + it 'has valid text' do + doc = reference_filter("Merge (#{reference}.)") + + expect(doc.text).to eq("Merge (#{project2.path}!#{merge.iid}.)") + end + + it 'ignores invalid merge IDs on the referenced project' do + exp = act = "Merge #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'cross-project shorthand reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, :public, namespace: namespace) } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let!(:merge) { create(:merge_request, source_project: project2) } + let(:reference) { "#{project2.path}!#{merge.iid}" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_merge_request_url(project2.namespace, + project2, merge) + end + + it 'link has valid text' do + doc = reference_filter("Merge (#{reference}.)") + + expect(doc.css('a').first.text).to eq("#{project2.path}!#{merge.iid}") + end + + it 'has valid text' do + doc = reference_filter("Merge (#{reference}.)") + + expect(doc.text).to eq("Merge (#{project2.path}!#{merge.iid}.)") end it 'ignores invalid merge IDs on the referenced project' do diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb index 7419863d848..73b5edb99b3 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -148,13 +148,51 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do end end - describe 'cross project milestone references' do - let(:another_project) { create(:empty_project, :public) } - let(:project_path) { another_project.path_with_namespace } - let(:milestone) { create(:milestone, project: another_project) } - let(:reference) { milestone.to_reference(project) } + describe 'cross-project / cross-namespace complete reference' do + let(:namespace) { create(:namespace) } + let(:another_project) { create(:empty_project, :public, namespace: namespace) } + let(:milestone) { create(:milestone, project: another_project) } + let(:reference) { "#{another_project.path_with_namespace}%#{milestone.iid}" } + let!(:result) { reference_filter("See #{reference}") } - let!(:result) { reference_filter("See #{reference}") } + it 'points to referenced project milestone page' do + expect(result.css('a').first.attr('href')).to eq urls. + namespace_project_milestone_url(another_project.namespace, + another_project, + milestone) + end + + it 'link has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.css('a').first.text). + to eq("#{milestone.name} in #{another_project.path_with_namespace}") + end + + it 'has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.text). + to eq("See (#{milestone.name} in #{another_project.path_with_namespace}.)") + end + + it 'escapes the name attribute' do + allow_any_instance_of(Milestone).to receive(:title).and_return(%{"></a>whatever<a title="}) + + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.text). + to eq "#{milestone.name} in #{another_project.path_with_namespace}" + end + end + + describe 'cross-project / same-namespace complete reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, :public, namespace: namespace) } + let(:another_project) { create(:empty_project, :public, namespace: namespace) } + let(:milestone) { create(:milestone, project: another_project) } + let(:reference) { "#{another_project.path_with_namespace}%#{milestone.iid}" } + let!(:result) { reference_filter("See #{reference}") } it 'points to referenced project milestone page' do expect(result.css('a').first.attr('href')).to eq urls. @@ -163,14 +201,66 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do milestone) end - it 'contains cross project content' do - expect(result.css('a').first.text).to eq "#{milestone.name} in #{project_path}" + it 'link has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.css('a').first.text). + to eq("#{milestone.name} in #{another_project.path}") + end + + it 'has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.text). + to eq("See (#{milestone.name} in #{another_project.path}.)") end it 'escapes the name attribute' do allow_any_instance_of(Milestone).to receive(:title).and_return(%{"></a>whatever<a title="}) + + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.text). + to eq "#{milestone.name} in #{another_project.path}" + end + end + + describe 'cross project shorthand reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, :public, namespace: namespace) } + let(:another_project) { create(:empty_project, :public, namespace: namespace) } + let(:milestone) { create(:milestone, project: another_project) } + let(:reference) { "#{another_project.path}%#{milestone.iid}" } + let!(:result) { reference_filter("See #{reference}") } + + it 'points to referenced project milestone page' do + expect(result.css('a').first.attr('href')).to eq urls. + namespace_project_milestone_url(another_project.namespace, + another_project, + milestone) + end + + it 'link has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.css('a').first.text). + to eq("#{milestone.name} in #{another_project.path}") + end + + it 'has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.text). + to eq("See (#{milestone.name} in #{another_project.path}.)") + end + + it 'escapes the name attribute' do + allow_any_instance_of(Milestone).to receive(:title).and_return(%{"></a>whatever<a title="}) + doc = reference_filter("See #{reference}") - expect(doc.css('a').first.text).to eq "#{milestone.name} in #{project_path}" + + expect(doc.css('a').first.text). + to eq "#{milestone.name} in #{another_project.path}" end end end diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb index 9b92d1a3926..e036514d283 100644 --- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb @@ -79,11 +79,11 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do end end - context 'cross-project reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } + context 'cross-project / cross-namespace complete reference' do + let(:namespace) { create(:namespace) } let(:project2) { create(:empty_project, :public, namespace: namespace) } - let(:snippet) { create(:project_snippet, project: project2) } - let(:reference) { snippet.to_reference(project) } + let!(:snippet) { create(:project_snippet, project: project2) } + let(:reference) { "#{project2.path_with_namespace}$#{snippet.id}" } it 'links to a valid reference' do doc = reference_filter("See #{reference}") @@ -92,9 +92,82 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) end - it 'links with adjacent text' do + it 'link has valid text' do doc = reference_filter("See (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + + expect(doc.css('a').first.text).to eql(reference) + end + + it 'has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.text).to eql("See (#{reference}.)") + end + + it 'ignores invalid snippet IDs on the referenced project' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'cross-project / same-namespace complete reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, :public, namespace: namespace) } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let!(:snippet) { create(:project_snippet, project: project2) } + let(:reference) { "#{project2.path_with_namespace}$#{snippet.id}" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) + end + + it 'link has valid text' do + doc = reference_filter("See (#{project2.path}$#{snippet.id}.)") + + expect(doc.css('a').first.text).to eql("#{project2.path}$#{snippet.id}") + end + + it 'has valid text' do + doc = reference_filter("See (#{project2.path}$#{snippet.id}.)") + + expect(doc.text).to eql("See (#{project2.path}$#{snippet.id}.)") + end + + it 'ignores invalid snippet IDs on the referenced project' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'cross-project shorthand reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, :public, namespace: namespace) } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let!(:snippet) { create(:project_snippet, project: project2) } + let(:reference) { "#{project2.path}$#{snippet.id}" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) + end + + it 'link has valid text' do + doc = reference_filter("See (#{project2.path}$#{snippet.id}.)") + + expect(doc.css('a').first.text).to eql("#{project2.path}$#{snippet.id}") + end + + it 'has valid text' do + doc = reference_filter("See (#{project2.path}$#{snippet.id}.)") + + expect(doc.text).to eql("See (#{project2.path}$#{snippet.id}.)") end it 'ignores invalid snippet IDs on the referenced project' do diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb index 6b3dfebd85d..d619e401897 100644 --- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb @@ -64,7 +64,7 @@ describe Gitlab::Gfm::ReferenceRewriter do context 'description with project labels' do let!(:label) { create(:label, id: 123, name: 'test', project: old_project) } - let(:project_ref) { old_project.to_reference } + let(:project_ref) { old_project.to_reference(new_project) } context 'label referenced by id' do let(:text) { '#1 and ~123' } @@ -80,7 +80,7 @@ describe Gitlab::Gfm::ReferenceRewriter do context 'description with group labels' do let(:old_group) { create(:group) } let!(:group_label) { create(:group_label, id: 321, name: 'group label', group: old_group) } - let(:project_ref) { old_project.to_reference } + let(:project_ref) { old_project.to_reference(new_project) } before do old_project.update(namespace: old_group) diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb index c41359b55a3..d89d4342dea 100644 --- a/spec/models/commit_range_spec.rb +++ b/spec/models/commit_range_spec.rb @@ -45,7 +45,7 @@ describe CommitRange, models: true do end describe '#to_reference' do - let(:cross) { create(:project) } + let(:cross) { create(:empty_project, namespace: project.namespace) } it 'returns a String reference to the object' do expect(range.to_reference).to eq "#{full_sha_from}...#{full_sha_to}" @@ -56,12 +56,12 @@ describe CommitRange, models: true do end it 'supports a cross-project reference' do - expect(range.to_reference(cross)).to eq "#{project.to_reference}@#{full_sha_from}...#{full_sha_to}" + expect(range.to_reference(cross)).to eq "#{project.path}@#{full_sha_from}...#{full_sha_to}" end end describe '#reference_link_text' do - let(:cross) { create(:project) } + let(:cross) { create(:empty_project, namespace: project.namespace) } it 'returns a String reference to the object' do expect(range.reference_link_text).to eq "#{sha_from}...#{sha_to}" @@ -72,7 +72,7 @@ describe CommitRange, models: true do end it 'supports a cross-project reference' do - expect(range.reference_link_text(cross)).to eq "#{project.to_reference}@#{sha_from}...#{sha_to}" + expect(range.reference_link_text(cross)).to eq "#{project.path}@#{sha_from}...#{sha_to}" end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 7194c20d3bf..eb482c7f913 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -34,24 +34,30 @@ describe Commit, models: true do end describe '#to_reference' do + let(:project) { create(:project, path: 'sample-project') } + let(:commit) { project.commit } + it 'returns a String reference to the object' do expect(commit.to_reference).to eq commit.id end it 'supports a cross-project reference' do - cross = double('project') - expect(commit.to_reference(cross)).to eq "#{project.to_reference}@#{commit.id}" + another_project = build(:project, name: 'another-project', namespace: project.namespace) + expect(commit.to_reference(another_project)).to eq "sample-project@#{commit.id}" end end describe '#reference_link_text' do + let(:project) { create(:project, path: 'sample-project') } + let(:commit) { project.commit } + it 'returns a String reference to the object' do expect(commit.reference_link_text).to eq commit.short_id end it 'supports a cross-project reference' do - cross = double('project') - expect(commit.reference_link_text(cross)).to eq "#{project.to_reference}@#{commit.short_id}" + another_project = build(:project, name: 'another-project', namespace: project.namespace) + expect(commit.reference_link_text(another_project)).to eq "sample-project@#{commit.short_id}" end end diff --git a/spec/models/group_label_spec.rb b/spec/models/group_label_spec.rb index 2369658bf78..668aa6fb357 100644 --- a/spec/models/group_label_spec.rb +++ b/spec/models/group_label_spec.rb @@ -37,6 +37,16 @@ describe GroupLabel, models: true do end end + context 'cross-project' do + let(:namespace) { build_stubbed(:namespace) } + let(:source_project) { build_stubbed(:empty_project, name: 'project-1', namespace: namespace) } + let(:target_project) { build_stubbed(:empty_project, name: 'project-2', namespace: namespace) } + + it 'returns a String reference to the object' do + expect(label.to_reference(source_project, target_project)).to eq %(project-1~#{label.id}) + end + end + context 'using invalid format' do it 'raises error' do expect { label.to_reference(format: :invalid) } diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 89e93dce8c5..24e216329a9 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -43,14 +43,16 @@ describe Issue, models: true do end describe '#to_reference' do + let(:project) { build(:empty_project, name: 'sample-project') } + let(:issue) { build(:issue, iid: 1, project: project) } + it 'returns a String reference to the object' do - expect(subject.to_reference).to eq "##{subject.iid}" + expect(issue.to_reference).to eq "#1" end it 'supports a cross-project reference' do - cross = double('project') - expect(subject.to_reference(cross)). - to eq "#{subject.project.to_reference}##{subject.iid}" + another_project = build(:project, name: 'another-project', namespace: project.namespace) + expect(issue.to_reference(another_project)).to eq "sample-project#1" end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index ec22ef93465..b9deb04da3a 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -142,13 +142,16 @@ describe MergeRequest, models: true do end describe '#to_reference' do + let(:project) { build(:empty_project, name: 'sample-project') } + let(:merge_request) { build(:merge_request, target_project: project, iid: 1) } + it 'returns a String reference to the object' do - expect(subject.to_reference).to eq "!#{subject.iid}" + expect(merge_request.to_reference).to eq "!1" end it 'supports a cross-project reference' do - cross = double('project') - expect(subject.to_reference(cross)).to eq "#{subject.source_project.to_reference}!#{subject.iid}" + another_project = build(:project, name: 'another-project', namespace: project.namespace) + expect(merge_request.to_reference(another_project)).to eq "sample-project!1" end end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index a4bfe851dfb..0cc2efae5f9 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -246,4 +246,18 @@ describe Milestone, models: true do end end end + + describe '#to_reference' do + let(:project) { build(:empty_project, name: 'sample-project') } + let(:milestone) { build(:milestone, iid: 1, project: project) } + + it 'returns a String reference to the object' do + expect(milestone.to_reference).to eq "%1" + end + + it 'supports a cross-project reference' do + another_project = build(:project, name: 'another-project', namespace: project.namespace) + expect(milestone.to_reference(another_project)).to eq "sample-project%1" + end + end end diff --git a/spec/models/project_label_spec.rb b/spec/models/project_label_spec.rb index 18c9d449ee5..4d538cac007 100644 --- a/spec/models/project_label_spec.rb +++ b/spec/models/project_label_spec.rb @@ -105,14 +105,14 @@ describe ProjectLabel, models: true do context 'using name' do it 'returns cross reference with label name' do expect(label.to_reference(project, format: :name)) - .to eq %Q(#{label.project.to_reference}~"#{label.name}") + .to eq %Q(#{label.project.path_with_namespace}~"#{label.name}") end end context 'using id' do it 'returns cross reference with label id' do expect(label.to_reference(project, format: :id)) - .to eq %Q(#{label.project.to_reference}~#{label.id}) + .to eq %Q(#{label.project.path_with_namespace}~#{label.id}) end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 8abcce42ce0..587ca1936a3 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -258,10 +258,70 @@ describe Project, models: true do end describe '#to_reference' do - let(:project) { create(:empty_project) } + let(:owner) { create(:user, name: 'Gitlab') } + let(:namespace) { create(:namespace, path: 'sample-namespace', owner: owner) } + let(:project) { create(:empty_project, path: 'sample-project', namespace: namespace) } + + context 'when nil argument' do + it 'returns nil' do + expect(project.to_reference).to be_nil + end + end + + context 'when same project argument' do + it 'returns nil' do + expect(project.to_reference(project)).to be_nil + end + end - it 'returns a String reference to the object' do - expect(project.to_reference).to eq project.path_with_namespace + context 'when cross namespace project argument' do + let(:another_namespace_project) { create(:empty_project, name: 'another-project') } + + it 'returns complete path to the project' do + expect(project.to_reference(another_namespace_project)).to eq 'sample-namespace/sample-project' + end + end + + context 'when same namespace / cross-project argument' do + let(:another_project) { create(:empty_project, namespace: namespace) } + + it 'returns complete path to the project' do + expect(project.to_reference(another_project)).to eq 'sample-project' + end + end + end + + describe '#to_human_reference' do + let(:owner) { create(:user, name: 'Gitlab') } + let(:namespace) { create(:namespace, name: 'Sample namespace', owner: owner) } + let(:project) { create(:empty_project, name: 'Sample project', namespace: namespace) } + + context 'when nil argument' do + it 'returns nil' do + expect(project.to_human_reference).to be_nil + end + end + + context 'when same project argument' do + it 'returns nil' do + expect(project.to_human_reference(project)).to be_nil + end + end + + context 'when cross namespace project argument' do + let(:another_namespace_project) { create(:empty_project, name: 'another-project') } + + it 'returns complete name with namespace of the project' do + expect(project.to_human_reference(another_namespace_project)).to eq 'Gitlab / Sample project' + end + end + + context 'when same namespace / cross-project argument' do + let(:another_project) { create(:empty_project, namespace: namespace) } + + it 'returns name of the project' do + expect(project.to_human_reference(another_project)).to eq 'Sample project' + end end end diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index f62f6bacbaa..79d2843e122 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -33,16 +33,31 @@ describe Snippet, models: true do end describe '#to_reference' do - let(:project) { create(:empty_project) } - let(:snippet) { create(:snippet, project: project) } + context 'when snippet belongs to a project' do + let(:project) { build(:empty_project, name: 'sample-project') } + let(:snippet) { build(:snippet, id: 1, project: project) } + + it 'returns a String reference to the object' do + expect(snippet.to_reference).to eq "$1" + end - it 'returns a String reference to the object' do - expect(snippet.to_reference).to eq "$#{snippet.id}" + it 'supports a cross-project reference' do + another_project = build(:project, name: 'another-project', namespace: project.namespace) + expect(snippet.to_reference(another_project)).to eq "sample-project$1" + end end - it 'supports a cross-project reference' do - cross = double('project') - expect(snippet.to_reference(cross)).to eq "#{project.to_reference}$#{snippet.id}" + context 'when snippet does not belong to a project' do + let(:snippet) { build(:snippet, id: 1, project: nil) } + + it 'returns a String reference to the object' do + expect(snippet.to_reference).to eq "$1" + end + + it 'still returns shortest reference when project arg present' do + another_project = build(:project, name: 'another-project') + expect(snippet.to_reference(another_project)).to eq "$1" + end end end diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index c7de0d0c534..db196ed5751 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -189,7 +189,7 @@ describe Issues::MoveService, services: true do it 'rewrites references using a cross reference to old project' do expect(new_note.note) - .to eq "Note with reference to merge request #{old_project.to_reference}!1" + .to eq "Note with reference to merge request #{old_project.to_reference(new_project)}!1" end end @@ -217,7 +217,7 @@ describe Issues::MoveService, services: true do it 'rewrites referenced issues creating cross project reference' do expect(new_issue.description) - .to eq "Some description #{old_project.to_reference}#{another_issue.to_reference}" + .to eq "Some description #{another_issue.to_reference(new_project)}" end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 4a8f6c321aa..835d7467bd9 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -530,7 +530,7 @@ describe SystemNoteService, services: true do end it 'mentions referenced project' do - expect(subject.note).to include new_project.to_reference + expect(subject.note).to include new_project.path_with_namespace end end -- cgit v1.2.1 From fe957b5ed8b473b695ad53cfa9680de110e3d3ee Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira <oswluizf@gmail.com> Date: Tue, 29 Nov 2016 21:16:53 -0200 Subject: Update user markdown docs with cross-project shorthand version --- doc/user/markdown.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 162d1bd7ed4..31acfe9dcd1 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -267,6 +267,18 @@ GFM also recognizes certain cross-project references: | `namespace/project@9ba12248...b19a04f5` | commit range comparison | | `namespace/project~"Some label"` | issues with given label | +It also has a shorthand version to reference other projects from the same namespace: + +| input | references | +|:------------------------------|:------------------------| +| `project#123` | issue | +| `project!123` | merge request | +| `project%123` | milestone | +| `project$123` | snippet | +| `project@9ba12248` | specific commit | +| `project@9ba12248...b19a04f5` | commit range comparison | +| `project~"Some label"` | issues with given label | + ### Task Lists > If this is not rendered correctly, see -- cgit v1.2.1 From 436ea57f56ace872f670562501ab01216f7d41a6 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Fri, 2 Dec 2016 13:43:33 -0600 Subject: Reduce base font sizes across app --- app/assets/stylesheets/framework/blank.scss | 4 ++-- app/assets/stylesheets/framework/common.scss | 2 +- app/assets/stylesheets/framework/dropdowns.scss | 6 +++--- app/assets/stylesheets/framework/header.scss | 4 ++-- app/assets/stylesheets/framework/nav.scss | 2 +- app/assets/stylesheets/framework/variables.scss | 4 ++-- app/assets/stylesheets/pages/admin.scss | 2 +- app/assets/stylesheets/pages/awards.scss | 2 +- app/assets/stylesheets/pages/cycle_analytics.scss | 2 +- app/assets/stylesheets/pages/lint.scss | 4 ++-- app/assets/stylesheets/pages/merge_requests.scss | 2 +- app/assets/stylesheets/pages/note_form.scss | 2 +- app/assets/stylesheets/pages/projects.scss | 2 +- spec/features/merge_requests/conflicts_spec.rb | 2 +- 14 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss index 540718197e0..a2fa2e7769b 100644 --- a/app/assets/stylesheets/framework/blank.scss +++ b/app/assets/stylesheets/framework/blank.scss @@ -32,14 +32,14 @@ .blank-state-title { margin-top: 0; margin-bottom: 5px; - font-size: 19px; + font-size: 18px; font-weight: normal; } .blank-state-text { margin-top: 0; margin-bottom: $gl-padding; - font-size: 15px; + font-size: 14px; > strong { font-weight: 600; diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index cdeef6fcc9e..16646e33a4b 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -33,7 +33,7 @@ .slead { color: $common-gray; - font-size: 15px; + font-size: 14px; margin-bottom: 12px; font-weight: normal; line-height: 24px; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index e6229a35b88..33de652c06f 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -36,7 +36,7 @@ padding: 6px 8px 6px 10px; background-color: $dropdown-toggle-bg; color: $dropdown-toggle-color; - font-size: 15px; + font-size: 14px; text-align: left; border: 1px solid $border-color; border-radius: $border-radius-base; @@ -123,7 +123,7 @@ width: 240px; margin-top: 2px; margin-bottom: 0; - font-size: 15px; + font-size: 14px; font-weight: normal; padding: 8px 0; background-color: $dropdown-bg; @@ -589,7 +589,7 @@ .ui-datepicker-title { color: $gl-gray; - font-size: 15px; + font-size: 14px; line-height: 1; font-weight: normal; } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index e40ff4d4688..cc2286038c0 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -71,7 +71,7 @@ header { } .fa-caret-down { - font-size: 15px; + font-size: 14px; } } @@ -156,7 +156,7 @@ header { position: relative; padding-right: 20px; margin: 0; - font-size: 19px; + font-size: 18px; max-width: 385px; display: inline-block; line-height: $header-height; diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index d2d3fc23b6c..c84a71a624d 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -49,7 +49,7 @@ padding: $gl-btn-padding; padding-bottom: 11px; margin-bottom: -1px; - font-size: 15px; + font-size: 14px; line-height: 28px; color: $note-toolbar-color; border-bottom: 2px solid transparent; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index edfb2c33f4a..647dcfc5187 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -102,7 +102,7 @@ $well-light-text-color: #5b6169; /* * Text */ -$gl-font-size: 15px; +$gl-font-size: 14px; $gl-title-color: #333; $gl-text-color: #5c5c5c; $gl-text-color-dark: #5c5d5e; @@ -380,7 +380,7 @@ $ci-skipped-color: #888; /* * Boards */ -$issue-boards-font-size: 15px; +$issue-boards-font-size: 14px; $issue-boards-card-shadow: rgba(186, 186, 186, 0.5); /* diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss index 291372b88e3..44eac21b143 100644 --- a/app/assets/stylesheets/pages/admin.scss +++ b/app/assets/stylesheets/pages/admin.scss @@ -156,7 +156,7 @@ } span { - font-size: 19px; + font-size: 18px; } } } diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index dce5c31f282..c13cb4a02b2 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -127,7 +127,7 @@ .award-control-icon { float: left; margin-right: 5px; - font-size: 19px; + font-size: 18px; } .award-control-icon-loading { diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index e7a2c91003f..57146e1fccd 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -109,7 +109,7 @@ &.title { line-height: 19px; - font-size: 15px; + font-size: 14px; font-weight: 600; color: $gl-title-color; } diff --git a/app/assets/stylesheets/pages/lint.scss b/app/assets/stylesheets/pages/lint.scss index 8d30bd64278..a7c80dce424 100644 --- a/app/assets/stylesheets/pages/lint.scss +++ b/app/assets/stylesheets/pages/lint.scss @@ -1,11 +1,11 @@ .ci-body { .incorrect-syntax { - font-size: 19px; + font-size: 18px; color: $lint-incorrect-color; } .correct-syntax { - font-size: 19px; + font-size: 18px; color: $lint-correct-color; } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 1c6fe7afe14..dc5945e1139 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -116,7 +116,7 @@ @media (max-width: $screen-xs-max) { h4 { - font-size: 15px; + font-size: 14px; } p { diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index ff092d53845..c35d71f9e7b 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -129,7 +129,7 @@ .note-edit-form { display: none; - font-size: 15px; + font-size: 14px; .md-area { background-color: $white-light; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 62862c72b3b..72b6685d940 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -521,7 +521,7 @@ a.deploy-project-label { .nav > li > a { padding: 0; background-color: transparent; - font-size: 15px; + font-size: 14px; line-height: 29px; color: $notes-light-color; diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb index d258ff52bbb..5bc4ab2dfe5 100644 --- a/spec/features/merge_requests/conflicts_spec.rb +++ b/spec/features/merge_requests/conflicts_spec.rb @@ -20,7 +20,7 @@ feature 'Merge request conflict resolution', js: true, feature: true do within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do all('button', text: 'Use ours').each do |button| - button.click + button.trigger('click') end end -- cgit v1.2.1 From ee32496d14669a42f79b2a9f61f150949d9543cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= <alejorro70@gmail.com> Date: Fri, 2 Dec 2016 20:28:31 -0300 Subject: Update CHANGELOG.md for 8.14.3 [ci skip] --- CHANGELOG.md | 9 +++++++++ ...-merge-request-dashboard-page-takes-over-a-minute-to-load.yml | 4 ---- changelogs/unreleased/25199-fix-broken-urls-in-help-page.yml | 4 ---- changelogs/unreleased/fix-github-branch-formatter.yml | 4 ---- changelogs/unreleased/process-commit-worker-improvements.yml | 4 ---- 5 files changed, 9 insertions(+), 16 deletions(-) delete mode 100644 changelogs/unreleased/24669-merge-request-dashboard-page-takes-over-a-minute-to-load.yml delete mode 100644 changelogs/unreleased/25199-fix-broken-urls-in-help-page.yml delete mode 100644 changelogs/unreleased/fix-github-branch-formatter.yml delete mode 100644 changelogs/unreleased/process-commit-worker-improvements.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 86b30d2832d..482937262eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 8.14.3 (2016-12-02) + +- Pass commit data to ProcessCommitWorker to reduce Git overhead. !7744 +- Speed up issuable dashboards. +- Don't change relative URLs to absolute URLs in the Help page. +- Fixes "ActionView::Template::Error: undefined method `text?` for nil:NilClass" on MR pages. +- Fix branch validation for GitHub PR where repo/fork was renamed/deleted. +- Validate state param when filtering issuables. + ## 8.14.2 (2016-12-01) - Remove caching of events data. !6578 diff --git a/changelogs/unreleased/24669-merge-request-dashboard-page-takes-over-a-minute-to-load.yml b/changelogs/unreleased/24669-merge-request-dashboard-page-takes-over-a-minute-to-load.yml deleted file mode 100644 index 01b19a47ecd..00000000000 --- a/changelogs/unreleased/24669-merge-request-dashboard-page-takes-over-a-minute-to-load.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Speed up issuable dashboards -merge_request: -author: diff --git a/changelogs/unreleased/25199-fix-broken-urls-in-help-page.yml b/changelogs/unreleased/25199-fix-broken-urls-in-help-page.yml deleted file mode 100644 index 58efd9113f2..00000000000 --- a/changelogs/unreleased/25199-fix-broken-urls-in-help-page.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don't change relative URLs to absolute URLs in the Help page -merge_request: -author: diff --git a/changelogs/unreleased/fix-github-branch-formatter.yml b/changelogs/unreleased/fix-github-branch-formatter.yml deleted file mode 100644 index c8698f507de..00000000000 --- a/changelogs/unreleased/fix-github-branch-formatter.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix branch validation for GitHub PR where repo/fork was renamed/deleted -merge_request: -author: diff --git a/changelogs/unreleased/process-commit-worker-improvements.yml b/changelogs/unreleased/process-commit-worker-improvements.yml deleted file mode 100644 index 0038c6e34e6..00000000000 --- a/changelogs/unreleased/process-commit-worker-improvements.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Pass commit data to ProcessCommitWorker to reduce Git overhead -merge_request: 7744 -author: -- cgit v1.2.1 From ab385ea7120c36ad185468b9a7c05caa646a38d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= <alejorro70@gmail.com> Date: Fri, 2 Dec 2016 20:32:08 -0300 Subject: Update CHANGELOG.md for 8.13.8 [ci skip] --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 482937262eb..e03123111c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -251,6 +251,11 @@ entry. - Fix "Without projects" filter. !6611 (Ben Bodenmiller) - Fix 404 when visit /projects page +## 8.13.8 (2016-12-02) + +- Pass tag SHA to post-receive hook when tag is created via UI. !7700 +- Validate state param when filtering issuables. + ## 8.13.7 (2016-11-28) - fixes 500 error on project show when user is not logged in and project is still empty. !7376 -- cgit v1.2.1 From 2de245c7bad211cd85a4a23f9110aac82f2bb0a2 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 10 Nov 2016 15:24:52 -0600 Subject: temporarily revert "Added forceLoad ability to singleFileDiffs, added callback to getContentHTML, added conditional force load if a collapsed diff line anchor is found" This reverts commit d2ee380816fa161d94da54c1f7e594c9a2ba2241. --- app/assets/javascripts/merge_request_tabs.js | 18 ++---------------- app/assets/javascripts/single_file_diff.js | 19 +++++++------------ 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 4a192ca5796..907f8f924d2 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -240,11 +240,8 @@ _this.expandViewContainer(); } _this.diffsLoaded = true; - var anchoredDiff = gl.utils.getLocationHash(); - if (anchoredDiff) _this.openAnchoredDiff(anchoredDiff, function() { - _this.scrollToElement("#diffs"); - _this.highlighSelectedLine(); - }); + _this.scrollToElement("#diffs"); + _this.highlighSelectedLine(); _this.filesCommentButton = $('.files .diff-file').filesCommentButton(); return $(document).off('click', '.diff-line-num a').on('click', '.diff-line-num a', function(e) { e.preventDefault(); @@ -257,17 +254,6 @@ }); }; - MergeRequestTabs.prototype.openAnchoredDiff = function(anchoredDiff, cb) { - var diffTitle = $('#file-path-' + anchoredDiff); - var diffFile = diffTitle.closest('.diff-file'); - var nothingHereBlock = $('.nothing-here-block:visible', diffFile); - if (nothingHereBlock.length) { - diffFile.singleFileDiff(true, cb); - } else { - cb(); - } - }; - MergeRequestTabs.prototype.highlighSelectedLine = function() { var $diffLine, diffLineTop, hashClassString, locationHash, navBarHeight; $('.hll').removeClass('hll'); diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index 2767849e673..f5e06e6c817 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -13,7 +13,7 @@ COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>'; - function SingleFileDiff(file, forceLoad, cb) { + function SingleFileDiff(file) { this.file = file; this.toggleDiff = bind(this.toggleDiff, this); this.content = $('.diff-content', this.file); @@ -32,12 +32,9 @@ this.$toggleIcon.addClass('fa-caret-down'); } $('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff); - if (forceLoad) { - this.toggleDiff(null, cb); - } } - SingleFileDiff.prototype.toggleDiff = function(e, cb) { + SingleFileDiff.prototype.toggleDiff = function(e) { var $target = $(e.target); if (!$target.hasClass('file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return; this.isOpen = !this.isOpen; @@ -57,11 +54,11 @@ } } else { this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right'); - return this.getContentHTML(cb); + return this.getContentHTML(); } }; - SingleFileDiff.prototype.getContentHTML = function(cb) { + SingleFileDiff.prototype.getContentHTML = function() { this.collapsedContent.hide(); this.loadingContent.show(); $.get(this.diffForPath, (function(_this) { @@ -79,8 +76,6 @@ if (typeof gl.diffNotesCompileComponents !== 'undefined') { gl.diffNotesCompileComponents(); } - - if (cb) cb(); }; })(this)); }; @@ -89,10 +84,10 @@ })(); - $.fn.singleFileDiff = function(forceLoad, cb) { + $.fn.singleFileDiff = function() { return this.each(function() { - if (!$.data(this, 'singleFileDiff') || forceLoad) { - return $.data(this, 'singleFileDiff', new SingleFileDiff(this, forceLoad, cb)); + if (!$.data(this, 'singleFileDiff')) { + return $.data(this, 'singleFileDiff', new SingleFileDiff(this)); } }); }; -- cgit v1.2.1 From 6c978994e1daf25da87d8b2cb6d8f1954022241f Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 25 Oct 2016 01:27:21 -0500 Subject: fix diff line highlighting by moving method from the MergeRequestTabs class to the Diff class. --- app/assets/javascripts/diff.js | 44 +++++++++++++++++++++----- app/assets/javascripts/merge_request_tabs.js | 46 +++++++--------------------- 2 files changed, 48 insertions(+), 42 deletions(-) diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index 00da5f17f9f..66580587629 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -11,21 +11,21 @@ if (this.diffViewType() === 'parallel') { $('.content-wrapper .container-fluid').removeClass('container-limited'); } - $(document).off('click', '.js-unfold'); - $(document).on('click', '.js-unfold', (function(_this) { - return function(event) { + $(document) + .off('click', '.js-unfold') + .on('click', '.js-unfold', (function(event) { var line_number, link, file, offset, old_line, params, prev_new_line, prev_old_line, ref, ref1, since, target, to, unfold, unfoldBottom; target = $(event.target); unfoldBottom = target.hasClass('js-unfold-bottom'); unfold = true; - ref = _this.lineNumbers(target.parent()), old_line = ref[0], line_number = ref[1]; + ref = this.lineNumbers(target.parent()), old_line = ref[0], line_number = ref[1]; offset = line_number - old_line; if (unfoldBottom) { line_number += 1; since = line_number; to = line_number + UNFOLD_COUNT; } else { - ref1 = _this.lineNumbers(target.parent().prev()), prev_old_line = ref1[0], prev_new_line = ref1[1]; + ref1 = this.lineNumbers(target.parent().prev()), prev_old_line = ref1[0], prev_new_line = ref1[1]; line_number -= 1; to = line_number; if (line_number - UNFOLD_COUNT > prev_new_line + 1) { @@ -48,8 +48,22 @@ return $.get(link, params, function(response) { return target.parent().replaceWith(response); }); - }; - })(this)); + }).bind(this)); + + $(document) + .off('click', '.diff-line-num a') + .on('click', '.diff-line-num a', (function(e) { + var hash = $(e.currentTarget).attr('href'); + e.preventDefault(); + if ( history.pushState ) { + history.pushState(null, null, hash); + } else { + window.location.hash = hash; + } + this.highlighSelectedLine(); + }).bind(this)); + + this.highlighSelectedLine(); } Diff.prototype.diffViewType = function() { @@ -66,6 +80,22 @@ }); }; + Diff.prototype.highlighSelectedLine = function() { + var $diffLine, dataLineString, locationHash; + $('.hll').removeClass('hll'); + locationHash = window.location.hash; + if (locationHash !== '') { + dataLineString = '[data-line-code="' + locationHash.replace('#', '') + '"]'; + $diffLine = $(".diff-file " + locationHash + ":not(.match)"); + if (!$diffLine.is('tr')) { + $diffLine = $(".diff-file td" + locationHash + ", .diff-file td" + dataLineString); + } else { + $diffLine = $diffLine.find('td'); + } + $diffLine.addClass('hll'); + } + }; + return Diff; })(); diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 907f8f924d2..d34fc4bc87c 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -76,14 +76,16 @@ } MergeRequestTabs.prototype.bindEvents = function() { - $(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown); - $(document).on('click', '.js-show-tab', this.showTab); - }; + $(document) + .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) + .on('click', '.js-show-tab', this.showTab); + } MergeRequestTabs.prototype.unbindEvents = function() { - $(document).off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown); - $(document).off('click', '.js-show-tab', this.showTab); - }; + $(document) + .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) + .off('click', '.js-show-tab', this.showTab); + } MergeRequestTabs.prototype.showTab = function(event) { event.preventDefault(); @@ -235,45 +237,19 @@ gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); $('#diffs .js-syntax-highlight').syntaxHighlight(); - $('#diffs .diff-file').singleFileDiff(); + if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) { _this.expandViewContainer(); } _this.diffsLoaded = true; _this.scrollToElement("#diffs"); - _this.highlighSelectedLine(); - _this.filesCommentButton = $('.files .diff-file').filesCommentButton(); - return $(document).off('click', '.diff-line-num a').on('click', '.diff-line-num a', function(e) { - e.preventDefault(); - window.location.hash = $(e.currentTarget).attr('href'); - _this.highlighSelectedLine(); - return _this.scrollToElement("#diffs"); - }); + + new Diff(); }; })(this) }); }; - MergeRequestTabs.prototype.highlighSelectedLine = function() { - var $diffLine, diffLineTop, hashClassString, locationHash, navBarHeight; - $('.hll').removeClass('hll'); - locationHash = window.location.hash; - if (locationHash !== '') { - dataLineString = '[data-line-code="' + locationHash.replace('#', '') + '"]'; - $diffLine = $(locationHash + ":not(.match)", $('#diffs')); - if (!$diffLine.is('tr')) { - $diffLine = $('#diffs').find("td" + locationHash + ", td" + dataLineString); - } else { - $diffLine = $diffLine.find('td'); - } - if ($diffLine.length) { - $diffLine.addClass('hll'); - diffLineTop = $diffLine.offset().top; - return navBarHeight = $('.navbar-gitlab').outerHeight(); - } - } - }; - MergeRequestTabs.prototype.loadBuilds = function(source) { if (this.buildsLoaded) { return; -- cgit v1.2.1 From ac0d9b34aa7940f9fe0d6025296cc1997fc44e36 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 25 Oct 2016 01:43:03 -0500 Subject: update CHANGELOG.md to reflect changes in !7090 --- changelogs/unreleased/23696-fix-diff-view-highlighting.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/23696-fix-diff-view-highlighting.yml diff --git a/changelogs/unreleased/23696-fix-diff-view-highlighting.yml b/changelogs/unreleased/23696-fix-diff-view-highlighting.yml new file mode 100644 index 00000000000..db523caffed --- /dev/null +++ b/changelogs/unreleased/23696-fix-diff-view-highlighting.yml @@ -0,0 +1,4 @@ +--- +title: Fix diff view permalink highlighting +merge_request: 7090 +author: -- cgit v1.2.1 From f14c5ae888606d230b26724bcea12aa50c44b01f Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 25 Oct 2016 10:42:25 -0500 Subject: refactor MergeRequestTabs to es6 class syntax --- app/assets/javascripts/merge_request.js | 2 +- app/assets/javascripts/merge_request_tabs.js | 405 ----------------------- app/assets/javascripts/merge_request_tabs.js.es6 | 402 ++++++++++++++++++++++ spec/javascripts/merge_request_tabs_spec.js | 2 +- 4 files changed, 404 insertions(+), 407 deletions(-) delete mode 100644 app/assets/javascripts/merge_request_tabs.js create mode 100644 app/assets/javascripts/merge_request_tabs.js.es6 diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index a4b4db14db8..88c3636be6c 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -40,7 +40,7 @@ if (window.mrTabs) { window.mrTabs.unbindEvents(); } - window.mrTabs = new MergeRequestTabs(this.opts); + window.mrTabs = new gl.MergeRequestTabs(this.opts); }; MergeRequest.prototype.showAllCommits = function() { diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js deleted file mode 100644 index d34fc4bc87c..00000000000 --- a/app/assets/javascripts/merge_request_tabs.js +++ /dev/null @@ -1,405 +0,0 @@ -/* eslint-disable max-len, func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-undef, one-var, one-var-declaration-per-line, quotes, comma-dangle, consistent-return, prefer-template, no-param-reassign, camelcase, vars-on-top, space-in-parens, curly, prefer-arrow-callback, no-unused-vars, no-return-assign, semi, object-shorthand, operator-assignment, padded-blocks, max-len */ -// MergeRequestTabs -// -// Handles persisting and restoring the current tab selection and lazily-loading -// content on the MergeRequests#show page. -// -/*= require js.cookie */ - -// -// ### Example Markup -// -// <ul class="nav-links merge-request-tabs"> -// <li class="notes-tab active"> -// <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1"> -// Discussion -// </a> -// </li> -// <li class="commits-tab"> -// <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits"> -// Commits -// </a> -// </li> -// <li class="diffs-tab"> -// <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs"> -// Diffs -// </a> -// </li> -// </ul> -// -// <div class="tab-content"> -// <div class="notes tab-pane active" id="notes"> -// Notes Content -// </div> -// <div class="commits tab-pane" id="commits"> -// Commits Content -// </div> -// <div class="diffs tab-pane" id="diffs"> -// Diffs Content -// </div> -// </div> -// -// <div class="mr-loading-status"> -// <div class="loading"> -// Loading Animation -// </div> -// </div> -// -(function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - this.MergeRequestTabs = (function() { - MergeRequestTabs.prototype.diffsLoaded = false; - - MergeRequestTabs.prototype.buildsLoaded = false; - - MergeRequestTabs.prototype.pipelinesLoaded = false; - - MergeRequestTabs.prototype.commitsLoaded = false; - - MergeRequestTabs.prototype.fixedLayoutPref = null; - - function MergeRequestTabs(opts) { - this.opts = opts != null ? opts : {}; - this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true; - - this.buildsLoaded = this.opts.buildsLoaded || false; - - this.setCurrentAction = bind(this.setCurrentAction, this); - this.tabShown = bind(this.tabShown, this); - this.showTab = bind(this.showTab, this); - // Store the `location` object, allowing for easier stubbing in tests - this._location = location; - this.bindEvents(); - this.activateTab(this.opts.action); - this.initAffix(); - } - - MergeRequestTabs.prototype.bindEvents = function() { - $(document) - .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) - .on('click', '.js-show-tab', this.showTab); - } - - MergeRequestTabs.prototype.unbindEvents = function() { - $(document) - .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) - .off('click', '.js-show-tab', this.showTab); - } - - MergeRequestTabs.prototype.showTab = function(event) { - event.preventDefault(); - return this.activateTab($(event.target).data('action')); - }; - - MergeRequestTabs.prototype.tabShown = function(event) { - var $target, action, navBarHeight; - $target = $(event.target); - action = $target.data('action'); - if (action === 'commits') { - this.loadCommits($target.attr('href')); - this.expandView(); - this.resetViewContainer(); - } else if (this.isDiffAction(action)) { - this.loadDiff($target.attr('href')); - if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') { - this.shrinkView(); - } - if (this.diffViewType() === 'parallel') { - this.expandViewContainer(); - } - navBarHeight = $('.navbar-gitlab').outerHeight(); - $.scrollTo(".merge-request-details .merge-request-tabs", { - offset: -navBarHeight - }); - } else if (action === 'builds') { - this.loadBuilds($target.attr('href')); - this.expandView(); - this.resetViewContainer(); - } else if (action === 'pipelines') { - this.loadPipelines($target.attr('href')); - this.expandView(); - this.resetViewContainer(); - } else { - this.expandView(); - this.resetViewContainer(); - } - if (this.opts.setUrl) { - this.setCurrentAction(action); - } - }; - - MergeRequestTabs.prototype.scrollToElement = function(container) { - var $el, navBarHeight; - if (window.location.hash) { - navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + document.querySelector('.js-tabs-affix').offsetHeight; - $el = $(container + " " + window.location.hash + ":not(.match)"); - if ($el.length) { - return $.scrollTo(container + " " + window.location.hash + ":not(.match)", { - offset: -navBarHeight - }); - } - } - }; - - // Activate a tab based on the current action - MergeRequestTabs.prototype.activateTab = function(action) { - if (action === 'show') { - action = 'notes'; - } - // important note: the .tab('show') method triggers 'shown.bs.tab' event itself - $(".merge-request-tabs a[data-action='" + action + "']").tab('show'); - }; - - // Replaces the current Merge Request-specific action in the URL with a new one - // - // If the action is "notes", the URL is reset to the standard - // `MergeRequests#show` route. - // - // Examples: - // - // location.pathname # => "/namespace/project/merge_requests/1" - // setCurrentAction('diffs') - // location.pathname # => "/namespace/project/merge_requests/1/diffs" - // - // location.pathname # => "/namespace/project/merge_requests/1/diffs" - // setCurrentAction('notes') - // location.pathname # => "/namespace/project/merge_requests/1" - // - // location.pathname # => "/namespace/project/merge_requests/1/diffs" - // setCurrentAction('commits') - // location.pathname # => "/namespace/project/merge_requests/1/commits" - // - // Returns the new URL String - MergeRequestTabs.prototype.setCurrentAction = function(action) { - var new_state; - // Normalize action, just to be safe - if (action === 'show') { - action = 'notes'; - } - this.currentAction = action; - // Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs' - new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, ''); - - // Append the new action if we're on a tab other than 'notes' - if (action !== 'notes') { - new_state += "/" + action; - } - // Ensure parameters and hash come along for the ride - new_state += this._location.search + this._location.hash; - history.replaceState({ - turbolinks: true, - url: new_state - // Replace the current history state with the new one without breaking - // Turbolinks' history. - // - // See https://github.com/rails/turbolinks/issues/363 - }, document.title, new_state); - return new_state; - }; - - MergeRequestTabs.prototype.loadCommits = function(source) { - if (this.commitsLoaded) { - return; - } - return this._get({ - url: source + ".json", - success: (function(_this) { - return function(data) { - document.querySelector("div#commits").innerHTML = data.html; - gl.utils.localTimeAgo($('.js-timeago', 'div#commits')); - _this.commitsLoaded = true; - return _this.scrollToElement("#commits"); - }; - })(this) - }); - }; - - MergeRequestTabs.prototype.loadDiff = function(source) { - if (this.diffsLoaded) { - return; - } - - // We extract pathname for the current Changes tab anchor href - // some pages like MergeRequestsController#new has query parameters on that anchor - var url = document.createElement('a'); - url.href = source; - - return this._get({ - url: (url.pathname + ".json") + this._location.search, - success: (function(_this) { - return function(data) { - $('#diffs').html(data.html); - - if (typeof gl.diffNotesCompileComponents !== 'undefined') { - gl.diffNotesCompileComponents(); - } - - gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); - $('#diffs .js-syntax-highlight').syntaxHighlight(); - - if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) { - _this.expandViewContainer(); - } - _this.diffsLoaded = true; - _this.scrollToElement("#diffs"); - - new Diff(); - }; - })(this) - }); - }; - - MergeRequestTabs.prototype.loadBuilds = function(source) { - if (this.buildsLoaded) { - return; - } - return this._get({ - url: source + ".json", - success: (function(_this) { - return function(data) { - document.querySelector("div#builds").innerHTML = data.html; - gl.utils.localTimeAgo($('.js-timeago', 'div#builds')); - _this.buildsLoaded = true; - if (!this.pipelines) this.pipelines = new gl.Pipelines(); - return _this.scrollToElement("#builds"); - }; - })(this) - }); - }; - - MergeRequestTabs.prototype.loadPipelines = function(source) { - if (this.pipelinesLoaded) { - return; - } - return this._get({ - url: source + ".json", - success: function(data) { - $('#pipelines').html(data.html); - gl.utils.localTimeAgo($('.js-timeago', '#pipelines')); - this.pipelinesLoaded = true; - return this.scrollToElement("#pipelines"); - }.bind(this) - }); - }; - - // Show or hide the loading spinner - // - // status - Boolean, true to show, false to hide - MergeRequestTabs.prototype.toggleLoading = function(status) { - return $('.mr-loading-status .loading').toggle(status); - }; - - MergeRequestTabs.prototype._get = function(options) { - var defaults; - defaults = { - beforeSend: (function(_this) { - return function() { - return _this.toggleLoading(true); - }; - })(this), - complete: (function(_this) { - return function() { - return _this.toggleLoading(false); - }; - })(this), - dataType: 'json', - type: 'GET' - }; - options = $.extend({}, defaults, options); - return $.ajax(options); - }; - - MergeRequestTabs.prototype.diffViewType = function() { - return $('.inline-parallel-buttons a.active').data('view-type'); - }; - - MergeRequestTabs.prototype.isDiffAction = function(action) { - return action === 'diffs' || action === 'new/diffs' - }; - - MergeRequestTabs.prototype.expandViewContainer = function() { - var $wrapper = $('.content-wrapper .container-fluid'); - if (this.fixedLayoutPref === null) { - this.fixedLayoutPref = $wrapper.hasClass('container-limited'); - } - $wrapper.removeClass('container-limited'); - }; - - MergeRequestTabs.prototype.resetViewContainer = function() { - if (this.fixedLayoutPref !== null) { - $('.content-wrapper .container-fluid') - .toggleClass('container-limited', this.fixedLayoutPref); - } - }; - - MergeRequestTabs.prototype.shrinkView = function() { - var $gutterIcon; - $gutterIcon = $('.js-sidebar-toggle i:visible'); - return setTimeout(function() { - if ($gutterIcon.is('.fa-angle-double-right')) { - return $gutterIcon.closest('a').trigger('click', [true]); - } - // Wait until listeners are set - // Only when sidebar is expanded - }, 0); - }; - - MergeRequestTabs.prototype.expandView = function() { - var $gutterIcon; - if (Cookies.get('collapsed_gutter') === 'true') { - return; - } - $gutterIcon = $('.js-sidebar-toggle i:visible'); - return setTimeout(function() { - if ($gutterIcon.is('.fa-angle-double-left')) { - return $gutterIcon.closest('a').trigger('click', [true]); - } - }, 0); - // Expand the issuable sidebar unless the user explicitly collapsed it - // Wait until listeners are set - // Only when sidebar is collapsed - }; - - MergeRequestTabs.prototype.initAffix = function () { - var $tabs = $('.js-tabs-affix'); - - // Screen space on small screens is usually very sparse - // So we dont affix the tabs on these - if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return; - - var $diffTabs = $('#diff-notes-app'), - $fixedNav = $('.navbar-fixed-top'), - $layoutNav = $('.layout-nav'); - - $tabs.off('affix.bs.affix affix-top.bs.affix') - .affix({ - offset: { - top: function () { - var tabsTop = $diffTabs.offset().top - $tabs.height(); - tabsTop = tabsTop - ($fixedNav.height() + $layoutNav.height()); - - return tabsTop; - } - } - }).on('affix.bs.affix', function () { - $diffTabs.css({ - marginTop: $tabs.height() - }); - }).on('affix-top.bs.affix', function () { - $diffTabs.css({ - marginTop: '' - }); - }); - - // Fix bug when reloading the page already scrolling - if ($tabs.hasClass('affix')) { - $tabs.trigger('affix.bs.affix'); - } - }; - - return MergeRequestTabs; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 new file mode 100644 index 00000000000..ded6e85a648 --- /dev/null +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -0,0 +1,402 @@ +/* eslint-disable max-len, func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-undef, one-var, one-var-declaration-per-line, quotes, comma-dangle, consistent-return, prefer-template, no-param-reassign, camelcase, vars-on-top, space-in-parens, curly, prefer-arrow-callback, no-unused-vars, no-return-assign, semi, object-shorthand, operator-assignment, padded-blocks, max-len */ +// MergeRequestTabs +// +// Handles persisting and restoring the current tab selection and lazily-loading +// content on the MergeRequests#show page. +// +/*= require js.cookie */ + +// +// ### Example Markup +// +// <ul class="nav-links merge-request-tabs"> +// <li class="notes-tab active"> +// <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1"> +// Discussion +// </a> +// </li> +// <li class="commits-tab"> +// <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits"> +// Commits +// </a> +// </li> +// <li class="diffs-tab"> +// <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs"> +// Diffs +// </a> +// </li> +// </ul> +// +// <div class="tab-content"> +// <div class="notes tab-pane active" id="notes"> +// Notes Content +// </div> +// <div class="commits tab-pane" id="commits"> +// Commits Content +// </div> +// <div class="diffs tab-pane" id="diffs"> +// Diffs Content +// </div> +// </div> +// +// <div class="mr-loading-status"> +// <div class="loading"> +// Loading Animation +// </div> +// </div> +// +((global) => { + + class MergeRequestTabs { + + constructor({ action, setUrl, buildsLoaded } = {}) { + this.diffsLoaded = false; + this.buildsLoaded = false; + this.pipelinesLoaded = false; + this.commitsLoaded = false; + this.fixedLayoutPref = null; + + this.setUrl = setUrl !== undefined ? setUrl : true; + this.buildsLoaded = buildsLoaded || false; + + this.setCurrentAction = this.setCurrentAction.bind(this); + this.tabShown = this.tabShown.bind(this); + this.showTab = this.showTab.bind(this); + + // Store the `location` object, allowing for easier stubbing in tests + this._location = window.location; + this.bindEvents(); + this.activateTab(action); + this.initAffix(); + } + + bindEvents() { + $(document) + .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) + .on('click', '.js-show-tab', this.showTab); + } + + unbindEvents() { + $(document) + .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) + .off('click', '.js-show-tab', this.showTab); + } + + showTab(event) { + event.preventDefault(); + this.activateTab($(event.target).data('action')); + } + + tabShown(event) { + var $target, action, navBarHeight; + $target = $(event.target); + action = $target.data('action'); + if (action === 'commits') { + this.loadCommits($target.attr('href')); + this.expandView(); + this.resetViewContainer(); + } else if (this.isDiffAction(action)) { + this.loadDiff($target.attr('href')); + if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') { + this.shrinkView(); + } + if (this.diffViewType() === 'parallel') { + this.expandViewContainer(); + } + navBarHeight = $('.navbar-gitlab').outerHeight(); + $.scrollTo(".merge-request-details .merge-request-tabs", { + offset: -navBarHeight + }); + } else if (action === 'builds') { + this.loadBuilds($target.attr('href')); + this.expandView(); + this.resetViewContainer(); + } else if (action === 'pipelines') { + this.loadPipelines($target.attr('href')); + this.expandView(); + this.resetViewContainer(); + } else { + this.expandView(); + this.resetViewContainer(); + } + if (this.setUrl) { + this.setCurrentAction(action); + } + } + + scrollToElement(container) { + var $el, navBarHeight; + if (window.location.hash) { + navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + document.querySelector('.js-tabs-affix').offsetHeight; + $el = $(container + " " + window.location.hash + ":not(.match)"); + if ($el.length) { + return $.scrollTo(container + " " + window.location.hash + ":not(.match)", { + offset: -navBarHeight + }); + } + } + } + + // Activate a tab based on the current action + activateTab(action) { + if (action === 'show') { + action = 'notes'; + } + // important note: the .tab('show') method triggers 'shown.bs.tab' event itself + $(".merge-request-tabs a[data-action='" + action + "']").tab('show'); + } + + // Replaces the current Merge Request-specific action in the URL with a new one + // + // If the action is "notes", the URL is reset to the standard + // `MergeRequests#show` route. + // + // Examples: + // + // location.pathname # => "/namespace/project/merge_requests/1" + // setCurrentAction('diffs') + // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // + // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // setCurrentAction('notes') + // location.pathname # => "/namespace/project/merge_requests/1" + // + // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // setCurrentAction('commits') + // location.pathname # => "/namespace/project/merge_requests/1/commits" + // + // Returns the new URL String + setCurrentAction(action) { + var new_state; + // Normalize action, just to be safe + if (action === 'show') { + action = 'notes'; + } + this.currentAction = action; + // Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs' + new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, ''); + + // Append the new action if we're on a tab other than 'notes' + if (action !== 'notes') { + new_state += "/" + action; + } + // Ensure parameters and hash come along for the ride + new_state += this._location.search + this._location.hash; + + // Replace the current history state with the new one without breaking + // Turbolinks' history. + // + // See https://github.com/rails/turbolinks/issues/363 + history.replaceState({ + turbolinks: true, + url: new_state + }, document.title, new_state); + + return new_state; + } + + loadCommits(source) { + if (this.commitsLoaded) { + return; + } + this.ajaxGet({ + url: source + ".json", + success: (function(_this) { + return function(data) { + document.querySelector("div#commits").innerHTML = data.html; + gl.utils.localTimeAgo($('.js-timeago', 'div#commits')); + _this.commitsLoaded = true; + _this.scrollToElement("#commits"); + }; + })(this) + }); + } + + loadDiff(source) { + if (this.diffsLoaded) { + return; + } + + // We extract pathname for the current Changes tab anchor href + // some pages like MergeRequestsController#new has query parameters on that anchor + var url = document.createElement('a'); + url.href = source; + + this.ajaxGet({ + url: (url.pathname + ".json") + this._location.search, + success: (function(_this) { + return function(data) { + $('#diffs').html(data.html); + + if (typeof gl.diffNotesCompileComponents !== 'undefined') { + gl.diffNotesCompileComponents(); + } + + gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); + $('#diffs .js-syntax-highlight').syntaxHighlight(); + + if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) { + _this.expandViewContainer(); + } + _this.diffsLoaded = true; + _this.scrollToElement("#diffs"); + + new Diff(); + }; + })(this) + }); + } + + loadBuilds(source) { + if (this.buildsLoaded) { + return; + } + this.ajaxGet({ + url: source + ".json", + success: (function(_this) { + return function(data) { + document.querySelector("div#builds").innerHTML = data.html; + gl.utils.localTimeAgo($('.js-timeago', 'div#builds')); + _this.buildsLoaded = true; + if (!this.pipelines) this.pipelines = new gl.Pipelines(); + _this.scrollToElement("#builds"); + }; + })(this) + }); + } + + loadPipelines(source) { + if (this.pipelinesLoaded) { + return; + } + this.ajaxGet({ + url: source + ".json", + success: function(data) { + $('#pipelines').html(data.html); + gl.utils.localTimeAgo($('.js-timeago', '#pipelines')); + this.pipelinesLoaded = true; + this.scrollToElement("#pipelines"); + }.bind(this) + }); + } + + // Show or hide the loading spinner + // + // status - Boolean, true to show, false to hide + toggleLoading(status) { + $('.mr-loading-status .loading').toggle(status); + } + + ajaxGet(options) { + var defaults = { + beforeSend: (function(_this) { + return function() { + _this.toggleLoading(true); + }; + })(this), + complete: (function(_this) { + return function() { + _this.toggleLoading(false); + }; + })(this), + dataType: 'json', + type: 'GET' + }; + options = $.extend({}, defaults, options); + $.ajax(options); + } + + diffViewType() { + return $('.inline-parallel-buttons a.active').data('view-type'); + } + + isDiffAction(action) { + return action === 'diffs' || action === 'new/diffs' + } + + expandViewContainer() { + var $wrapper = $('.content-wrapper .container-fluid'); + if (this.fixedLayoutPref === null) { + this.fixedLayoutPref = $wrapper.hasClass('container-limited'); + } + $wrapper.removeClass('container-limited'); + } + + resetViewContainer() { + if (this.fixedLayoutPref !== null) { + $('.content-wrapper .container-fluid') + .toggleClass('container-limited', this.fixedLayoutPref); + } + } + + shrinkView() { + var $gutterIcon; + $gutterIcon = $('.js-sidebar-toggle i:visible'); + + // Wait until listeners are set + setTimeout(function() { + // Only when sidebar is expanded + if ($gutterIcon.is('.fa-angle-double-right')) { + $gutterIcon.closest('a').trigger('click', [true]); + } + }, 0); + } + + // Expand the issuable sidebar unless the user explicitly collapsed it + expandView() { + var $gutterIcon; + if (Cookies.get('collapsed_gutter') === 'true') { + return; + } + $gutterIcon = $('.js-sidebar-toggle i:visible'); + + // Wait until listeners are set + setTimeout(function() { + // Only when sidebar is collapsed + if ($gutterIcon.is('.fa-angle-double-left')) { + $gutterIcon.closest('a').trigger('click', [true]); + } + }, 0); + } + + initAffix() { + var $tabs = $('.js-tabs-affix'); + + // Screen space on small screens is usually very sparse + // So we dont affix the tabs on these + if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return; + + var $diffTabs = $('#diff-notes-app'), + $fixedNav = $('.navbar-fixed-top'), + $layoutNav = $('.layout-nav'); + + $tabs.off('affix.bs.affix affix-top.bs.affix') + .affix({ + offset: { + top: function () { + var tabsTop = $diffTabs.offset().top - $tabs.height(); + tabsTop = tabsTop - ($fixedNav.height() + $layoutNav.height()); + + return tabsTop; + } + } + }).on('affix.bs.affix', function () { + $diffTabs.css({ + marginTop: $tabs.height() + }); + }).on('affix-top.bs.affix', function () { + $diffTabs.css({ + marginTop: '' + }); + }); + + // Fix bug when reloading the page already scrolling + if ($tabs.hasClass('affix')) { + $tabs.trigger('affix.bs.affix'); + } + } + } + + global.MergeRequestTabs = MergeRequestTabs; + +})(window.gl || (window.gl = {})); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 971222c44e1..6435e5a511c 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -17,7 +17,7 @@ }; fixture.preload('merge_request_tabs.html'); beforeEach(function() { - this["class"] = new MergeRequestTabs(); + this["class"] = new gl.MergeRequestTabs(); return this.spies = { ajax: spyOn($, 'ajax').and.callFake(function() {}), history: spyOn(history, 'replaceState').and.callFake(function() {}) -- cgit v1.2.1 From 2e217bc8656a4aca0b547c5670728dd7a7008797 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 25 Oct 2016 13:15:11 -0500 Subject: refactor awkward bind workarounds into arrow functions --- app/assets/javascripts/merge_request_tabs.js.es6 | 113 +++++++++-------------- 1 file changed, 44 insertions(+), 69 deletions(-) diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index ded6e85a648..9587f5ca07f 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -201,14 +201,12 @@ } this.ajaxGet({ url: source + ".json", - success: (function(_this) { - return function(data) { - document.querySelector("div#commits").innerHTML = data.html; - gl.utils.localTimeAgo($('.js-timeago', 'div#commits')); - _this.commitsLoaded = true; - _this.scrollToElement("#commits"); - }; - })(this) + success: (data) => { + document.querySelector("div#commits").innerHTML = data.html; + gl.utils.localTimeAgo($('.js-timeago', 'div#commits')); + this.commitsLoaded = true; + this.scrollToElement("#commits"); + } }); } @@ -224,26 +222,24 @@ this.ajaxGet({ url: (url.pathname + ".json") + this._location.search, - success: (function(_this) { - return function(data) { - $('#diffs').html(data.html); - - if (typeof gl.diffNotesCompileComponents !== 'undefined') { - gl.diffNotesCompileComponents(); - } - - gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); - $('#diffs .js-syntax-highlight').syntaxHighlight(); - - if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) { - _this.expandViewContainer(); - } - _this.diffsLoaded = true; - _this.scrollToElement("#diffs"); - - new Diff(); - }; - })(this) + success: (data) => { + $('#diffs').html(data.html); + + if (typeof gl.diffNotesCompileComponents !== 'undefined') { + gl.diffNotesCompileComponents(); + } + + gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); + $('#diffs .js-syntax-highlight').syntaxHighlight(); + + if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction) ) { + this.expandViewContainer(); + } + this.diffsLoaded = true; + this.scrollToElement("#diffs"); + + new Diff(); + } }); } @@ -253,15 +249,13 @@ } this.ajaxGet({ url: source + ".json", - success: (function(_this) { - return function(data) { - document.querySelector("div#builds").innerHTML = data.html; - gl.utils.localTimeAgo($('.js-timeago', 'div#builds')); - _this.buildsLoaded = true; - if (!this.pipelines) this.pipelines = new gl.Pipelines(); - _this.scrollToElement("#builds"); - }; - })(this) + success: (data) => { + document.querySelector("div#builds").innerHTML = data.html; + gl.utils.localTimeAgo($('.js-timeago', 'div#builds')); + this.buildsLoaded = true; + new gl.Pipelines(); + this.scrollToElement("#builds"); + } }); } @@ -271,12 +265,12 @@ } this.ajaxGet({ url: source + ".json", - success: function(data) { + success: (data) => { $('#pipelines').html(data.html); gl.utils.localTimeAgo($('.js-timeago', '#pipelines')); this.pipelinesLoaded = true; this.scrollToElement("#pipelines"); - }.bind(this) + } }); } @@ -289,16 +283,8 @@ ajaxGet(options) { var defaults = { - beforeSend: (function(_this) { - return function() { - _this.toggleLoading(true); - }; - })(this), - complete: (function(_this) { - return function() { - _this.toggleLoading(false); - }; - })(this), + beforeSend: () => this.toggleLoading(true), + complete: () => this.toggleLoading(false), dataType: 'json', type: 'GET' }; @@ -334,7 +320,7 @@ $gutterIcon = $('.js-sidebar-toggle i:visible'); // Wait until listeners are set - setTimeout(function() { + setTimeout(() => { // Only when sidebar is expanded if ($gutterIcon.is('.fa-angle-double-right')) { $gutterIcon.closest('a').trigger('click', [true]); @@ -351,7 +337,7 @@ $gutterIcon = $('.js-sidebar-toggle i:visible'); // Wait until listeners are set - setTimeout(function() { + setTimeout(() => { // Only when sidebar is collapsed if ($gutterIcon.is('.fa-angle-double-left')) { $gutterIcon.closest('a').trigger('click', [true]); @@ -371,24 +357,13 @@ $layoutNav = $('.layout-nav'); $tabs.off('affix.bs.affix affix-top.bs.affix') - .affix({ - offset: { - top: function () { - var tabsTop = $diffTabs.offset().top - $tabs.height(); - tabsTop = tabsTop - ($fixedNav.height() + $layoutNav.height()); - - return tabsTop; - } - } - }).on('affix.bs.affix', function () { - $diffTabs.css({ - marginTop: $tabs.height() - }); - }).on('affix-top.bs.affix', function () { - $diffTabs.css({ - marginTop: '' - }); - }); + .affix({ offset: { + top: () => ( + $diffTabs.offset().top - $tabs.height() - $fixedNav.height() - $layoutNav.height() + ) + }}) + .on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() })) + .on('affix-top.bs.affix', () => $diffTabs.css({ marginTop: '' })); // Fix bug when reloading the page already scrolling if ($tabs.hasClass('affix')) { -- cgit v1.2.1 From 6cbc305daf9a30e01943c5d990424d21fb907010 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 25 Oct 2016 13:33:03 -0500 Subject: prefer es6 string interpolation over concatination --- app/assets/javascripts/merge_request_tabs.js.es6 | 53 ++++++++++++------------ 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index 9587f5ca07f..b0e068fc37b 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -88,9 +88,9 @@ } tabShown(event) { - var $target, action, navBarHeight; - $target = $(event.target); - action = $target.data('action'); + const $target = $(event.target); + const action = $target.data('action'); + if (action === 'commits') { this.loadCommits($target.attr('href')); this.expandView(); @@ -103,7 +103,7 @@ if (this.diffViewType() === 'parallel') { this.expandViewContainer(); } - navBarHeight = $('.navbar-gitlab').outerHeight(); + const navBarHeight = $('.navbar-gitlab').outerHeight(); $.scrollTo(".merge-request-details .merge-request-tabs", { offset: -navBarHeight }); @@ -125,12 +125,12 @@ } scrollToElement(container) { - var $el, navBarHeight; if (window.location.hash) { - navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + document.querySelector('.js-tabs-affix').offsetHeight; - $el = $(container + " " + window.location.hash + ":not(.match)"); + const navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + document.querySelector('.js-tabs-affix').offsetHeight; + const navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight(); + const $el = $(`${container} ${window.location.hash}:not(.match)`); if ($el.length) { - return $.scrollTo(container + " " + window.location.hash + ":not(.match)", { + $.scrollTo($el[0], { offset: -navBarHeight }); } @@ -143,7 +143,7 @@ action = 'notes'; } // important note: the .tab('show') method triggers 'shown.bs.tab' event itself - $(".merge-request-tabs a[data-action='" + action + "']").tab('show'); + $(`.merge-request-tabs a[data-action='${action}']`).tab('show'); } // Replaces the current Merge Request-specific action in the URL with a new one @@ -167,19 +167,20 @@ // // Returns the new URL String setCurrentAction(action) { - var new_state; // Normalize action, just to be safe if (action === 'show') { action = 'notes'; } this.currentAction = action; + // Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs' - new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, ''); + let new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, ''); // Append the new action if we're on a tab other than 'notes' if (action !== 'notes') { - new_state += "/" + action; + new_state += `/${action}`; } + // Ensure parameters and hash come along for the ride new_state += this._location.search + this._location.hash; @@ -200,7 +201,7 @@ return; } this.ajaxGet({ - url: source + ".json", + url: `${source}.json`, success: (data) => { document.querySelector("div#commits").innerHTML = data.html; gl.utils.localTimeAgo($('.js-timeago', 'div#commits')); @@ -217,11 +218,11 @@ // We extract pathname for the current Changes tab anchor href // some pages like MergeRequestsController#new has query parameters on that anchor - var url = document.createElement('a'); + const url = document.createElement('a'); url.href = source; this.ajaxGet({ - url: (url.pathname + ".json") + this._location.search, + url: `${url.pathname}.json${this._location.search}`, success: (data) => { $('#diffs').html(data.html); @@ -248,7 +249,7 @@ return; } this.ajaxGet({ - url: source + ".json", + url: `${source}.json`, success: (data) => { document.querySelector("div#builds").innerHTML = data.html; gl.utils.localTimeAgo($('.js-timeago', 'div#builds')); @@ -264,7 +265,7 @@ return; } this.ajaxGet({ - url: source + ".json", + url: `${source}.json`, success: (data) => { $('#pipelines').html(data.html); gl.utils.localTimeAgo($('.js-timeago', '#pipelines')); @@ -282,7 +283,7 @@ } ajaxGet(options) { - var defaults = { + const defaults = { beforeSend: () => this.toggleLoading(true), complete: () => this.toggleLoading(false), dataType: 'json', @@ -301,7 +302,7 @@ } expandViewContainer() { - var $wrapper = $('.content-wrapper .container-fluid'); + const $wrapper = $('.content-wrapper .container-fluid'); if (this.fixedLayoutPref === null) { this.fixedLayoutPref = $wrapper.hasClass('container-limited'); } @@ -316,8 +317,7 @@ } shrinkView() { - var $gutterIcon; - $gutterIcon = $('.js-sidebar-toggle i:visible'); + const $gutterIcon = $('.js-sidebar-toggle i:visible'); // Wait until listeners are set setTimeout(() => { @@ -330,11 +330,10 @@ // Expand the issuable sidebar unless the user explicitly collapsed it expandView() { - var $gutterIcon; if (Cookies.get('collapsed_gutter') === 'true') { return; } - $gutterIcon = $('.js-sidebar-toggle i:visible'); + const $gutterIcon = $('.js-sidebar-toggle i:visible'); // Wait until listeners are set setTimeout(() => { @@ -346,15 +345,15 @@ } initAffix() { - var $tabs = $('.js-tabs-affix'); + const $tabs = $('.js-tabs-affix'); // Screen space on small screens is usually very sparse // So we dont affix the tabs on these if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return; - var $diffTabs = $('#diff-notes-app'), - $fixedNav = $('.navbar-fixed-top'), - $layoutNav = $('.layout-nav'); + const $diffTabs = $('#diff-notes-app'); + const $fixedNav = $('.navbar-fixed-top'); + const $layoutNav = $('.layout-nav'); $tabs.off('affix.bs.affix affix-top.bs.affix') .affix({ offset: { -- cgit v1.2.1 From 4f107f3fc4333fe2234fac1bf1247164542484a2 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 25 Oct 2016 14:25:46 -0500 Subject: refactor Diff to es6 class syntax --- app/assets/javascripts/diff.js | 103 ----------------------- app/assets/javascripts/diff.js.es6 | 96 +++++++++++++++++++++ app/assets/javascripts/dispatcher.js.es6 | 10 +-- app/assets/javascripts/merge_request_tabs.js.es6 | 2 +- 4 files changed, 102 insertions(+), 109 deletions(-) delete mode 100644 app/assets/javascripts/diff.js create mode 100644 app/assets/javascripts/diff.js.es6 diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js deleted file mode 100644 index 66580587629..00000000000 --- a/app/assets/javascripts/diff.js +++ /dev/null @@ -1,103 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, max-len, one-var, camelcase, one-var-declaration-per-line, no-unused-vars, no-unused-expressions, no-sequences, object-shorthand, comma-dangle, prefer-arrow-callback, semi, radix, padded-blocks, max-len */ -(function() { - this.Diff = (function() { - var UNFOLD_COUNT; - - UNFOLD_COUNT = 20; - - function Diff() { - $('.files .diff-file').singleFileDiff(); - this.filesCommentButton = $('.files .diff-file').filesCommentButton(); - if (this.diffViewType() === 'parallel') { - $('.content-wrapper .container-fluid').removeClass('container-limited'); - } - $(document) - .off('click', '.js-unfold') - .on('click', '.js-unfold', (function(event) { - var line_number, link, file, offset, old_line, params, prev_new_line, prev_old_line, ref, ref1, since, target, to, unfold, unfoldBottom; - target = $(event.target); - unfoldBottom = target.hasClass('js-unfold-bottom'); - unfold = true; - ref = this.lineNumbers(target.parent()), old_line = ref[0], line_number = ref[1]; - offset = line_number - old_line; - if (unfoldBottom) { - line_number += 1; - since = line_number; - to = line_number + UNFOLD_COUNT; - } else { - ref1 = this.lineNumbers(target.parent().prev()), prev_old_line = ref1[0], prev_new_line = ref1[1]; - line_number -= 1; - to = line_number; - if (line_number - UNFOLD_COUNT > prev_new_line + 1) { - since = line_number - UNFOLD_COUNT; - } else { - since = prev_new_line + 1; - unfold = false; - } - } - file = target.parents('.diff-file'); - link = file.data('blob-diff-path'); - params = { - since: since, - to: to, - bottom: unfoldBottom, - offset: offset, - unfold: unfold, - view: file.data('view') - }; - return $.get(link, params, function(response) { - return target.parent().replaceWith(response); - }); - }).bind(this)); - - $(document) - .off('click', '.diff-line-num a') - .on('click', '.diff-line-num a', (function(e) { - var hash = $(e.currentTarget).attr('href'); - e.preventDefault(); - if ( history.pushState ) { - history.pushState(null, null, hash); - } else { - window.location.hash = hash; - } - this.highlighSelectedLine(); - }).bind(this)); - - this.highlighSelectedLine(); - } - - Diff.prototype.diffViewType = function() { - return $('.inline-parallel-buttons a.active').data('view-type'); - } - - Diff.prototype.lineNumbers = function(line) { - if (!line.children().length) { - return [0, 0]; - } - - return line.find('.diff-line-num').map(function() { - return parseInt($(this).data('linenumber')); - }); - }; - - Diff.prototype.highlighSelectedLine = function() { - var $diffLine, dataLineString, locationHash; - $('.hll').removeClass('hll'); - locationHash = window.location.hash; - if (locationHash !== '') { - dataLineString = '[data-line-code="' + locationHash.replace('#', '') + '"]'; - $diffLine = $(".diff-file " + locationHash + ":not(.match)"); - if (!$diffLine.is('tr')) { - $diffLine = $(".diff-file td" + locationHash + ", .diff-file td" + dataLineString); - } else { - $diffLine = $diffLine.find('td'); - } - $diffLine.addClass('hll'); - } - }; - - return Diff; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6 new file mode 100644 index 00000000000..27aa67ba09d --- /dev/null +++ b/app/assets/javascripts/diff.js.es6 @@ -0,0 +1,96 @@ +/* eslint-disable */ + +((global) => { + const UNFOLD_COUNT = 20; + + class Diff { + constructor() { + $('.files .diff-file').singleFileDiff(); + $('.files .diff-file').filesCommentButton(); + + if (this.diffViewType() === 'parallel') { + $('.content-wrapper .container-fluid').removeClass('container-limited'); + } + $(document) + .off('click', '.js-unfold') + .on('click', '.js-unfold', (event) => { + var line_number, link, file, offset, old_line, params, prev_new_line, prev_old_line, ref, ref1, since, target, to, unfold, unfoldBottom; + target = $(event.target); + unfoldBottom = target.hasClass('js-unfold-bottom'); + unfold = true; + ref = this.lineNumbers(target.parent()), old_line = ref[0], line_number = ref[1]; + offset = line_number - old_line; + if (unfoldBottom) { + line_number += 1; + since = line_number; + to = line_number + UNFOLD_COUNT; + } else { + ref1 = this.lineNumbers(target.parent().prev()), prev_old_line = ref1[0], prev_new_line = ref1[1]; + line_number -= 1; + to = line_number; + if (line_number - UNFOLD_COUNT > prev_new_line + 1) { + since = line_number - UNFOLD_COUNT; + } else { + since = prev_new_line + 1; + unfold = false; + } + } + file = target.parents('.diff-file'); + link = file.data('blob-diff-path'); + params = { + since: since, + to: to, + bottom: unfoldBottom, + offset: offset, + unfold: unfold, + view: file.data('view') + }; + return $.get(link, params, function(response) { + return target.parent().replaceWith(response); + }); + }) + .off('click', '.diff-line-num a') + .on('click', '.diff-line-num a', (event) => { + var hash = $(event.currentTarget).attr('href'); + event.preventDefault(); + if ( history.pushState ) { + history.pushState(null, null, hash); + } else { + window.location.hash = hash; + } + this.highlighSelectedLine(); + }); + + this.highlighSelectedLine(); + } + + diffViewType() { + return $('.inline-parallel-buttons a.active').data('view-type'); + } + + lineNumbers(line) { + if (!line.children().length) { + return [0, 0]; + } + + return line.find('.diff-line-num').map(function() { + return parseInt($(this).data('linenumber')); + }); + } + + highlighSelectedLine() { + const $diffFiles = $('.diff-file'); + $diffFiles.find('.hll').removeClass('hll'); + + if (window.location.hash !== '') { + const hash = window.location.hash.replace('#', ''); + $diffFiles + .find(`tr#${hash}:not(.match) td, td#${hash}, td[data-line-code="${hash}"]`) + .addClass('hll'); + } + } + } + + global.Diff = Diff; + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index ab521c6c1fc..3a7c5ff3681 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -61,7 +61,7 @@ new ZenMode(); break; case 'projects:compare:show': - new Diff(); + new gl.Diff(); break; case 'projects:issues:new': case 'projects:issues:edit': @@ -74,7 +74,7 @@ break; case 'projects:merge_requests:new': case 'projects:merge_requests:edit': - new Diff(); + new gl.Diff(); shortcut_handler = new ShortcutsNavigation(); new GLForm($('.merge-request-form')); new IssuableForm($('.merge-request-form')); @@ -91,7 +91,7 @@ new GLForm($('.release-form')); break; case 'projects:merge_requests:show': - new Diff(); + new gl.Diff(); shortcut_handler = new ShortcutsIssuable(true); new ZenMode(); new MergedButtons(); @@ -101,7 +101,7 @@ new MergedButtons(); break; case "projects:merge_requests:diffs": - new Diff(); + new gl.Diff(); new ZenMode(); new MergedButtons(); break; @@ -117,7 +117,7 @@ break; case 'projects:commit:show': new Commit(); - new Diff(); + new gl.Diff(); new ZenMode(); shortcut_handler = new ShortcutsNavigation(); break; diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index b0e068fc37b..e45de446e4c 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -239,7 +239,7 @@ this.diffsLoaded = true; this.scrollToElement("#diffs"); - new Diff(); + new gl.Diff(); } }); } -- cgit v1.2.1 From 1d3b77683dc13bdf354436c9f6b16e6842810488 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 25 Oct 2016 20:22:09 -0500 Subject: refactor Diff class event bindings --- app/assets/javascripts/diff.js.es6 | 96 +++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6 index 27aa67ba09d..343285cebd1 100644 --- a/app/assets/javascripts/diff.js.es6 +++ b/app/assets/javascripts/diff.js.es6 @@ -11,59 +11,59 @@ if (this.diffViewType() === 'parallel') { $('.content-wrapper .container-fluid').removeClass('container-limited'); } + $(document) - .off('click', '.js-unfold') - .on('click', '.js-unfold', (event) => { - var line_number, link, file, offset, old_line, params, prev_new_line, prev_old_line, ref, ref1, since, target, to, unfold, unfoldBottom; - target = $(event.target); - unfoldBottom = target.hasClass('js-unfold-bottom'); - unfold = true; - ref = this.lineNumbers(target.parent()), old_line = ref[0], line_number = ref[1]; - offset = line_number - old_line; - if (unfoldBottom) { - line_number += 1; - since = line_number; - to = line_number + UNFOLD_COUNT; - } else { - ref1 = this.lineNumbers(target.parent().prev()), prev_old_line = ref1[0], prev_new_line = ref1[1]; - line_number -= 1; - to = line_number; - if (line_number - UNFOLD_COUNT > prev_new_line + 1) { - since = line_number - UNFOLD_COUNT; - } else { - since = prev_new_line + 1; - unfold = false; - } - } - file = target.parents('.diff-file'); - link = file.data('blob-diff-path'); - params = { - since: since, - to: to, - bottom: unfoldBottom, - offset: offset, - unfold: unfold, - view: file.data('view') - }; - return $.get(link, params, function(response) { - return target.parent().replaceWith(response); - }); - }) - .off('click', '.diff-line-num a') - .on('click', '.diff-line-num a', (event) => { - var hash = $(event.currentTarget).attr('href'); - event.preventDefault(); - if ( history.pushState ) { - history.pushState(null, null, hash); - } else { - window.location.hash = hash; - } - this.highlighSelectedLine(); - }); + .off('click', '.js-unfold, .diff-line-num a') + .on('click', '.js-unfold', this.handleClickUnfold.bind(this)) + .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this)); this.highlighSelectedLine(); } + handleClickUnfold(event) { + const $target = $(event.target); + const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent()); + const offset = newLineNumber - oldLineNumber; + const bottom = $target.hasClass('js-unfold-bottom'); + let since, to; + let unfold = true; + + if (bottom) { + const lineNumber = newLineNumber + 1; + since = lineNumber; + to = lineNumber + UNFOLD_COUNT; + } else { + const lineNumber = newLineNumber - 1; + since = lineNumber - UNFOLD_COUNT; + to = lineNumber; + + // make sure we aren't loading more than we need + const [prevOldLine, prevNewLine] = this.lineNumbers($target.parent().prev()); + if (since <= prevNewLine + 1) { + since = prevNewLine + 1; + unfold = false; + } + } + + const file = $target.parents('.diff-file'); + const link = file.data('blob-diff-path'); + const view = file.data('view'); + + const params = { since, to, bottom, offset, unfold, view }; + $.get(link, params, (response) => $target.parent().replaceWith(response)); + } + + handleClickLineNum(event) { + const hash = $(event.currentTarget).attr('href'); + event.preventDefault(); + if (history.pushState) { + history.pushState(null, null, hash); + } else { + window.location.hash = hash; + } + this.highlighSelectedLine(); + }; + diffViewType() { return $('.inline-parallel-buttons a.active').data('view-type'); } -- cgit v1.2.1 From 825fea63a39aa586b07ba9ffc62252a90435c8ad Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 26 Oct 2016 09:37:17 -0500 Subject: remove array destructuring until we can fix babel config --- app/assets/javascripts/diff.js.es6 | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6 index 343285cebd1..462658419be 100644 --- a/app/assets/javascripts/diff.js.es6 +++ b/app/assets/javascripts/diff.js.es6 @@ -22,7 +22,11 @@ handleClickUnfold(event) { const $target = $(event.target); - const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent()); + // current babel config relies on iterators implementation, so we cannot simply do: + // const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent()); + const ref = this.lineNumbers($target.parent()); + const oldLineNumber = ref[0]; + const newLineNumber = ref[1]; const offset = newLineNumber - oldLineNumber; const bottom = $target.hasClass('js-unfold-bottom'); let since, to; @@ -38,7 +42,7 @@ to = lineNumber; // make sure we aren't loading more than we need - const [prevOldLine, prevNewLine] = this.lineNumbers($target.parent().prev()); + const prevNewLine = this.lineNumbers($target.parent().prev())[1]; if (since <= prevNewLine + 1) { since = prevNewLine + 1; unfold = false; -- cgit v1.2.1 From f6624b5ce433b7148287e898f164476608868433 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 27 Oct 2016 15:32:30 -0500 Subject: fix eslint failures on Diff and MergeRequestTabs --- app/assets/javascripts/diff.js.es6 | 19 ++-- app/assets/javascripts/merge_request_tabs.js.es6 | 108 +++++++++++----------- spec/javascripts/merge_request_tabs_spec.js | 109 ++++++++++++----------- 3 files changed, 119 insertions(+), 117 deletions(-) diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6 index 462658419be..eacf53d1e1b 100644 --- a/app/assets/javascripts/diff.js.es6 +++ b/app/assets/javascripts/diff.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* eslint-disable class-methods-use-this, no-param-reassign */ ((global) => { const UNFOLD_COUNT = 20; @@ -29,7 +29,8 @@ const newLineNumber = ref[1]; const offset = newLineNumber - oldLineNumber; const bottom = $target.hasClass('js-unfold-bottom'); - let since, to; + let since; + let to; let unfold = true; if (bottom) { @@ -54,19 +55,19 @@ const view = file.data('view'); const params = { since, to, bottom, offset, unfold, view }; - $.get(link, params, (response) => $target.parent().replaceWith(response)); + $.get(link, params, response => $target.parent().replaceWith(response)); } handleClickLineNum(event) { const hash = $(event.currentTarget).attr('href'); event.preventDefault(); - if (history.pushState) { - history.pushState(null, null, hash); + if (window.history.pushState) { + window.history.pushState(null, null, hash); } else { window.location.hash = hash; } this.highlighSelectedLine(); - }; + } diffViewType() { return $('.inline-parallel-buttons a.active').data('view-type'); @@ -76,10 +77,7 @@ if (!line.children().length) { return [0, 0]; } - - return line.find('.diff-line-num').map(function() { - return parseInt($(this).data('linenumber')); - }); + return line.find('.diff-line-num').map((i, elm) => parseInt($(elm).data('linenumber'), 10)); } highlighSelectedLine() { @@ -96,5 +94,4 @@ } global.Diff = Diff; - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index e45de446e4c..d3248a4757e 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -1,11 +1,14 @@ -/* eslint-disable max-len, func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-undef, one-var, one-var-declaration-per-line, quotes, comma-dangle, consistent-return, prefer-template, no-param-reassign, camelcase, vars-on-top, space-in-parens, curly, prefer-arrow-callback, no-unused-vars, no-return-assign, semi, object-shorthand, operator-assignment, padded-blocks, max-len */ +/* eslint-disable no-new, no-param-reassign, class-methods-use-this */ +/* global Breakpoints, Cookies, DiffNotesApp */ + +/*= require js.cookie */ +/*= require breakpoints */ + +/* eslint-disable max-len */ // MergeRequestTabs // // Handles persisting and restoring the current tab selection and lazily-loading // content on the MergeRequests#show page. -// -/*= require js.cookie */ - // // ### Example Markup // @@ -45,11 +48,15 @@ // </div> // </div> // +/* eslint-enable max-len */ + ((global) => { + // Store the `location` object, allowing for easier stubbing in tests + let location = window.location; class MergeRequestTabs { - constructor({ action, setUrl, buildsLoaded } = {}) { + constructor({ action, setUrl, buildsLoaded, stubLocation } = {}) { this.diffsLoaded = false; this.buildsLoaded = false; this.pipelinesLoaded = false; @@ -63,8 +70,10 @@ this.tabShown = this.tabShown.bind(this); this.showTab = this.showTab.bind(this); - // Store the `location` object, allowing for easier stubbing in tests - this._location = window.location; + if (stubLocation) { + location = stubLocation; + } + this.bindEvents(); this.activateTab(action); this.initAffix(); @@ -97,15 +106,15 @@ this.resetViewContainer(); } else if (this.isDiffAction(action)) { this.loadDiff($target.attr('href')); - if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') { + if (Breakpoints.get().getBreakpointSize() !== 'lg') { this.shrinkView(); } if (this.diffViewType() === 'parallel') { this.expandViewContainer(); } const navBarHeight = $('.navbar-gitlab').outerHeight(); - $.scrollTo(".merge-request-details .merge-request-tabs", { - offset: -navBarHeight + $.scrollTo('.merge-request-details .merge-request-tabs', { + offset: -navBarHeight, }); } else if (action === 'builds') { this.loadBuilds($target.attr('href')); @@ -125,13 +134,12 @@ } scrollToElement(container) { - if (window.location.hash) { + if (location.hash) { const navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + document.querySelector('.js-tabs-affix').offsetHeight; - const navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight(); - const $el = $(`${container} ${window.location.hash}:not(.match)`); + const $el = $(`${container} ${location.hash}:not(.match)`); if ($el.length) { $.scrollTo($el[0], { - offset: -navBarHeight + offset: -navBarHeight, }); } } @@ -139,11 +147,9 @@ // Activate a tab based on the current action activateTab(action) { - if (action === 'show') { - action = 'notes'; - } + const activate = action === 'show' ? 'notes' : action; // important note: the .tab('show') method triggers 'shown.bs.tab' event itself - $(`.merge-request-tabs a[data-action='${action}']`).tab('show'); + $(`.merge-request-tabs a[data-action='${activate}']`).tab('show'); } // Replaces the current Merge Request-specific action in the URL with a new one @@ -167,33 +173,29 @@ // // Returns the new URL String setCurrentAction(action) { - // Normalize action, just to be safe - if (action === 'show') { - action = 'notes'; - } - this.currentAction = action; + this.currentAction = action === 'show' ? 'notes' : action; // Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs' - let new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, ''); + let newState = location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, ''); // Append the new action if we're on a tab other than 'notes' - if (action !== 'notes') { - new_state += `/${action}`; + if (this.currentAction !== 'notes') { + newState += `/${this.currentAction}`; } // Ensure parameters and hash come along for the ride - new_state += this._location.search + this._location.hash; + newState += location.search + location.hash; // Replace the current history state with the new one without breaking // Turbolinks' history. // // See https://github.com/rails/turbolinks/issues/363 - history.replaceState({ + window.history.replaceState({ turbolinks: true, - url: new_state - }, document.title, new_state); + url: newState, + }, document.title, newState); - return new_state; + return newState; } loadCommits(source) { @@ -203,11 +205,11 @@ this.ajaxGet({ url: `${source}.json`, success: (data) => { - document.querySelector("div#commits").innerHTML = data.html; + document.querySelector('div#commits').innerHTML = data.html; gl.utils.localTimeAgo($('.js-timeago', 'div#commits')); this.commitsLoaded = true; - this.scrollToElement("#commits"); - } + this.scrollToElement('#commits'); + }, }); } @@ -222,7 +224,7 @@ url.href = source; this.ajaxGet({ - url: `${url.pathname}.json${this._location.search}`, + url: `${url.pathname}.json${location.search}`, success: (data) => { $('#diffs').html(data.html); @@ -233,14 +235,14 @@ gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); $('#diffs .js-syntax-highlight').syntaxHighlight(); - if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction) ) { + if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) { this.expandViewContainer(); } this.diffsLoaded = true; - this.scrollToElement("#diffs"); + this.scrollToElement('#diffs'); new gl.Diff(); - } + }, }); } @@ -251,12 +253,12 @@ this.ajaxGet({ url: `${source}.json`, success: (data) => { - document.querySelector("div#builds").innerHTML = data.html; + document.querySelector('div#builds').innerHTML = data.html; gl.utils.localTimeAgo($('.js-timeago', 'div#builds')); this.buildsLoaded = true; new gl.Pipelines(); - this.scrollToElement("#builds"); - } + this.scrollToElement('#builds'); + }, }); } @@ -270,8 +272,8 @@ $('#pipelines').html(data.html); gl.utils.localTimeAgo($('.js-timeago', '#pipelines')); this.pipelinesLoaded = true; - this.scrollToElement("#pipelines"); - } + this.scrollToElement('#pipelines'); + }, }); } @@ -287,10 +289,9 @@ beforeSend: () => this.toggleLoading(true), complete: () => this.toggleLoading(false), dataType: 'json', - type: 'GET' + type: 'GET', }; - options = $.extend({}, defaults, options); - $.ajax(options); + $.ajax($.extend({}, defaults, options)); } diffViewType() { @@ -298,7 +299,7 @@ } isDiffAction(action) { - return action === 'diffs' || action === 'new/diffs' + return action === 'diffs' || action === 'new/diffs'; } expandViewContainer() { @@ -356,11 +357,13 @@ const $layoutNav = $('.layout-nav'); $tabs.off('affix.bs.affix affix-top.bs.affix') - .affix({ offset: { - top: () => ( - $diffTabs.offset().top - $tabs.height() - $fixedNav.height() - $layoutNav.height() - ) - }}) + .affix({ + offset: { + top: () => ( + $diffTabs.offset().top - $tabs.height() - $fixedNav.height() - $layoutNav.height() + ), + }, + }) .on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() })) .on('affix-top.bs.affix', () => $diffTabs.css({ marginTop: '' })); @@ -372,5 +375,4 @@ } global.MergeRequestTabs = MergeRequestTabs; - })(window.gl || (window.gl = {})); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 6435e5a511c..65e4177ecfe 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -1,108 +1,111 @@ -/* eslint-disable space-before-function-paren, no-var, comma-dangle, dot-notation, quotes, no-undef, no-return-assign, no-underscore-dangle, camelcase, padded-blocks, max-len */ +/* eslint-disable no-var, comma-dangle, object-shorthand */ /*= require merge_request_tabs */ //= require breakpoints -(function() { - describe('MergeRequestTabs', function() { - var stubLocation; - stubLocation = function(stubs) { - var defaults; - defaults = { +(function () { + describe('MergeRequestTabs', function () { + var stubLocation = {}; + var setLocation = function (stubs) { + var defaults = { pathname: '', search: '', hash: '' }; - return $.extend(defaults, stubs); + $.extend(stubLocation, defaults, stubs || {}); }; fixture.preload('merge_request_tabs.html'); - beforeEach(function() { - this["class"] = new gl.MergeRequestTabs(); - return this.spies = { - ajax: spyOn($, 'ajax').and.callFake(function() {}), - history: spyOn(history, 'replaceState').and.callFake(function() {}) + + beforeEach(function () { + this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation }); + setLocation(); + + this.spies = { + ajax: spyOn($, 'ajax').and.callFake(function () {}), + history: spyOn(window.history, 'replaceState').and.callFake(function () {}) }; }); - describe('#activateTab', function() { - beforeEach(function() { + + describe('#activateTab', function () { + beforeEach(function () { fixture.load('merge_request_tabs.html'); - return this.subject = this["class"].activateTab; + this.subject = this.class.activateTab; }); - it('shows the first tab when action is show', function() { + it('shows the first tab when action is show', function () { this.subject('show'); - return expect($('#notes')).toHaveClass('active'); + expect($('#notes')).toHaveClass('active'); }); - it('shows the notes tab when action is notes', function() { + it('shows the notes tab when action is notes', function () { this.subject('notes'); - return expect($('#notes')).toHaveClass('active'); + expect($('#notes')).toHaveClass('active'); }); - it('shows the commits tab when action is commits', function() { + it('shows the commits tab when action is commits', function () { this.subject('commits'); - return expect($('#commits')).toHaveClass('active'); + expect($('#commits')).toHaveClass('active'); }); - return it('shows the diffs tab when action is diffs', function() { + it('shows the diffs tab when action is diffs', function () { this.subject('diffs'); - return expect($('#diffs')).toHaveClass('active'); + expect($('#diffs')).toHaveClass('active'); }); }); - return describe('#setCurrentAction', function() { - beforeEach(function() { - return this.subject = this["class"].setCurrentAction; + + describe('#setCurrentAction', function () { + beforeEach(function () { + this.subject = this.class.setCurrentAction; }); - it('changes from commits', function() { - this["class"]._location = stubLocation({ + it('changes from commits', function () { + setLocation({ pathname: '/foo/bar/merge_requests/1/commits' }); expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1'); - return expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs'); + expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs'); }); - it('changes from diffs', function() { - this["class"]._location = stubLocation({ + it('changes from diffs', function () { + setLocation({ pathname: '/foo/bar/merge_requests/1/diffs' }); expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1'); - return expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); + expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); }); - it('changes from diffs.html', function() { - this["class"]._location = stubLocation({ + it('changes from diffs.html', function () { + setLocation({ pathname: '/foo/bar/merge_requests/1/diffs.html' }); expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1'); - return expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); + expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); }); - it('changes from notes', function() { - this["class"]._location = stubLocation({ + it('changes from notes', function () { + setLocation({ pathname: '/foo/bar/merge_requests/1' }); expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs'); - return expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); + expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); }); - it('includes search parameters and hash string', function() { - this["class"]._location = stubLocation({ + it('includes search parameters and hash string', function () { + setLocation({ pathname: '/foo/bar/merge_requests/1/diffs', search: '?view=parallel', hash: '#L15-35' }); - return expect(this.subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35'); + expect(this.subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35'); }); - it('replaces the current history state', function() { - var new_state; - this["class"]._location = stubLocation({ + it('replaces the current history state', function () { + var newState; + setLocation({ pathname: '/foo/bar/merge_requests/1' }); - new_state = this.subject('commits'); - return expect(this.spies.history).toHaveBeenCalledWith({ + newState = this.subject('commits'); + expect(this.spies.history).toHaveBeenCalledWith({ turbolinks: true, - url: new_state - }, document.title, new_state); + url: newState + }, document.title, newState); }); - return it('treats "show" like "notes"', function() { - this["class"]._location = stubLocation({ + it('treats "show" like "notes"', function () { + setLocation({ pathname: '/foo/bar/merge_requests/1/commits' }); - return expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); + expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); }); }); }); - }).call(this); -- cgit v1.2.1 From c434568f3759dfcdb8f9c55a11aa037a760ec7d2 Mon Sep 17 00:00:00 2001 From: Luke Bennett <lukeeeebennettplus@gmail.com> Date: Fri, 9 Sep 2016 16:47:43 +0100 Subject: re-apply MR !6285 "Added forceLoad ability to singleFileDiffs, added callback to getContentHTML, added conditional force load if a collapsed diff line anchor is found" Use url utility to retrieve hash --- app/assets/javascripts/diff.js.es6 | 11 +++++++++++ app/assets/javascripts/merge_request_tabs.js.es6 | 9 +++++++-- app/assets/javascripts/single_file_diff.js | 22 ++++++++++++++-------- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6 index eacf53d1e1b..239dd84d8a3 100644 --- a/app/assets/javascripts/diff.js.es6 +++ b/app/assets/javascripts/diff.js.es6 @@ -58,6 +58,17 @@ $.get(link, params, response => $target.parent().replaceWith(response)); } + openAnchoredDiff(anchoredDiff, cb) { + const diffTitle = $(`#file-path-${anchoredDiff}`); + const diffFile = diffTitle.closest('.diff-file'); + const nothingHereBlock = $('.nothing-here-block:visible', diffFile); + if (nothingHereBlock.length) { + diffFile.singleFileDiff(true, cb); + } else { + cb(); + } + } + handleClickLineNum(event) { const hash = $(event.currentTarget).attr('href'); event.preventDefault(); diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index d3248a4757e..1a3df1bed36 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -239,9 +239,14 @@ this.expandViewContainer(); } this.diffsLoaded = true; - this.scrollToElement('#diffs'); - new gl.Diff(); + const diffPage = new gl.Diff(); + + const locationHash = gl.utils.getLocationHash(); + const anchoredDiff = locationHash && locationHash.split('_')[0]; + if (anchoredDiff) { + diffPage.openAnchoredDiff(anchoredDiff, () => this.scrollToElement('#diffs')); + } }, }); } diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index f5e06e6c817..0d48e69cce9 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -13,7 +13,8 @@ COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>'; - function SingleFileDiff(file) { + function SingleFileDiff(file, forceLoad, cb) { + var clickTarget; this.file = file; this.toggleDiff = bind(this.toggleDiff, this); this.content = $('.diff-content', this.file); @@ -31,10 +32,13 @@ this.content.after(this.collapsedContent); this.$toggleIcon.addClass('fa-caret-down'); } - $('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff); + clickTarget = $('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff); + if (forceLoad) { + this.toggleDiff({ target: clickTarget }, cb); + } } - SingleFileDiff.prototype.toggleDiff = function(e) { + SingleFileDiff.prototype.toggleDiff = function(e, cb) { var $target = $(e.target); if (!$target.hasClass('file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return; this.isOpen = !this.isOpen; @@ -54,11 +58,11 @@ } } else { this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right'); - return this.getContentHTML(); + return this.getContentHTML(cb); } }; - SingleFileDiff.prototype.getContentHTML = function() { + SingleFileDiff.prototype.getContentHTML = function(cb) { this.collapsedContent.hide(); this.loadingContent.show(); $.get(this.diffForPath, (function(_this) { @@ -76,6 +80,8 @@ if (typeof gl.diffNotesCompileComponents !== 'undefined') { gl.diffNotesCompileComponents(); } + + if (cb) cb(); }; })(this)); }; @@ -84,10 +90,10 @@ })(); - $.fn.singleFileDiff = function() { + $.fn.singleFileDiff = function(forceLoad, cb) { return this.each(function() { - if (!$.data(this, 'singleFileDiff')) { - return $.data(this, 'singleFileDiff', new SingleFileDiff(this)); + if (!$.data(this, 'singleFileDiff') || forceLoad) { + return $.data(this, 'singleFileDiff', new SingleFileDiff(this, forceLoad, cb)); } }); }; -- cgit v1.2.1 From 54a794f2dfe88cdaa0dcc5c43f17fbc966082035 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 10 Nov 2016 17:24:57 -0600 Subject: adjust scrollToElement to account for fixed merge request tabs --- app/assets/javascripts/merge_request_tabs.js.es6 | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index 1a3df1bed36..10a2f8091b8 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -135,12 +135,14 @@ scrollToElement(container) { if (location.hash) { - const navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + document.querySelector('.js-tabs-affix').offsetHeight; + const offset = 0 - ( + $('.navbar-gitlab').outerHeight() + + $('.layout-nav').outerHeight() + + $('.js-tabs-affix').outerHeight() + ); const $el = $(`${container} ${location.hash}:not(.match)`); - if ($el.length) { - $.scrollTo($el[0], { - offset: -navBarHeight, - }); + if ($el.length > 0) { + $.scrollTo($el[0], { offset }); } } } -- cgit v1.2.1 From 6dd2a5204e9c17410c627737c7fed14642ed4e76 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 1 Dec 2016 11:35:26 -0600 Subject: use e instead of event variable name --- app/assets/javascripts/diff.js.es6 | 10 +++++----- app/assets/javascripts/merge_request_tabs.js.es6 | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6 index 239dd84d8a3..4730d4b4e7b 100644 --- a/app/assets/javascripts/diff.js.es6 +++ b/app/assets/javascripts/diff.js.es6 @@ -20,8 +20,8 @@ this.highlighSelectedLine(); } - handleClickUnfold(event) { - const $target = $(event.target); + handleClickUnfold(e) { + const $target = $(e.target); // current babel config relies on iterators implementation, so we cannot simply do: // const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent()); const ref = this.lineNumbers($target.parent()); @@ -69,9 +69,9 @@ } } - handleClickLineNum(event) { - const hash = $(event.currentTarget).attr('href'); - event.preventDefault(); + handleClickLineNum(e) { + const hash = $(e.currentTarget).attr('href'); + e.preventDefault(); if (window.history.pushState) { window.history.pushState(null, null, hash); } else { diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index 10a2f8091b8..0e69785a3d9 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -91,13 +91,13 @@ .off('click', '.js-show-tab', this.showTab); } - showTab(event) { - event.preventDefault(); - this.activateTab($(event.target).data('action')); + showTab(e) { + e.preventDefault(); + this.activateTab($(e.target).data('action')); } - tabShown(event) { - const $target = $(event.target); + tabShown(e) { + const $target = $(e.target); const action = $target.data('action'); if (action === 'commits') { -- cgit v1.2.1 From c1ea41e2f564f3113b3dfcd2e654531d6b475a7f Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 1 Dec 2016 12:19:22 -0600 Subject: use shorthand instead of length > 0 (http://airbnb.io/javascript/#comparison--shortcuts) --- app/assets/javascripts/merge_request_tabs.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index 0e69785a3d9..73828c8ab0e 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -141,7 +141,7 @@ $('.js-tabs-affix').outerHeight() ); const $el = $(`${container} ${location.hash}:not(.match)`); - if ($el.length > 0) { + if ($el.length) { $.scrollTo($el[0], { offset }); } } -- cgit v1.2.1 From 8732ac4084c1f6a2a5b4e491e32424f738f7a01a Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 1 Dec 2016 13:09:45 -0600 Subject: display error when a tab cannot be loaded --- app/assets/javascripts/merge_request_tabs.js.es6 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index 73828c8ab0e..35e869d2e39 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -1,5 +1,8 @@ /* eslint-disable no-new, no-param-reassign, class-methods-use-this */ -/* global Breakpoints, Cookies, DiffNotesApp */ +/* global Breakpoints */ +/* global Cookies */ +/* global DiffNotesApp */ +/* global Flash */ /*= require js.cookie */ /*= require breakpoints */ @@ -294,6 +297,7 @@ ajaxGet(options) { const defaults = { beforeSend: () => this.toggleLoading(true), + error: () => new Flash('An error occurred while fetching this tab.', 'alert'), complete: () => this.toggleLoading(false), dataType: 'json', type: 'GET', -- cgit v1.2.1 From ed5f22cb93cbabcd2bcf38e7a6b22efb03d7e212 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Fri, 2 Dec 2016 17:43:20 -0600 Subject: satisfy eslint no-param-reassign rule --- app/assets/javascripts/diff.js.es6 | 9 +++++---- app/assets/javascripts/merge_request_tabs.js.es6 | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6 index 4730d4b4e7b..ecf9d1de81c 100644 --- a/app/assets/javascripts/diff.js.es6 +++ b/app/assets/javascripts/diff.js.es6 @@ -1,6 +1,6 @@ -/* eslint-disable class-methods-use-this, no-param-reassign */ +/* eslint-disable class-methods-use-this */ -((global) => { +(() => { const UNFOLD_COUNT = 20; class Diff { @@ -104,5 +104,6 @@ } } - global.Diff = Diff; -})(window.gl || (window.gl = {})); + window.gl = window.gl || {}; + window.gl.Diff = Diff; +})(); diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index 35e869d2e39..583fb9fc03d 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable no-new, no-param-reassign, class-methods-use-this */ +/* eslint-disable no-new, class-methods-use-this */ /* global Breakpoints */ /* global Cookies */ /* global DiffNotesApp */ @@ -53,7 +53,7 @@ // /* eslint-enable max-len */ -((global) => { +(() => { // Store the `location` object, allowing for easier stubbing in tests let location = window.location; @@ -385,5 +385,6 @@ } } - global.MergeRequestTabs = MergeRequestTabs; -})(window.gl || (window.gl = {})); + window.gl = window.gl || {}; + window.gl.MergeRequestTabs = MergeRequestTabs; +})(); -- cgit v1.2.1 From 04c2bce07cb20a1b24f8befd94973e4fba953f5c Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Sat, 3 Dec 2016 11:32:38 +0100 Subject: Link to old patch update guide now that we install workhorse with a raketask [ci skip] --- doc/update/patch_versions.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index e98c40ca4c0..2f0559dbcc5 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -44,6 +44,10 @@ sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`ca ### 4. Update gitlab-workhorse to the corresponding version +>**Note:** +This new way to install GitLab workhorse was introduced with GitLab 8.15. If you +are using an older version follow [this guide instead][oldguide]. + ```bash cd /home/git/gitlab @@ -93,3 +97,5 @@ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production ``` If all items are green, then congratulations upgrade complete! + +[oldguide]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-14-stable/doc/update/patch_versions.md -- cgit v1.2.1 From 1e67ea815fee9f70f1a72c6b8f1e98bdb8bb2710 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Sat, 3 Dec 2016 12:54:51 +0000 Subject: Revert "Merge branch 'docs/patch-guide-link-to-old-guide' into 'master'" This reverts merge request !7909 --- doc/update/patch_versions.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index 2f0559dbcc5..e98c40ca4c0 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -44,10 +44,6 @@ sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`ca ### 4. Update gitlab-workhorse to the corresponding version ->**Note:** -This new way to install GitLab workhorse was introduced with GitLab 8.15. If you -are using an older version follow [this guide instead][oldguide]. - ```bash cd /home/git/gitlab @@ -97,5 +93,3 @@ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production ``` If all items are green, then congratulations upgrade complete! - -[oldguide]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-14-stable/doc/update/patch_versions.md -- cgit v1.2.1 From 785a80132c2716446cfde57a196d495e1e97a4c0 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Fri, 2 Dec 2016 20:01:19 +0100 Subject: Bump ruby version --- .gitlab-ci.yml | 8 +++----- .ruby-version | 2 +- changelogs/unreleased/zj-use-ruby-2-3-3.yml | 4 ++++ doc/install/installation.md | 6 +++--- 4 files changed, 11 insertions(+), 9 deletions(-) create mode 100644 changelogs/unreleased/zj-use-ruby-2-3-3.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c7528029c89..8de7ca897ad 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,7 @@ -image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3-git-2.7-phantomjs-2.1" +image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-git-2.7-phantomjs-2.1-node-7.1" cache: - key: "ruby-231" + key: "ruby-233" paths: - vendor/ruby @@ -235,7 +235,7 @@ rake ee_compat_check: - /^[\d-]+-stable(-ee)?$/ allow_failure: yes cache: - key: "ruby231-ee_compat_check_repo" + key: "ruby233-ee_compat_check_repo" paths: - ee_compat_check/repo/ - vendor/ruby @@ -277,8 +277,6 @@ teaspoon: stage: test <<: *use-db script: - - curl --silent --location https://deb.nodesource.com/setup_6.x | bash - - - apt-get install --assume-yes nodejs - npm install - npm link istanbul - rake teaspoon diff --git a/.ruby-version b/.ruby-version index 2bf1c1ccf36..0bee604df76 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.3.1 +2.3.3 diff --git a/changelogs/unreleased/zj-use-ruby-2-3-3.yml b/changelogs/unreleased/zj-use-ruby-2-3-3.yml new file mode 100644 index 00000000000..0d1a0fcd79d --- /dev/null +++ b/changelogs/unreleased/zj-use-ruby-2-3-3.yml @@ -0,0 +1,4 @@ +--- +title: Bump ruby version to 2.3.3 +merge_request: 7904 +author: diff --git a/doc/install/installation.md b/doc/install/installation.md index 4b0c585e51e..5099d639347 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -123,9 +123,9 @@ Remove the old Ruby 1.8 if present: Download Ruby and compile it: mkdir /tmp/ruby && cd /tmp/ruby - curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.1.tar.gz - echo 'c39b4001f7acb4e334cb60a0f4df72d434bef711 ruby-2.3.1.tar.gz' | shasum -c - && tar xzf ruby-2.3.1.tar.gz - cd ruby-2.3.1 + curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz + echo 'a8db9ce7f9110320f33b8325200e3ecfbd2b534b ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz + cd ruby-2.3.3 ./configure --disable-install-rdoc make sudo make install -- cgit v1.2.1 From e4afac13b707e621b241827544de5d11161dddc2 Mon Sep 17 00:00:00 2001 From: Rydkin Maxim <maks.rydkin@gmail.com> Date: Sat, 3 Dec 2016 23:56:17 +0300 Subject: fix gfm doc typo about two spaces for next line transfer --- doc/user/markdown.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 162d1bd7ed4..85b165ac44f 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -604,7 +604,7 @@ This line is separated from the one above by two newlines, so it will be a *sepa This line is also a separate paragraph, but... This line is only separated by a single newline, so it's a separate line in the *same paragraph*. -This line is also a separate paragraph, and... +This line is also a separate paragraph, and... This line is on its own line, because the previous line ends with two spaces. ``` @@ -616,7 +616,7 @@ This line is separated from the one above by two newlines, so it will be a *sepa This line is also begins a separate paragraph, but... This line is only separated by a single newline, so it's a separate line in the *same paragraph*. -This line is also a separate paragraph, and... +This line is also a separate paragraph, and... This line is on its own line, because the previous line ends with two spaces. -- cgit v1.2.1 From 48895aa40dd2bfc13453b0b28c6d122f4e1c58be Mon Sep 17 00:00:00 2001 From: BM5k <me@bm5k.com> Date: Wed, 30 Nov 2016 14:28:42 -0700 Subject: change the date label to match the date used --- app/views/projects/blame/show.html.haml | 2 +- app/views/projects/commits/_commit.html.haml | 2 +- changelogs/unreleased/issues-1608-text.yml | 4 ++++ spec/features/commits_spec.rb | 19 +++++++++++++++++++ 4 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/issues-1608-text.yml diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index cadfe5a3e30..f63802ac88b 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -31,7 +31,7 @@   .light = commit_author_link(commit, avatar: false) - authored + committed #{time_ago_with_tooltip(commit.committed_date)} %td.line-numbers - line_count = blame_group[:lines].count diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 12096941209..a940515fadf 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -37,5 +37,5 @@ = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author)) = commit_author_link(commit, avatar: false, size: 24) - authored + committed #{time_ago_with_tooltip(commit.committed_date)} diff --git a/changelogs/unreleased/issues-1608-text.yml b/changelogs/unreleased/issues-1608-text.yml new file mode 100644 index 00000000000..bef427a1e1e --- /dev/null +++ b/changelogs/unreleased/issues-1608-text.yml @@ -0,0 +1,4 @@ +--- +title: change text around timestamps to make it clear which timestamp is displayed +merge_request: 7860 +author: BM5k diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 44646ffc602..23a504ff965 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -177,4 +177,23 @@ describe 'Commits' do end end end + + context 'viewing commits for a branch' do + let(:branch_name) { 'master' } + let(:user) { create(:user) } + + before do + project.team << [user, :master] + login_with(user) + visit namespace_project_commits_path(project.namespace, project, branch_name) + end + + it 'includes the committed_date for each commit' do + commits = project.repository.commits(branch_name) + + commits.each do |commit| + expect(page).to have_content("committed #{commit.committed_date}") + end + end + end end -- cgit v1.2.1 From 0966c6d2c674f288be3f1adf576a2fa2aada3691 Mon Sep 17 00:00:00 2001 From: Steffen Rauh <steffen.rauh@gmx.de> Date: Thu, 17 Nov 2016 13:05:32 +0100 Subject: Fix compatibility with Internet Explorer 11 for merge requests --- app/assets/javascripts/lib/utils/common_utils.js | 13 +++++++++ app/assets/javascripts/merge_request_tabs.js.es6 | 5 ++-- ...-compatibility-with-ie11-for-merge-requests.yml | 4 +++ .../javascripts/lib/utils/common_utils_spec.js.es6 | 32 ++++++++++++++++++++++ spec/javascripts/merge_request_tabs_spec.js | 13 ++++++++- 5 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/fix-compatibility-with-ie11-for-merge-requests.yml create mode 100644 spec/javascripts/lib/utils/common_utils_spec.js.es6 diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 29cba1a49dd..8fa80502d92 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -97,6 +97,19 @@ return $('body').data('page').split(':')[0]; }; + gl.utils.parseUrl = function (url) { + var parser = document.createElement('a'); + parser.href = url; + return parser; + }; + + gl.utils.parseUrlPathname = function (url) { + var parsedUrl = gl.utils.parseUrl(url); + // parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11 + // We have to make sure we always have an absolute path. + return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname; + }; + gl.utils.isMetaKey = function(e) { return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; }; diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index 583fb9fc03d..771803edb7c 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -225,11 +225,10 @@ // We extract pathname for the current Changes tab anchor href // some pages like MergeRequestsController#new has query parameters on that anchor - const url = document.createElement('a'); - url.href = source; + var urlPathname = gl.utils.parseUrlPathname(source); this.ajaxGet({ - url: `${url.pathname}.json${location.search}`, + url: `${urlPathname}.json${location.search}`, success: (data) => { $('#diffs').html(data.html); diff --git a/changelogs/unreleased/fix-compatibility-with-ie11-for-merge-requests.yml b/changelogs/unreleased/fix-compatibility-with-ie11-for-merge-requests.yml new file mode 100644 index 00000000000..db92e45d8f1 --- /dev/null +++ b/changelogs/unreleased/fix-compatibility-with-ie11-for-merge-requests.yml @@ -0,0 +1,4 @@ +--- +title: Fix compatibility with Internet Explorer 11 for merge requests +merge_request: 7525 +author: Steffen Rauh diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 new file mode 100644 index 00000000000..ef75f600898 --- /dev/null +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -0,0 +1,32 @@ +//= require lib/utils/common_utils + +(() => { + describe('common_utils', () => { + describe('gl.utils.parseUrl', () => { + it('returns an anchor tag with url', () => { + expect(gl.utils.parseUrl('/some/absolute/url').pathname).toContain('some/absolute/url'); + }); + it('url is escaped', () => { + // IE11 will return a relative pathname while other browsers will return a full pathname. + // parseUrl uses an anchor element for parsing an url. With relative urls, the anchor + // element will create an absolute url relative to the current execution context. + // The JavaScript test suite is executed at '/teaspoon' which will lead to an absolute + // url starting with '/teaspoon'. + expect(gl.utils.parseUrl('" test="asf"').pathname).toEqual('/teaspoon/%22%20test=%22asf%22'); + }); + }); + describe('gl.utils.parseUrlPathname', () => { + beforeEach(() => { + spyOn(gl.utils, 'parseUrl').and.callFake(url => ({ + pathname: url, + })); + }); + it('returns an absolute url when given an absolute url', () => { + expect(gl.utils.parseUrlPathname('/some/absolute/url')).toEqual('/some/absolute/url'); + }); + it('returns an absolute url when given a relative url', () => { + expect(gl.utils.parseUrlPathname('some/relative/url')).toEqual('/some/relative/url'); + }); + }); + }); +})(); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 65e4177ecfe..4facc42c5b4 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -2,6 +2,8 @@ /*= require merge_request_tabs */ //= require breakpoints +//= require lib/utils/common_utils +//= require jquery.scrollTo (function () { describe('MergeRequestTabs', function () { @@ -21,13 +23,13 @@ setLocation(); this.spies = { - ajax: spyOn($, 'ajax').and.callFake(function () {}), history: spyOn(window.history, 'replaceState').and.callFake(function () {}) }; }); describe('#activateTab', function () { beforeEach(function () { + spyOn($, 'ajax').and.callFake(function() {}); fixture.load('merge_request_tabs.html'); this.subject = this.class.activateTab; }); @@ -51,6 +53,7 @@ describe('#setCurrentAction', function () { beforeEach(function () { + spyOn($, 'ajax').and.callFake(function() {}); this.subject = this.class.setCurrentAction; }); it('changes from commits', function () { @@ -107,5 +110,13 @@ expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); }); }); + describe('#loadDiff', function() { + it('requires an absolute pathname', function() { + spyOn($, 'ajax').and.callFake(function(options) { + expect(options.url).toEqual('/foo/bar/merge_requests/1/diffs.json'); + }); + this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + }); + }); }); }).call(this); -- cgit v1.2.1 From 0d53e97ee80be012d4b486092098333a0185c69b Mon Sep 17 00:00:00 2001 From: Steffen Rauh <steffen.rauh@gmx.de> Date: Sat, 3 Dec 2016 23:04:21 +0100 Subject: Satisfied eslint --- app/assets/javascripts/merge_request_tabs.js.es6 | 2 +- spec/javascripts/merge_request_tabs_spec.js | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index 771803edb7c..3ec0f1fd613 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -225,7 +225,7 @@ // We extract pathname for the current Changes tab anchor href // some pages like MergeRequestsController#new has query parameters on that anchor - var urlPathname = gl.utils.parseUrlPathname(source); + const urlPathname = gl.utils.parseUrlPathname(source); this.ajaxGet({ url: `${urlPathname}.json${location.search}`, diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 4facc42c5b4..130d391bfab 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -29,7 +29,7 @@ describe('#activateTab', function () { beforeEach(function () { - spyOn($, 'ajax').and.callFake(function() {}); + spyOn($, 'ajax').and.callFake(function () {}); fixture.load('merge_request_tabs.html'); this.subject = this.class.activateTab; }); @@ -53,7 +53,7 @@ describe('#setCurrentAction', function () { beforeEach(function () { - spyOn($, 'ajax').and.callFake(function() {}); + spyOn($, 'ajax').and.callFake(function () {}); this.subject = this.class.setCurrentAction; }); it('changes from commits', function () { @@ -110,9 +110,9 @@ expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); }); }); - describe('#loadDiff', function() { - it('requires an absolute pathname', function() { - spyOn($, 'ajax').and.callFake(function(options) { + describe('#loadDiff', function () { + it('requires an absolute pathname', function () { + spyOn($, 'ajax').and.callFake(function (options) { expect(options.url).toEqual('/foo/bar/merge_requests/1/diffs.json'); }); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); -- cgit v1.2.1 From e5f6c786981da7cf1ba7261b964bdb943bd5e97f Mon Sep 17 00:00:00 2001 From: Ryan Harris <harrisryan1@gmail.com> Date: Sat, 3 Dec 2016 19:08:40 -0500 Subject: Fixed top margin for Builds page status header information --- app/assets/stylesheets/pages/builds.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 842c0434bf2..dcc13f6d74a 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -64,6 +64,7 @@ @media (max-width: $screen-sm-max) { padding-right: 40px; + margin-top: 6px; .btn-inverted { display: none; -- cgit v1.2.1 From a45f7701c90f1f858de1267492cfa4a28dc698a7 Mon Sep 17 00:00:00 2001 From: Ryan Harris <harrisryan1@gmail.com> Date: Sat, 3 Dec 2016 19:21:04 -0500 Subject: Added changelog for #25221 --- changelogs/unreleased/25221-fix-build-status-overflow-mobile.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/25221-fix-build-status-overflow-mobile.yml diff --git a/changelogs/unreleased/25221-fix-build-status-overflow-mobile.yml b/changelogs/unreleased/25221-fix-build-status-overflow-mobile.yml new file mode 100644 index 00000000000..52de34478f0 --- /dev/null +++ b/changelogs/unreleased/25221-fix-build-status-overflow-mobile.yml @@ -0,0 +1,4 @@ +--- +title: Added top margin to Build status page header for mobile views +merge_request: +author: Ryan Harris -- cgit v1.2.1 From c3415873b969ab3a8708da1f35388aa4f2e4155a Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" <lbennett@gitlab.com> Date: Sun, 4 Dec 2016 11:12:23 +0000 Subject: Instantiate ImageFile for diff-files in Diff --- app/assets/javascripts/commit/file.js | 2 +- app/assets/javascripts/commit/image_file.js | 2 +- app/assets/javascripts/diff.js.es6 | 7 +++++-- ...on-skin-controls-for-merge-request-diff-containing-an-image.yml | 4 ++++ 4 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js index 3f29826fa9b..600bac8834a 100644 --- a/app/assets/javascripts/commit/file.js +++ b/app/assets/javascripts/commit/file.js @@ -3,7 +3,7 @@ this.CommitFile = (function() { function CommitFile(file) { if ($('.image', file).length) { - new ImageFile(file); + new gl.ImageFile(file); } } diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index 4c2ae595319..fd8910e916f 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -1,6 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, padded-blocks, max-len */ (function() { - this.ImageFile = (function() { + gl.ImageFile = (function() { var prepareFrames; // Width where images must fits in, for 2-up this gets divided by 2 diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6 index ecf9d1de81c..9cf33e62958 100644 --- a/app/assets/javascripts/diff.js.es6 +++ b/app/assets/javascripts/diff.js.es6 @@ -5,8 +5,11 @@ class Diff { constructor() { - $('.files .diff-file').singleFileDiff(); - $('.files .diff-file').filesCommentButton(); + const $diffFile = $('.files .diff-file'); + $diffFile.singleFileDiff(); + $diffFile.filesCommentButton(); + + $diffFile.each((index, file) => new gl.ImageFile(file)); if (this.diffViewType() === 'parallel') { $('.content-wrapper .container-fluid').removeClass('container-limited'); diff --git a/changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml b/changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml new file mode 100644 index 00000000000..b8ba9391530 --- /dev/null +++ b/changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml @@ -0,0 +1,4 @@ +--- +title: Add image controls to MR diffs +merge_request: 7919 +author: -- cgit v1.2.1 From 617f43c74b967a085f6cd7afb1408cfa28187b52 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <zegerjan@gitlab.com> Date: Thu, 13 Oct 2016 09:38:03 +0200 Subject: Guests can read builds if those are public Fixes #18448 --- app/policies/ci/build_policy.rb | 2 + app/policies/project_policy.rb | 5 +++ .../unreleased/zj-guest-reads-public-builds.yml | 4 ++ .../projects/guest_navigation_menu_spec.rb | 4 +- .../security/project/private_access_spec.rb | 52 ++++++++++++++++++++++ spec/policies/project_policy_spec.rb | 36 ++++++++++++--- spec/requests/api/builds_spec.rb | 2 +- 7 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 changelogs/unreleased/zj-guest-reads-public-builds.yml diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index 8b25332b73c..7b1752df0e1 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -1,6 +1,8 @@ module Ci class BuildPolicy < CommitStatusPolicy def rules + can! :read_build if @subject.project.public_builds? + super # If we can't read build we should also not have that diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 8ac4bd9df6d..b4c1fcabefd 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -46,6 +46,11 @@ class ProjectPolicy < BasePolicy can! :create_note can! :upload_file can! :read_cycle_analytics + + if project.public_builds? + can! :read_pipeline + can! :read_build + end end def reporter_access! diff --git a/changelogs/unreleased/zj-guest-reads-public-builds.yml b/changelogs/unreleased/zj-guest-reads-public-builds.yml new file mode 100644 index 00000000000..1859addd606 --- /dev/null +++ b/changelogs/unreleased/zj-guest-reads-public-builds.yml @@ -0,0 +1,4 @@ +--- +title: Guests can read builds when public +merge_request: 6842 +author: diff --git a/spec/features/projects/guest_navigation_menu_spec.rb b/spec/features/projects/guest_navigation_menu_spec.rb index c22441f8929..8120a51c515 100644 --- a/spec/features/projects/guest_navigation_menu_spec.rb +++ b/spec/features/projects/guest_navigation_menu_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' describe "Guest navigation menu" do - let(:project) { create :empty_project, :private } - let(:guest) { create :user } + let(:project) { create(:empty_project, :private, public_builds: false) } + let(:guest) { create(:user) } before do project.team << [guest, :guest] diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index 290ddb4c6dd..a942a1ace3b 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -260,6 +260,19 @@ describe "Private Project Access", feature: true do it { is_expected.to be_denied_for(:user) } it { is_expected.to be_denied_for(:external) } it { is_expected.to be_denied_for(:visitor) } + + context 'when public builds is enabled' do + it { is_expected.to be_allowed_for guest } + end + + context 'when public buils are disabled' do + before do + project.public_builds = false + project.save + end + + it { is_expected.to be_denied_for guest } + end end describe "GET /:project_path/pipelines/:id" do @@ -275,6 +288,19 @@ describe "Private Project Access", feature: true do it { is_expected.to be_denied_for(:user) } it { is_expected.to be_denied_for(:external) } it { is_expected.to be_denied_for(:visitor) } + + context 'when public builds is enabled' do + it { is_expected.to be_allowed_for guest } + end + + context 'when public buils are disabled' do + before do + project.public_builds = false + project.save + end + + it { is_expected.to be_denied_for guest } + end end describe "GET /:project_path/builds" do @@ -289,6 +315,19 @@ describe "Private Project Access", feature: true do it { is_expected.to be_denied_for(:user) } it { is_expected.to be_denied_for(:external) } it { is_expected.to be_denied_for(:visitor) } + + context 'when public builds is enabled' do + it { is_expected.to be_allowed_for guest } + end + + context 'when public buils are disabled' do + before do + project.public_builds = false + project.save + end + + it { is_expected.to be_denied_for guest } + end end describe "GET /:project_path/builds/:id" do @@ -305,6 +344,19 @@ describe "Private Project Access", feature: true do it { is_expected.to be_denied_for(:user) } it { is_expected.to be_denied_for(:external) } it { is_expected.to be_denied_for(:visitor) } + + context 'when public builds is enabled' do + it { is_expected.to be_allowed_for guest } + end + + context 'when public buils are disabled' do + before do + project.public_builds = false + project.save + end + + it { is_expected.to be_denied_for guest } + end end describe "GET /:project_path/environments" do diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index b49e4f3a8bc..34a0937d9bc 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -111,13 +111,35 @@ describe ProjectPolicy, models: true do context 'guests' do let(:current_user) { guest } - it do - is_expected.to include(*guest_permissions) - is_expected.not_to include(*reporter_permissions) - is_expected.not_to include(*team_member_reporter_permissions) - is_expected.not_to include(*developer_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + context 'public builds enabled' do + let(:reporter_public_build_permissions) do + reporter_permissions - [:read_build, :read_pipeline] + end + + it do + is_expected.to include(*guest_permissions) + is_expected.not_to include(*reporter_public_build_permissions) + is_expected.not_to include(*team_member_reporter_permissions) + is_expected.not_to include(*developer_permissions) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'public builds disabled' do + before do + project.public_builds = false + project.save + end + + it do + is_expected.to include(*guest_permissions) + is_expected.not_to include(*reporter_permissions) + is_expected.not_to include(*team_member_reporter_permissions) + is_expected.not_to include(*developer_permissions) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end end end diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 0ea991b18b8..7be7acebb19 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -5,7 +5,7 @@ describe API::Builds, api: true do let(:user) { create(:user) } let(:api_user) { user } - let!(:project) { create(:project, creator_id: user.id) } + let!(:project) { create(:project, creator_id: user.id, public_builds: false) } let!(:developer) { create(:project_member, :developer, user: user, project: project) } let(:reporter) { create(:project_member, :reporter, project: project) } let(:guest) { create(:project_member, :guest, project: project) } -- cgit v1.2.1 From 8b5c16e4b1d54745ba6ca65ecfbf14c1683db3b4 Mon Sep 17 00:00:00 2001 From: Robert Schilling <rschilling@student.tugraz.at> Date: Mon, 28 Nov 2016 21:33:12 +0100 Subject: API: Ability to remove source branch --- changelogs/unreleased/api-remove-source-branch.yml | 4 ++ doc/api/merge_requests.md | 49 ++++++++++++---------- lib/api/merge_requests.rb | 8 +++- spec/requests/api/merge_requests_spec.rb | 12 +++++- 4 files changed, 47 insertions(+), 26 deletions(-) create mode 100644 changelogs/unreleased/api-remove-source-branch.yml diff --git a/changelogs/unreleased/api-remove-source-branch.yml b/changelogs/unreleased/api-remove-source-branch.yml new file mode 100644 index 00000000000..d1b6507aedb --- /dev/null +++ b/changelogs/unreleased/api-remove-source-branch.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Ability to set ''should_remove_source_branch'' on merge requests' +merge_request: +author: Robert Schilling diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 12d24543bbe..8d9d3cd5580 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -271,17 +271,18 @@ Creates a new merge request. POST /projects/:id/merge_requests ``` -Parameters: - -- `id` (required) - The ID of a project -- `source_branch` (required) - The source branch -- `target_branch` (required) - The target branch -- `assignee_id` (optional) - Assignee user ID -- `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 -- `milestone_id` (optional) - Milestone ID +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | string | yes | The ID of a project | +| `source_branch` | string | yes | The source branch | +| `target_branch` | string | yes | The target branch | +| `title` | string | yes | Title of MR | +| `assignee_id` | integer | no | Assignee user ID | +| `description` | string | no | Description of MR | +| `target_project_id` | integer | no | The target project (numeric id) | +| `labels` | string | no | Labels for MR as a comma-separated list | +| `milestone_id` | integer | no | The ID of a milestone | +| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging | ```json { @@ -346,17 +347,19 @@ Updates an existing merge request. You can change the target branch, title, or e PUT /projects/:id/merge_requests/:merge_request_id ``` -Parameters: - -- `id` (required) - The ID of a project -- `merge_request_id` (required) - ID of MR -- `target_branch` - The target branch -- `assignee_id` - Assignee user ID -- `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 -- `milestone_id` (optional) - Milestone ID +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | string | yes | The ID of a project | +| `merge_request_id` | integer | yes | The ID of a merge request | +| `source_branch` | string | yes | The source branch | +| `target_branch` | string | yes | The target branch | +| `title` | string | yes | Title of MR | +| `assignee_id` | integer | no | Assignee user ID | +| `description` | string | no | Description of MR | +| `target_project_id` | integer | no | The target project (numeric id) | +| `labels` | string | no | Labels for MR as a comma-separated list | +| `milestone_id` | integer | no | The ID of a milestone | +| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging | ```json { @@ -807,7 +810,7 @@ Example response: ## Create a todo -Manually creates a todo for the current user on a merge request. +Manually creates a todo for the current user on a merge request. If there already exists a todo for the user on that merge request, status code `304` is returned. diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 97baebc1d27..5535c807d21 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -28,6 +28,7 @@ module API optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request' optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request' optional :labels, type: String, desc: 'Comma-separated list of label names' + optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging' end end @@ -75,7 +76,8 @@ module API post ":id/merge_requests" do authorize! :create_merge_request, user_project - mr_params = declared_params + mr_params = declared_params(include_missing: false) + mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present? merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute @@ -144,13 +146,15 @@ module API desc: 'Status of the merge request' use :optional_params at_least_one_of :title, :target_branch, :description, :assignee_id, - :milestone_id, :labels, :state_event + :milestone_id, :labels, :state_event, + :remove_source_branch end put path do merge_request = user_project.merge_requests.find(params.delete(:merge_request_id)) authorize! :update_merge_request, merge_request mr_params = declared_params(include_missing: false) + mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present? merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request) diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 918a71129f7..894896b95e4 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -234,11 +234,14 @@ describe API::MergeRequests, api: true do target_branch: 'master', author: user, labels: 'label, label2', - milestone_id: milestone.id + milestone_id: milestone.id, + remove_source_branch: true + expect(response).to have_http_status(201) expect(json_response['title']).to eq('Test merge_request') expect(json_response['labels']).to eq(['label', 'label2']) expect(json_response['milestone']['id']).to eq(milestone.id) + expect(json_response['force_remove_source_branch']).to be_truthy end it "returns 422 when source_branch equals target_branch" do @@ -511,6 +514,13 @@ describe API::MergeRequests, api: true do expect(json_response['target_branch']).to eq('wiki') end + it "returns merge_request that removes the source branch" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), remove_source_branch: true + + expect(response).to have_http_status(200) + expect(json_response['force_remove_source_branch']).to be_truthy + end + it 'allows special label names' do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: 'new issue', -- cgit v1.2.1 From 10960400245ca338e32a3c55538ace976df962c6 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Tue, 29 Nov 2016 13:43:58 +0100 Subject: Update effected tests --- app/policies/project_policy.rb | 3 -- features/steps/shared/project.rb | 2 +- .../security/project/private_access_spec.rb | 49 +++++++++++----------- .../lib/gitlab/cycle_analytics/permissions_spec.rb | 2 +- spec/policies/project_policy_spec.rb | 32 +++++++------- .../projects/cycle_analytics_events_spec.rb | 2 +- spec/workers/pipeline_notification_worker_spec.rb | 2 +- 7 files changed, 45 insertions(+), 47 deletions(-) diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index b4c1fcabefd..d5aadfce76a 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -12,9 +12,6 @@ class ProjectPolicy < BasePolicy guest_access! public_access! - # Allow to read builds for internal projects - can! :read_build if project.public_builds? - if project.request_access_enabled && !(owner || user.admin? || project.team.member?(user) || project_group_member?(user)) can! :request_access diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index cab85a48396..b51152c79c6 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -9,7 +9,7 @@ module SharedProject step "project exists in some group namespace" do @group = create(:group, name: 'some group') - @project = create(:project, namespace: @group) + @project = create(:project, namespace: @group, public_builds: false) end # Create a specific project called "Shop" diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index a942a1ace3b..f52e23f9433 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe "Private Project Access", feature: true do include AccessMatchers - let(:project) { create(:project, :private) } + let(:project) { create(:project, :private, public_builds: false) } describe "Project should be private" do describe '#private?' do @@ -262,16 +262,15 @@ describe "Private Project Access", feature: true do it { is_expected.to be_denied_for(:visitor) } context 'when public builds is enabled' do - it { is_expected.to be_allowed_for guest } - end - - context 'when public buils are disabled' do before do - project.public_builds = false - project.save + project.update(public_builds: true) end - it { is_expected.to be_denied_for guest } + it { is_expected.to be_allowed_for(:guest).of(project) } + end + + context 'when public buils are disabled' do + it { is_expected.to be_denied_for(:guest).of(project) } end end @@ -290,16 +289,15 @@ describe "Private Project Access", feature: true do it { is_expected.to be_denied_for(:visitor) } context 'when public builds is enabled' do - it { is_expected.to be_allowed_for guest } - end - - context 'when public buils are disabled' do before do - project.public_builds = false - project.save + project.update(public_builds: true) end - it { is_expected.to be_denied_for guest } + it { is_expected.to be_allowed_for(:guest).of(project) } + end + + context 'when public buils are disabled' do + it { is_expected.to be_denied_for(:guest).of(project) } end end @@ -317,16 +315,15 @@ describe "Private Project Access", feature: true do it { is_expected.to be_denied_for(:visitor) } context 'when public builds is enabled' do - it { is_expected.to be_allowed_for guest } - end - - context 'when public buils are disabled' do before do - project.public_builds = false - project.save + project.update(public_builds: true) end - it { is_expected.to be_denied_for guest } + it { is_expected.to be_allowed_for(:guest).of(project) } + end + + context 'when public buils are disabled' do + it { is_expected.to be_denied_for(:guest).of(project) } end end @@ -346,7 +343,11 @@ describe "Private Project Access", feature: true do it { is_expected.to be_denied_for(:visitor) } context 'when public builds is enabled' do - it { is_expected.to be_allowed_for guest } + before do + project.update(public_builds: true) + end + + it { is_expected.to be_allowed_for(:guest).of(project) } end context 'when public buils are disabled' do @@ -355,7 +356,7 @@ describe "Private Project Access", feature: true do project.save end - it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for(:guest).of(project) } end end diff --git a/spec/lib/gitlab/cycle_analytics/permissions_spec.rb b/spec/lib/gitlab/cycle_analytics/permissions_spec.rb index dc4f7dc69db..2d85e712db0 100644 --- a/spec/lib/gitlab/cycle_analytics/permissions_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/permissions_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::CycleAnalytics::Permissions do - let(:project) { create(:empty_project) } + let(:project) { create(:empty_project, public_builds: false) } let(:user) { create(:user) } subject { described_class.get(user: user, project: project) } diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 34a0937d9bc..eeab9827d99 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -111,34 +111,34 @@ describe ProjectPolicy, models: true do context 'guests' do let(:current_user) { guest } - context 'public builds enabled' do - let(:reporter_public_build_permissions) do - reporter_permissions - [:read_build, :read_pipeline] - end + let(:reporter_public_build_permissions) do + reporter_permissions - [:read_build, :read_pipeline] + end + + it do + is_expected.to include(*guest_permissions) + is_expected.not_to include(*reporter_public_build_permissions) + is_expected.not_to include(*team_member_reporter_permissions) + is_expected.not_to include(*developer_permissions) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + context 'public builds enabled' do it do is_expected.to include(*guest_permissions) - is_expected.not_to include(*reporter_public_build_permissions) - is_expected.not_to include(*team_member_reporter_permissions) - is_expected.not_to include(*developer_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + is_expected.to include(:read_build, :read_pipeline) end end context 'public builds disabled' do before do - project.public_builds = false - project.save + project.update(public_builds: false) end it do is_expected.to include(*guest_permissions) - is_expected.not_to include(*reporter_permissions) - is_expected.not_to include(*team_member_reporter_permissions) - is_expected.not_to include(*developer_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + is_expected.not_to include(:read_build, :read_pipeline) end end end diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb index f5e0fdcda2d..e0368e6001f 100644 --- a/spec/requests/projects/cycle_analytics_events_spec.rb +++ b/spec/requests/projects/cycle_analytics_events_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe 'cycle analytics events' do let(:user) { create(:user) } - let(:project) { create(:project) } + let(:project) { create(:project, public_builds: false) } let(:issue) { create(:issue, project: project, created_at: 2.days.ago) } describe 'GET /:namespace/:project/cycle_analytics/events/issues' do diff --git a/spec/workers/pipeline_notification_worker_spec.rb b/spec/workers/pipeline_notification_worker_spec.rb index 739f9b63967..603ae52ed1e 100644 --- a/spec/workers/pipeline_notification_worker_spec.rb +++ b/spec/workers/pipeline_notification_worker_spec.rb @@ -11,7 +11,7 @@ describe PipelineNotificationWorker do status: status) end - let(:project) { create(:project) } + let(:project) { create(:project, public_builds: false) } let(:user) { create(:user) } let(:pusher) { user } let(:watcher) { pusher } -- cgit v1.2.1 From 74c8669b0a96b6afcb41ce5e09b147c7309ecbeb Mon Sep 17 00:00:00 2001 From: Robert Schilling <rschilling@student.tugraz.at> Date: Sun, 4 Dec 2016 18:11:19 +0100 Subject: Use the pagination helper in the API --- lib/api/access_requests.rb | 5 +++++ lib/api/award_emoji.rb | 5 +++++ lib/api/builds.rb | 7 +++++-- lib/api/commit_statuses.rb | 4 +++- lib/api/groups.rb | 12 ++++++++++-- lib/api/members.rb | 6 ++++-- lib/api/merge_requests.rb | 9 +++++++++ lib/api/milestones.rb | 5 ++++- lib/api/namespaces.rb | 4 +++- lib/api/notes.rb | 4 +++- lib/api/project_hooks.rb | 12 ++++++++---- lib/api/project_snippets.rb | 6 +++++- lib/api/runners.rb | 5 +++++ lib/api/todos.rb | 10 ++++++---- lib/api/triggers.rb | 5 +++++ lib/api/users.rb | 5 ++++- 16 files changed, 84 insertions(+), 20 deletions(-) diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb index ed723b94cfd..789f45489eb 100644 --- a/lib/api/access_requests.rb +++ b/lib/api/access_requests.rb @@ -1,5 +1,7 @@ module API class AccessRequests < Grape::API + include PaginationParams + before { authenticate! } helpers ::API::Helpers::MembersHelpers @@ -13,6 +15,9 @@ module API detail 'This feature was introduced in GitLab 8.11.' success Entities::AccessRequester end + params do + use :pagination + end get ":id/access_requests" do source = find_source(source_type, params[:id]) diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb index e9ccba3b465..58a4df54bea 100644 --- a/lib/api/award_emoji.rb +++ b/lib/api/award_emoji.rb @@ -1,5 +1,7 @@ module API class AwardEmoji < Grape::API + include PaginationParams + before { authenticate! } AWARDABLES = %w[issue merge_request snippet] @@ -21,6 +23,9 @@ module API detail 'This feature was introduced in 8.9' success Entities::AwardEmoji end + params do + use :pagination + end get endpoint do if can_read_awardable? awards = paginate(awardable.award_emoji) diff --git a/lib/api/builds.rb b/lib/api/builds.rb index 67adca6605f..af61be343be 100644 --- a/lib/api/builds.rb +++ b/lib/api/builds.rb @@ -1,6 +1,7 @@ module API - # Projects builds API class Builds < Grape::API + include PaginationParams + before { authenticate! } params do @@ -28,6 +29,7 @@ module API end params do use :optional_scope + use :pagination end get ':id/builds' do builds = user_project.builds.order('id DESC') @@ -41,8 +43,9 @@ module API success Entities::Build end params do - requires :sha, type: String, desc: 'The SHA id of a commit' + requires :sha, type: String, desc: 'The SHA id of a commit' use :optional_scope + use :pagination end get ':id/repository/commits/:sha/builds' do authorize_read_builds! diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 492884d162b..4bbdf06a49c 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -1,9 +1,10 @@ require 'mime/types' module API - # Project commit statuses API class CommitStatuses < Grape::API resource :projects do + include PaginationParams + before { authenticate! } desc "Get a commit's statuses" do @@ -16,6 +17,7 @@ module API optional :stage, type: String, desc: 'The stage' optional :name, type: String, desc: 'The name' optional :all, type: String, desc: 'Show all statuses, default: false' + use :pagination end get ':id/repository/commits/:sha/statuses' do authorize!(:read_commit_status, user_project) diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 5315c22e1e4..fbf7513302b 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -1,5 +1,7 @@ module API class Groups < Grape::API + include PaginationParams + before { authenticate! } helpers do @@ -21,6 +23,7 @@ module API optional :search, type: String, desc: 'Search for a specific group' optional :order_by, type: String, values: %w[name path], default: 'name', desc: 'Order by name or path' optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)' + use :pagination end get do groups = if current_user.admin @@ -41,6 +44,9 @@ module API desc 'Get list of owned groups for authenticated user' do success Entities::Group end + params do + use :pagination + end get '/owned' do groups = current_user.owned_groups present paginate(groups), with: Entities::Group, user: current_user @@ -110,11 +116,13 @@ module API desc 'Get a list of projects in this group.' do success Entities::Project end + params do + use :pagination + end get ":id/projects" do group = find_group!(params[:id]) projects = GroupProjectsFinder.new(group).execute(current_user) - projects = paginate projects - present projects, with: Entities::Project, user: current_user + present paginate(projects), with: Entities::Project, user: current_user end desc 'Transfer a project to the group namespace. Available only for admin.' do diff --git a/lib/api/members.rb b/lib/api/members.rb index 2d4d5cedf20..d85f1f78cd6 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -1,5 +1,7 @@ module API class Members < Grape::API + include PaginationParams + before { authenticate! } helpers ::API::Helpers::MembersHelpers @@ -14,15 +16,15 @@ module API end params do optional :query, type: String, desc: 'A query string to search for members' + use :pagination end get ":id/members" do source = find_source(source_type, params[:id]) users = source.users users = users.merge(User.search(params[:query])) if params[:query] - users = paginate(users) - present users, with: Entities::Member, source: source + present paginate(users), with: Entities::Member, source: source end desc 'Gets a member of a group or project.' do diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 97baebc1d27..752c105ff7c 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -1,5 +1,7 @@ module API class MergeRequests < Grape::API + include PaginationParams + DEPRECATION_MESSAGE = 'This endpoint is deprecated and will be removed in GitLab 9.0.'.freeze before { authenticate! } @@ -42,6 +44,7 @@ module API optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'Return merge requests sorted in `asc` or `desc` order.' optional :iid, type: Array[Integer], desc: 'The IID of the merge requests' + use :pagination end get ":id/merge_requests" do authorize! :read_merge_request, user_project @@ -218,6 +221,9 @@ module API detail 'Duplicate. DEPRECATED and WILL BE REMOVED in 9.0' success Entities::MRNote end + params do + use :pagination + end get "#{path}/comments" do merge_request = user_project.merge_requests.find(params[:merge_request_id]) @@ -255,6 +261,9 @@ module API desc 'List issues that will be closed on merge' do success Entities::MRNote end + params do + use :pagination + end get "#{path}/closes_issues" do merge_request = user_project.merge_requests.find(params[:merge_request_id]) issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index 50d6109be3d..3c373a84ec5 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -1,6 +1,7 @@ module API - # Milestones API class Milestones < Grape::API + include PaginationParams + before { authenticate! } helpers do @@ -30,6 +31,7 @@ module API optional :state, type: String, values: %w[active closed all], default: 'all', desc: 'Return "active", "closed", or "all" milestones' optional :iid, type: Array[Integer], desc: 'The IID of the milestone' + use :pagination end get ":id/milestones" do authorize! :read_milestone, user_project @@ -103,6 +105,7 @@ module API end params do requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' + use :pagination end get ":id/milestones/:milestone_id/issues" do authorize! :read_milestone, user_project diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb index fe981d7b9fa..30761cb9b55 100644 --- a/lib/api/namespaces.rb +++ b/lib/api/namespaces.rb @@ -1,6 +1,7 @@ module API - # namespaces API class Namespaces < Grape::API + include PaginationParams + before { authenticate! } resource :namespaces do @@ -9,6 +10,7 @@ module API end params do optional :search, type: String, desc: "Search query for namespaces" + use :pagination end get do namespaces = current_user.admin ? Namespace.all : current_user.namespaces diff --git a/lib/api/notes.rb b/lib/api/notes.rb index b255b47742b..d0faf17714b 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -1,6 +1,7 @@ module API - # Notes API class Notes < Grape::API + include PaginationParams + before { authenticate! } NOTEABLE_TYPES = [Issue, MergeRequest, Snippet] @@ -17,6 +18,7 @@ module API end params do requires :noteable_id, type: Integer, desc: 'The ID of the noteable' + use :pagination end get ":id/#{noteables_str}/:noteable_id/notes" do noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id]) diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index 2b36ef7c426..dcc0fb7a911 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -1,6 +1,10 @@ module API - # Projects API class ProjectHooks < Grape::API + include PaginationParams + + before { authenticate! } + before { authorize_admin_project } + helpers do params :project_hook_properties do requires :url, type: String, desc: "The URL to send the request to" @@ -17,9 +21,6 @@ module API end end - before { authenticate! } - before { authorize_admin_project } - params do requires :id, type: String, desc: 'The ID of a project' end @@ -27,6 +28,9 @@ module API desc 'Get project hooks' do success Entities::ProjectHook end + params do + use :pagination + end get ":id/hooks" do hooks = paginate user_project.hooks diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index d0ee9c9a5b2..9d8c5b63685 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -1,6 +1,7 @@ module API - # Projects API class ProjectSnippets < Grape::API + include PaginationParams + before { authenticate! } params do @@ -24,6 +25,9 @@ module API desc 'Get all project snippets' do success Entities::ProjectSnippet end + params do + use :pagination + end get ":id/snippets" do present paginate(snippets_for_current_user), with: Entities::ProjectSnippet end diff --git a/lib/api/runners.rb b/lib/api/runners.rb index b145cce7e3e..4816b5ed1b7 100644 --- a/lib/api/runners.rb +++ b/lib/api/runners.rb @@ -1,5 +1,7 @@ module API class Runners < Grape::API + include PaginationParams + before { authenticate! } resource :runners do @@ -9,6 +11,7 @@ module API params do optional :scope, type: String, values: %w[active paused online], desc: 'The scope of specific runners to show' + use :pagination end get do runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: ['specific', 'shared']) @@ -21,6 +24,7 @@ module API params do optional :scope, type: String, values: %w[active paused online specific shared], desc: 'The scope of specific runners to show' + use :pagination end get 'all' do authenticated_as_admin! @@ -91,6 +95,7 @@ module API params do optional :scope, type: String, values: %w[active paused online specific shared], desc: 'The scope of specific runners to show' + use :pagination end get ':id/runners' do runners = filter_runners(Ci::Runner.owned_or_shared(user_project.id), params[:scope]) diff --git a/lib/api/todos.rb b/lib/api/todos.rb index 832b04a3bb1..ed8f48aa1e3 100644 --- a/lib/api/todos.rb +++ b/lib/api/todos.rb @@ -1,6 +1,7 @@ module API - # Todos API class Todos < Grape::API + include PaginationParams + before { authenticate! } ISSUABLE_TYPES = { @@ -44,10 +45,11 @@ module API desc 'Get a todo list' do success Entities::Todo end + params do + use :pagination + end get do - todos = find_todos - - present paginate(todos), with: Entities::Todo, current_user: current_user + present paginate(find_todos), with: Entities::Todo, current_user: current_user end desc 'Mark a todo as done' do diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index bb4de39def1..87a717ba751 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -1,5 +1,7 @@ module API class Triggers < Grape::API + include PaginationParams + params do requires :id, type: String, desc: 'The ID of a project' end @@ -42,6 +44,9 @@ module API desc 'Get triggers list' do success Entities::Trigger end + params do + use :pagination + end get ':id/triggers' do authenticate! authorize! :admin_build, user_project diff --git a/lib/api/users.rb b/lib/api/users.rb index a73650dc361..bc2362aa72e 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -1,6 +1,7 @@ module API - # Users API class Users < Grape::API + include PaginationParams + before { authenticate! } resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do @@ -33,6 +34,7 @@ module API optional :active, type: Boolean, default: false, desc: 'Filters only active users' optional :external, type: Boolean, default: false, desc: 'Filters only external users' optional :blocked, type: Boolean, default: false, desc: 'Filters only blocked users' + use :pagination end get do unless can?(current_user, :read_users_list, nil) @@ -330,6 +332,7 @@ module API end params do requires :id, type: Integer, desc: 'The ID of the user' + use :pagination end get ':id/events' do user = User.find_by(id: params[:id]) -- cgit v1.2.1 From 2c76665274362d463158918105465c59eccfbaa7 Mon Sep 17 00:00:00 2001 From: Ryan Harris <harrisryan1@gmail.com> Date: Sun, 4 Dec 2016 16:30:06 -0500 Subject: Cursor now changes to a pointer when mousing over stages on Cycle Analytics page --- app/assets/stylesheets/pages/cycle_analytics.scss | 1 + changelogs/unreleased/24803-change-cursor-for-ca-stages.yml | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 changelogs/unreleased/24803-change-cursor-for-ca-stages.yml diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index e7a2c91003f..7c558445674 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -247,6 +247,7 @@ &.stage-name { width: 70%; + cursor: pointer; } &.stage-median { diff --git a/changelogs/unreleased/24803-change-cursor-for-ca-stages.yml b/changelogs/unreleased/24803-change-cursor-for-ca-stages.yml new file mode 100644 index 00000000000..b9d84c0ce31 --- /dev/null +++ b/changelogs/unreleased/24803-change-cursor-for-ca-stages.yml @@ -0,0 +1,5 @@ +--- +title: Changed cursor icon to pointer when mousing over stages on the Cycle Analytics + pages +merge_request: +author: Ryan Harris -- cgit v1.2.1 From bb447383c527d28bf4f884df18462e22be05f3b4 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen <oxan@oxanvanleeuwen.nl> Date: Tue, 22 Nov 2016 18:11:34 +0100 Subject: Add issue events filter and make sure "All" really shows everything Currently, the EventFilter whitelists event types for the "All" filter. This has gotten outdated, which causes the confusing behaviour of the "All" tab not really showing all events. To make matters worse, by default no tab at all is selected, which does show all events, so selecting the "All" tab actually hides some events. Fix this by: - Making sure All always includes all activity, by abolishing the whitelist and just returning all events instead. - Make the All tab selected by default. - Add Issue events tab to include the specific events around opening and closing issues, since there was no specific filter to see them yet. Fixes #24826 --- app/views/shared/_event_filter.html.haml | 2 ++ changelogs/unreleased/issue-events-filter.yml | 4 +++ lib/event_filter.rb | 38 +++++++++++------------- spec/javascripts/fixtures/event_filter.html.haml | 4 +++ spec/lib/event_filter_spec.rb | 15 ++++++++-- 5 files changed, 39 insertions(+), 24 deletions(-) create mode 100644 changelogs/unreleased/issue-events-filter.yml diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml index c367ae336db..67c145cef17 100644 --- a/app/views/shared/_event_filter.html.haml +++ b/app/views/shared/_event_filter.html.haml @@ -4,6 +4,8 @@ = event_filter_link EventFilter.push, 'Push events' - if event_filter_visible(:merge_requests) = event_filter_link EventFilter.merged, 'Merge events' + - if event_filter_visible(:issues) + = event_filter_link EventFilter.issue, 'Issue events' - if event_filter_visible(:issues) = event_filter_link EventFilter.comments, 'Comments' = event_filter_link EventFilter.team, 'Team' diff --git a/changelogs/unreleased/issue-events-filter.yml b/changelogs/unreleased/issue-events-filter.yml new file mode 100644 index 00000000000..a3b08bde6e7 --- /dev/null +++ b/changelogs/unreleased/issue-events-filter.yml @@ -0,0 +1,4 @@ +--- +title: Add issue events filter and make all really show all events +merge_request: 7673 +author: Oxan van Leeuwen diff --git a/lib/event_filter.rb b/lib/event_filter.rb index 21f6a9a762b..515095af1c2 100644 --- a/lib/event_filter.rb +++ b/lib/event_filter.rb @@ -14,6 +14,10 @@ class EventFilter 'merged' end + def issue + 'issue' + end + def comments 'comments' end @@ -32,32 +36,20 @@ class EventFilter end def apply_filter(events) - return events unless params.present? - - filter = params.dup - actions = [] + return events if params.blank? || params == EventFilter.all - case filter + case params when EventFilter.push - actions = [Event::PUSHED] + events.where(action: Event::PUSHED) when EventFilter.merged - actions = [Event::MERGED] + events.where(action: Event::MERGED) when EventFilter.comments - actions = [Event::COMMENTED] + events.where(action: Event::COMMENTED) when EventFilter.team - actions = [Event::JOINED, Event::LEFT, Event::EXPIRED] - when EventFilter.all - actions = [ - Event::PUSHED, - Event::MERGED, - Event::COMMENTED, - Event::JOINED, - Event::LEFT, - Event::EXPIRED - ] + events.where(action: [Event::JOINED, Event::LEFT, Event::EXPIRED]) + when EventFilter.issue + events.where(action: [Event::CREATED, Event::UPDATED, Event::CLOSED, Event::REOPENED]) end - - events.where(action: actions) end def options(key) @@ -73,6 +65,10 @@ class EventFilter end def active?(key) - params.include? key + if params.present? + params.include? key + else + key == EventFilter.all + end end end diff --git a/spec/javascripts/fixtures/event_filter.html.haml b/spec/javascripts/fixtures/event_filter.html.haml index 95e248cadf8..5477c6075f0 100644 --- a/spec/javascripts/fixtures/event_filter.html.haml +++ b/spec/javascripts/fixtures/event_filter.html.haml @@ -11,6 +11,10 @@ %a.event-filter-link{ id: "merged_event_filter", title: "Filter by merge events", href: "/dashboard/activity"} %span Merge events + %li + %a.event-filter-link{ id: "issue_event_filter", title: "Filter by issue events", href: "/dashboard/activity"} + %span + Issue events %li %a.event-filter-link{ id: "comments_event_filter", title: "Filter by comments", href: "/dashboard/activity"} %span diff --git a/spec/lib/event_filter_spec.rb b/spec/lib/event_filter_spec.rb index a6d8e6927e0..ec2f66b1136 100644 --- a/spec/lib/event_filter_spec.rb +++ b/spec/lib/event_filter_spec.rb @@ -7,6 +7,10 @@ describe EventFilter, lib: true do let!(:push_event) { create(:event, action: Event::PUSHED, project: public_project, target: public_project, author: source_user) } let!(:merged_event) { create(:event, action: Event::MERGED, project: public_project, target: public_project, author: source_user) } + let!(:created_event) { create(:event, action: Event::CREATED, project: public_project, target: public_project, author: source_user) } + let!(:updated_event) { create(:event, action: Event::UPDATED, project: public_project, target: public_project, author: source_user) } + let!(:closed_event) { create(:event, action: Event::CLOSED, project: public_project, target: public_project, author: source_user) } + let!(:reopened_event) { create(:event, action: Event::REOPENED, project: public_project, target: public_project, author: source_user) } let!(:comments_event) { create(:event, action: Event::COMMENTED, project: public_project, target: public_project, author: source_user) } let!(:joined_event) { create(:event, action: Event::JOINED, project: public_project, target: public_project, author: source_user) } let!(:left_event) { create(:event, action: Event::LEFT, project: public_project, target: public_project, author: source_user) } @@ -21,6 +25,11 @@ describe EventFilter, lib: true do expect(events).to contain_exactly(merged_event) end + it 'applies issue filter' do + events = EventFilter.new(EventFilter.issue).apply_filter(Event.all) + expect(events).to contain_exactly(created_event, updated_event, closed_event, reopened_event) + end + it 'applies comments filter' do events = EventFilter.new(EventFilter.comments).apply_filter(Event.all) expect(events).to contain_exactly(comments_event) @@ -33,17 +42,17 @@ describe EventFilter, lib: true do it 'applies all filter' do events = EventFilter.new(EventFilter.all).apply_filter(Event.all) - expect(events).to contain_exactly(push_event, merged_event, comments_event, joined_event, left_event) + expect(events).to contain_exactly(push_event, merged_event, created_event, updated_event, closed_event, reopened_event, comments_event, joined_event, left_event) end it 'applies no filter' do events = EventFilter.new(nil).apply_filter(Event.all) - expect(events).to contain_exactly(push_event, merged_event, comments_event, joined_event, left_event) + expect(events).to contain_exactly(push_event, merged_event, created_event, updated_event, closed_event, reopened_event, comments_event, joined_event, left_event) end it 'applies unknown filter' do events = EventFilter.new('').apply_filter(Event.all) - expect(events).to contain_exactly(push_event, merged_event, comments_event, joined_event, left_event) + expect(events).to contain_exactly(push_event, merged_event, created_event, updated_event, closed_event, reopened_event, comments_event, joined_event, left_event) end end end -- cgit v1.2.1 From 7ee0d651a3ada02373969f40330b3baf0fb26427 Mon Sep 17 00:00:00 2001 From: Connor Shea <connor.james.shea@gmail.com> Date: Sun, 4 Dec 2016 17:28:36 -0700 Subject: Update paranoia from 2.1.4 to 2.2.0. Includes support for Rails 5. Changelog: https://github.com/rubysherpas/paranoia/blob/879fd18caa46af70fceca2e8f46886b3eff072ec/CHANGELOG.md#220-2016-10-21 --- Gemfile | 2 +- Gemfile.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 83edf420798..e3f2b3ea0b2 100644 --- a/Gemfile +++ b/Gemfile @@ -338,7 +338,7 @@ gem 'ruby-prof', '~> 0.16.2' gem 'oauth2', '~> 1.2.0' # Soft deletion -gem 'paranoia', '~> 2.0' +gem 'paranoia', '~> 2.2' # Health check gem 'health_check', '~> 2.2.0' diff --git a/Gemfile.lock b/Gemfile.lock index 5a14ed6fede..e5e6e6895a8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -460,8 +460,8 @@ GEM org-ruby (0.9.12) rubypants (~> 0.2) orm_adapter (0.5.0) - paranoia (2.1.4) - activerecord (~> 4.0) + paranoia (2.2.0) + activerecord (>= 4.0, < 5.1) parser (2.3.1.4) ast (~> 2.2) pg (0.18.4) @@ -887,7 +887,7 @@ DEPENDENCIES omniauth-twitter (~> 1.2.0) omniauth_crowd (~> 2.2.0) org-ruby (~> 0.9.12) - paranoia (~> 2.0) + paranoia (~> 2.2) pg (~> 0.18.2) poltergeist (~> 1.9.0) premailer-rails (~> 1.9.0) -- cgit v1.2.1 From 13858d6873afb1168247f79c183ad8260bf4ccf4 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Mon, 28 Nov 2016 13:18:06 +0100 Subject: Accept `issue new` as command to create an issue Now only `/trigger issue create` is a valid command, but our UI shows Issue new everywhere. The default now will be `/trigger issue new`. The help message is adjusted to reflect this. Fixes: gitlab-org/gitlab-ce#25025 --- changelogs/unreleased/zj-issue-new-over-issue-create.yml | 4 ++++ lib/gitlab/chat_commands/issue_create.rb | 4 ++-- spec/lib/gitlab/chat_commands/issue_create_spec.rb | 7 +++++++ 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/zj-issue-new-over-issue-create.yml diff --git a/changelogs/unreleased/zj-issue-new-over-issue-create.yml b/changelogs/unreleased/zj-issue-new-over-issue-create.yml new file mode 100644 index 00000000000..9dd463e4efa --- /dev/null +++ b/changelogs/unreleased/zj-issue-new-over-issue-create.yml @@ -0,0 +1,4 @@ +--- +title: Accept issue new as command to create an issue +merge_request: +author: diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb index 99c1382af44..1dba85c1b51 100644 --- a/lib/gitlab/chat_commands/issue_create.rb +++ b/lib/gitlab/chat_commands/issue_create.rb @@ -4,11 +4,11 @@ module Gitlab def self.match(text) # we can not match \n with the dot by passing the m modifier as than # the title and description are not seperated - /\Aissue\s+create\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text) + /\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text) end def self.help_message - 'issue create <title>\n<description>' + 'issue new <title>\n<description>' end def self.allowed?(project, user) diff --git a/spec/lib/gitlab/chat_commands/issue_create_spec.rb b/spec/lib/gitlab/chat_commands/issue_create_spec.rb index dd07cff9243..6c71e79ff6d 100644 --- a/spec/lib/gitlab/chat_commands/issue_create_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_create_spec.rb @@ -57,5 +57,12 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do expect(match[:title]).to eq('my title') expect(match[:description]).to eq('description') end + + it 'matches the alias new' do + match = described_class.match("issue new my title") + + expect(match).not_to be_nil + expect(match[:title]).to eq('my title') + end end end -- cgit v1.2.1 From 4efdbdb266ae15e1effb768b5fea7ee3d0027e66 Mon Sep 17 00:00:00 2001 From: Steffen Rauh <steffen.rauh@gmx.de> Date: Mon, 5 Dec 2016 11:00:19 +0100 Subject: Fixed influence from other specs. --- spec/javascripts/bootstrap_linked_tabs_spec.js.es6 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 index 9aa3c50611d..133712debab 100644 --- a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 +++ b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 @@ -9,6 +9,10 @@ }); describe('when is initialized', () => { + beforeEach(() => { + spyOn(window.history, 'replaceState').and.callFake(function () {}); + }); + it('should activate the tab correspondent to the given action', () => { const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line action: 'tab1', -- cgit v1.2.1 From b86d8afe23524d10956cfbc1b87337fd2ce75e8c Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 5 Dec 2016 11:35:27 +0100 Subject: Fold core/extended status modules to reduce nesting --- lib/gitlab/ci/status/canceled.rb | 19 +++++++ lib/gitlab/ci/status/core.rb | 47 ++++++++++++++++ lib/gitlab/ci/status/core/base.rb | 47 ---------------- lib/gitlab/ci/status/core/canceled.rb | 19 ------- lib/gitlab/ci/status/core/created.rb | 19 ------- lib/gitlab/ci/status/core/failed.rb | 19 ------- lib/gitlab/ci/status/core/pending.rb | 19 ------- lib/gitlab/ci/status/core/running.rb | 19 ------- lib/gitlab/ci/status/core/skipped.rb | 19 ------- lib/gitlab/ci/status/core/success.rb | 19 ------- lib/gitlab/ci/status/created.rb | 19 +++++++ lib/gitlab/ci/status/extended.rb | 11 ++++ lib/gitlab/ci/status/extended/base.rb | 11 ---- lib/gitlab/ci/status/extended/pipeline/common.rb | 23 -------- .../extended/pipeline/success_with_warnings.rb | 27 --------- lib/gitlab/ci/status/factory.rb | 6 -- lib/gitlab/ci/status/failed.rb | 19 +++++++ lib/gitlab/ci/status/pending.rb | 19 +++++++ lib/gitlab/ci/status/pipeline/common.rb | 23 ++++++++ .../ci/status/pipeline/success_with_warnings.rb | 27 +++++++++ lib/gitlab/ci/status/running.rb | 19 +++++++ lib/gitlab/ci/status/skipped.rb | 19 +++++++ lib/gitlab/ci/status/success.rb | 19 +++++++ spec/lib/gitlab/ci/status/canceled_spec.rb | 21 +++++++ spec/lib/gitlab/ci/status/core/canceled_spec.rb | 21 ------- spec/lib/gitlab/ci/status/core/created_spec.rb | 21 ------- spec/lib/gitlab/ci/status/core/failed_spec.rb | 21 ------- spec/lib/gitlab/ci/status/core/pending_spec.rb | 21 ------- spec/lib/gitlab/ci/status/core/running_spec.rb | 21 ------- spec/lib/gitlab/ci/status/core/skipped_spec.rb | 21 ------- spec/lib/gitlab/ci/status/core/success_spec.rb | 21 ------- spec/lib/gitlab/ci/status/created_spec.rb | 21 +++++++ spec/lib/gitlab/ci/status/extended/base_spec.rb | 12 ---- .../ci/status/extended/pipeline/common_spec.rb | 23 -------- .../pipeline/success_with_warnings_spec.rb | 65 ---------------------- spec/lib/gitlab/ci/status/extended_spec.rb | 12 ++++ spec/lib/gitlab/ci/status/factory_spec.rb | 0 spec/lib/gitlab/ci/status/failed_spec.rb | 21 +++++++ spec/lib/gitlab/ci/status/pending_spec.rb | 21 +++++++ spec/lib/gitlab/ci/status/pipeline/common_spec.rb | 23 ++++++++ .../status/pipeline/success_with_warnings_spec.rb | 65 ++++++++++++++++++++++ spec/lib/gitlab/ci/status/running_spec.rb | 21 +++++++ spec/lib/gitlab/ci/status/skipped_spec.rb | 21 +++++++ spec/lib/gitlab/ci/status/success_spec.rb | 21 +++++++ 44 files changed, 488 insertions(+), 494 deletions(-) create mode 100644 lib/gitlab/ci/status/canceled.rb create mode 100644 lib/gitlab/ci/status/core.rb delete mode 100644 lib/gitlab/ci/status/core/base.rb delete mode 100644 lib/gitlab/ci/status/core/canceled.rb delete mode 100644 lib/gitlab/ci/status/core/created.rb delete mode 100644 lib/gitlab/ci/status/core/failed.rb delete mode 100644 lib/gitlab/ci/status/core/pending.rb delete mode 100644 lib/gitlab/ci/status/core/running.rb delete mode 100644 lib/gitlab/ci/status/core/skipped.rb delete mode 100644 lib/gitlab/ci/status/core/success.rb create mode 100644 lib/gitlab/ci/status/created.rb create mode 100644 lib/gitlab/ci/status/extended.rb delete mode 100644 lib/gitlab/ci/status/extended/base.rb delete mode 100644 lib/gitlab/ci/status/extended/pipeline/common.rb delete mode 100644 lib/gitlab/ci/status/extended/pipeline/success_with_warnings.rb delete mode 100644 lib/gitlab/ci/status/factory.rb create mode 100644 lib/gitlab/ci/status/failed.rb create mode 100644 lib/gitlab/ci/status/pending.rb create mode 100644 lib/gitlab/ci/status/pipeline/common.rb create mode 100644 lib/gitlab/ci/status/pipeline/success_with_warnings.rb create mode 100644 lib/gitlab/ci/status/running.rb create mode 100644 lib/gitlab/ci/status/skipped.rb create mode 100644 lib/gitlab/ci/status/success.rb create mode 100644 spec/lib/gitlab/ci/status/canceled_spec.rb delete mode 100644 spec/lib/gitlab/ci/status/core/canceled_spec.rb delete mode 100644 spec/lib/gitlab/ci/status/core/created_spec.rb delete mode 100644 spec/lib/gitlab/ci/status/core/failed_spec.rb delete mode 100644 spec/lib/gitlab/ci/status/core/pending_spec.rb delete mode 100644 spec/lib/gitlab/ci/status/core/running_spec.rb delete mode 100644 spec/lib/gitlab/ci/status/core/skipped_spec.rb delete mode 100644 spec/lib/gitlab/ci/status/core/success_spec.rb create mode 100644 spec/lib/gitlab/ci/status/created_spec.rb delete mode 100644 spec/lib/gitlab/ci/status/extended/base_spec.rb delete mode 100644 spec/lib/gitlab/ci/status/extended/pipeline/common_spec.rb delete mode 100644 spec/lib/gitlab/ci/status/extended/pipeline/success_with_warnings_spec.rb create mode 100644 spec/lib/gitlab/ci/status/extended_spec.rb delete mode 100644 spec/lib/gitlab/ci/status/factory_spec.rb create mode 100644 spec/lib/gitlab/ci/status/failed_spec.rb create mode 100644 spec/lib/gitlab/ci/status/pending_spec.rb create mode 100644 spec/lib/gitlab/ci/status/pipeline/common_spec.rb create mode 100644 spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb create mode 100644 spec/lib/gitlab/ci/status/running_spec.rb create mode 100644 spec/lib/gitlab/ci/status/skipped_spec.rb create mode 100644 spec/lib/gitlab/ci/status/success_spec.rb diff --git a/lib/gitlab/ci/status/canceled.rb b/lib/gitlab/ci/status/canceled.rb new file mode 100644 index 00000000000..dd6d99e9075 --- /dev/null +++ b/lib/gitlab/ci/status/canceled.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + class Canceled < Status::Core + def text + 'canceled' + end + + def label + 'canceled' + end + + def icon + 'icon_status_canceled' + end + end + end + end +end diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb new file mode 100644 index 00000000000..fbfe257eeca --- /dev/null +++ b/lib/gitlab/ci/status/core.rb @@ -0,0 +1,47 @@ +module Gitlab + module Ci + module Status + # Base abstract class fore core status + # + class Core + include Gitlab::Routing.url_helpers + + def initialize(subject) + @subject = subject + end + + def icon + raise NotImplementedError + end + + def label + raise NotImplementedError + end + + def title + "#{@subject.class.name.demodulize}: #{label}" + end + + def has_details? + raise NotImplementedError + end + + def details_path + raise NotImplementedError + end + + def has_action? + raise NotImplementedError + end + + def action_icon + raise NotImplementedError + end + + def action_path + raise NotImplementedError + end + end + end + end +end diff --git a/lib/gitlab/ci/status/core/base.rb b/lib/gitlab/ci/status/core/base.rb deleted file mode 100644 index d1896a610b7..00000000000 --- a/lib/gitlab/ci/status/core/base.rb +++ /dev/null @@ -1,47 +0,0 @@ -module Gitlab::Ci - module Status - module Core - # Base abstract class fore core status - # - class Base - include Gitlab::Routing.url_helpers - - def initialize(subject) - @subject = subject - end - - def icon - raise NotImplementedError - end - - def label - raise NotImplementedError - end - - def title - "#{@subject.class.name.demodulize}: #{label}" - end - - def has_details? - raise NotImplementedError - end - - def details_path - raise NotImplementedError - end - - def has_action? - raise NotImplementedError - end - - def action_icon - raise NotImplementedError - end - - def action_path - raise NotImplementedError - end - end - end - end -end diff --git a/lib/gitlab/ci/status/core/canceled.rb b/lib/gitlab/ci/status/core/canceled.rb deleted file mode 100644 index a05ac8ee3cc..00000000000 --- a/lib/gitlab/ci/status/core/canceled.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Gitlab::Ci - module Status - module Core - class Canceled < Core::Base - def text - 'canceled' - end - - def label - 'canceled' - end - - def icon - 'icon_status_canceled' - end - end - end - end -end diff --git a/lib/gitlab/ci/status/core/created.rb b/lib/gitlab/ci/status/core/created.rb deleted file mode 100644 index ee8bf2e8dac..00000000000 --- a/lib/gitlab/ci/status/core/created.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Gitlab::Ci - module Status - module Core - class Created < Core::Base - def text - 'created' - end - - def label - 'created' - end - - def icon - 'icon_status_created' - end - end - end - end -end diff --git a/lib/gitlab/ci/status/core/failed.rb b/lib/gitlab/ci/status/core/failed.rb deleted file mode 100644 index ea1615853c0..00000000000 --- a/lib/gitlab/ci/status/core/failed.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Gitlab::Ci - module Status - module Core - class Failed < Core::Base - def text - 'failed' - end - - def label - 'failed' - end - - def icon - 'icon_status_failed' - end - end - end - end -end diff --git a/lib/gitlab/ci/status/core/pending.rb b/lib/gitlab/ci/status/core/pending.rb deleted file mode 100644 index 95fbb710735..00000000000 --- a/lib/gitlab/ci/status/core/pending.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Gitlab::Ci - module Status - module Core - class Pending < Core::Base - def text - 'pending' - end - - def label - 'pending' - end - - def icon - 'icon_status_pending' - end - end - end - end -end diff --git a/lib/gitlab/ci/status/core/running.rb b/lib/gitlab/ci/status/core/running.rb deleted file mode 100644 index 5580c1a5154..00000000000 --- a/lib/gitlab/ci/status/core/running.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Gitlab::Ci - module Status - module Core - class Running < Core::Base - def text - 'running' - end - - def label - 'running' - end - - def icon - 'icon_status_running' - end - end - end - end -end diff --git a/lib/gitlab/ci/status/core/skipped.rb b/lib/gitlab/ci/status/core/skipped.rb deleted file mode 100644 index 0e8e42f525b..00000000000 --- a/lib/gitlab/ci/status/core/skipped.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Gitlab::Ci - module Status - module Core - class Skipped < Core::Base - def text - 'skipped' - end - - def label - 'skipped' - end - - def icon - 'icon_status_skipped' - end - end - end - end -end diff --git a/lib/gitlab/ci/status/core/success.rb b/lib/gitlab/ci/status/core/success.rb deleted file mode 100644 index 7efafdb615f..00000000000 --- a/lib/gitlab/ci/status/core/success.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Gitlab::Ci - module Status - module Core - class Success < Core::Base - def text - 'passed' - end - - def label - 'passed' - end - - def icon - 'icon_status_success' - end - end - end - end -end diff --git a/lib/gitlab/ci/status/created.rb b/lib/gitlab/ci/status/created.rb new file mode 100644 index 00000000000..6596d7e01ca --- /dev/null +++ b/lib/gitlab/ci/status/created.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + class Created < Status::Core + def text + 'created' + end + + def label + 'created' + end + + def icon + 'icon_status_created' + end + end + end + end +end diff --git a/lib/gitlab/ci/status/extended.rb b/lib/gitlab/ci/status/extended.rb new file mode 100644 index 00000000000..6bfb5d38c1f --- /dev/null +++ b/lib/gitlab/ci/status/extended.rb @@ -0,0 +1,11 @@ +module Gitlab + module Ci + module Status + module Extended + def matches?(_subject) + raise NotImplementedError + end + end + end + end +end diff --git a/lib/gitlab/ci/status/extended/base.rb b/lib/gitlab/ci/status/extended/base.rb deleted file mode 100644 index 1d7819c6891..00000000000 --- a/lib/gitlab/ci/status/extended/base.rb +++ /dev/null @@ -1,11 +0,0 @@ -module Gitlab::Ci - module Status - module Extended - module Base - def matches?(_subject) - raise NotImplementedError - end - end - end - end -end diff --git a/lib/gitlab/ci/status/extended/pipeline/common.rb b/lib/gitlab/ci/status/extended/pipeline/common.rb deleted file mode 100644 index 1b70ba303dc..00000000000 --- a/lib/gitlab/ci/status/extended/pipeline/common.rb +++ /dev/null @@ -1,23 +0,0 @@ -module Gitlab::Ci - module Status - module Extended - module Pipeline - module Common - def has_details? - true - end - - def details_path - namespace_project_pipeline_path(@subject.project.namespace, - @subject.project, - @subject) - end - - def has_action? - false - end - end - end - end - end -end diff --git a/lib/gitlab/ci/status/extended/pipeline/success_with_warnings.rb b/lib/gitlab/ci/status/extended/pipeline/success_with_warnings.rb deleted file mode 100644 index 8f1d9cf87c7..00000000000 --- a/lib/gitlab/ci/status/extended/pipeline/success_with_warnings.rb +++ /dev/null @@ -1,27 +0,0 @@ -module Gitlab::Ci - module Status - module Extended - module Pipeline - class SuccessWithWarnings < SimpleDelegator - extend Status::Extended::Base - - def text - 'passed' - end - - def label - 'passed with warnings' - end - - def icon - 'icon_status_warning' - end - - def self.matches?(pipeline) - pipeline.success? && pipeline.has_warnings? - end - end - end - end - end -end diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb deleted file mode 100644 index 212cd1a1687..00000000000 --- a/lib/gitlab/ci/status/factory.rb +++ /dev/null @@ -1,6 +0,0 @@ -module Gitlab::Ci - module Status - class Factory - end - end -end diff --git a/lib/gitlab/ci/status/failed.rb b/lib/gitlab/ci/status/failed.rb new file mode 100644 index 00000000000..c5b5e3203ad --- /dev/null +++ b/lib/gitlab/ci/status/failed.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + class Failed < Status::Core + def text + 'failed' + end + + def label + 'failed' + end + + def icon + 'icon_status_failed' + end + end + end + end +end diff --git a/lib/gitlab/ci/status/pending.rb b/lib/gitlab/ci/status/pending.rb new file mode 100644 index 00000000000..d30f35a59a2 --- /dev/null +++ b/lib/gitlab/ci/status/pending.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + class Pending < Status::Core + def text + 'pending' + end + + def label + 'pending' + end + + def icon + 'icon_status_pending' + end + end + end + end +end diff --git a/lib/gitlab/ci/status/pipeline/common.rb b/lib/gitlab/ci/status/pipeline/common.rb new file mode 100644 index 00000000000..25e52bec3da --- /dev/null +++ b/lib/gitlab/ci/status/pipeline/common.rb @@ -0,0 +1,23 @@ +module Gitlab + module Ci + module Status + module Pipeline + module Common + def has_details? + true + end + + def details_path + namespace_project_pipeline_path(@subject.project.namespace, + @subject.project, + @subject) + end + + def has_action? + false + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb new file mode 100644 index 00000000000..97dfba81ff5 --- /dev/null +++ b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb @@ -0,0 +1,27 @@ +module Gitlab + module Ci + module Status + module Pipeline + class SuccessWithWarnings < SimpleDelegator + extend Status::Extended + + def text + 'passed' + end + + def label + 'passed with warnings' + end + + def icon + 'icon_status_warning' + end + + def self.matches?(pipeline) + pipeline.success? && pipeline.has_warnings? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/running.rb b/lib/gitlab/ci/status/running.rb new file mode 100644 index 00000000000..2aba3c373c7 --- /dev/null +++ b/lib/gitlab/ci/status/running.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + class Running < Status::Core + def text + 'running' + end + + def label + 'running' + end + + def icon + 'icon_status_running' + end + end + end + end +end diff --git a/lib/gitlab/ci/status/skipped.rb b/lib/gitlab/ci/status/skipped.rb new file mode 100644 index 00000000000..16282aefd03 --- /dev/null +++ b/lib/gitlab/ci/status/skipped.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + class Skipped < Status::Core + def text + 'skipped' + end + + def label + 'skipped' + end + + def icon + 'icon_status_skipped' + end + end + end + end +end diff --git a/lib/gitlab/ci/status/success.rb b/lib/gitlab/ci/status/success.rb new file mode 100644 index 00000000000..c09c5f006e3 --- /dev/null +++ b/lib/gitlab/ci/status/success.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + class Success < Status::Core + def text + 'passed' + end + + def label + 'passed' + end + + def icon + 'icon_status_success' + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb new file mode 100644 index 00000000000..619ecbcba67 --- /dev/null +++ b/spec/lib/gitlab/ci/status/canceled_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Canceled do + subject { described_class.new(double('subject')) } + + describe '#text' do + it { expect(subject.label).to eq 'canceled' } + end + + describe '#label' do + it { expect(subject.label).to eq 'canceled' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_canceled' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: canceled' } + end +end diff --git a/spec/lib/gitlab/ci/status/core/canceled_spec.rb b/spec/lib/gitlab/ci/status/core/canceled_spec.rb deleted file mode 100644 index fd90eb6beda..00000000000 --- a/spec/lib/gitlab/ci/status/core/canceled_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Ci::Status::Core::Canceled do - subject { described_class.new(double('subject')) } - - describe '#text' do - it { expect(subject.label).to eq 'canceled' } - end - - describe '#label' do - it { expect(subject.label).to eq 'canceled' } - end - - describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_canceled' } - end - - describe '#title' do - it { expect(subject.title).to eq 'Double: canceled' } - end -end diff --git a/spec/lib/gitlab/ci/status/core/created_spec.rb b/spec/lib/gitlab/ci/status/core/created_spec.rb deleted file mode 100644 index a35a3e14929..00000000000 --- a/spec/lib/gitlab/ci/status/core/created_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Ci::Status::Core::Created do - subject { described_class.new(double('subject')) } - - describe '#text' do - it { expect(subject.label).to eq 'created' } - end - - describe '#label' do - it { expect(subject.label).to eq 'created' } - end - - describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_created' } - end - - describe '#title' do - it { expect(subject.title).to eq 'Double: created' } - end -end diff --git a/spec/lib/gitlab/ci/status/core/failed_spec.rb b/spec/lib/gitlab/ci/status/core/failed_spec.rb deleted file mode 100644 index 41ce63b3a6f..00000000000 --- a/spec/lib/gitlab/ci/status/core/failed_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Ci::Status::Core::Failed do - subject { described_class.new(double('subject')) } - - describe '#text' do - it { expect(subject.label).to eq 'failed' } - end - - describe '#label' do - it { expect(subject.label).to eq 'failed' } - end - - describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_failed' } - end - - describe '#title' do - it { expect(subject.title).to eq 'Double: failed' } - end -end diff --git a/spec/lib/gitlab/ci/status/core/pending_spec.rb b/spec/lib/gitlab/ci/status/core/pending_spec.rb deleted file mode 100644 index 988d3c0a9e2..00000000000 --- a/spec/lib/gitlab/ci/status/core/pending_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Ci::Status::Core::Pending do - subject { described_class.new(double('subject')) } - - describe '#text' do - it { expect(subject.label).to eq 'pending' } - end - - describe '#label' do - it { expect(subject.label).to eq 'pending' } - end - - describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_pending' } - end - - describe '#title' do - it { expect(subject.title).to eq 'Double: pending' } - end -end diff --git a/spec/lib/gitlab/ci/status/core/running_spec.rb b/spec/lib/gitlab/ci/status/core/running_spec.rb deleted file mode 100644 index dbb0d37659c..00000000000 --- a/spec/lib/gitlab/ci/status/core/running_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Ci::Status::Core::Running do - subject { described_class.new(double('subject')) } - - describe '#text' do - it { expect(subject.label).to eq 'running' } - end - - describe '#label' do - it { expect(subject.label).to eq 'running' } - end - - describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_running' } - end - - describe '#title' do - it { expect(subject.title).to eq 'Double: running' } - end -end diff --git a/spec/lib/gitlab/ci/status/core/skipped_spec.rb b/spec/lib/gitlab/ci/status/core/skipped_spec.rb deleted file mode 100644 index 624348af2d1..00000000000 --- a/spec/lib/gitlab/ci/status/core/skipped_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Ci::Status::Core::Skipped do - subject { described_class.new(double('subject')) } - - describe '#text' do - it { expect(subject.label).to eq 'skipped' } - end - - describe '#label' do - it { expect(subject.label).to eq 'skipped' } - end - - describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_skipped' } - end - - describe '#title' do - it { expect(subject.title).to eq 'Double: skipped' } - end -end diff --git a/spec/lib/gitlab/ci/status/core/success_spec.rb b/spec/lib/gitlab/ci/status/core/success_spec.rb deleted file mode 100644 index c4bc0d5e234..00000000000 --- a/spec/lib/gitlab/ci/status/core/success_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Ci::Status::Core::Success do - subject { described_class.new(double('subject')) } - - describe '#text' do - it { expect(subject.label).to eq 'passed' } - end - - describe '#label' do - it { expect(subject.label).to eq 'passed' } - end - - describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_success' } - end - - describe '#title' do - it { expect(subject.title).to eq 'Double: passed' } - end -end diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb new file mode 100644 index 00000000000..157302c65a8 --- /dev/null +++ b/spec/lib/gitlab/ci/status/created_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Created do + subject { described_class.new(double('subject')) } + + describe '#text' do + it { expect(subject.label).to eq 'created' } + end + + describe '#label' do + it { expect(subject.label).to eq 'created' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_created' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: created' } + end +end diff --git a/spec/lib/gitlab/ci/status/extended/base_spec.rb b/spec/lib/gitlab/ci/status/extended/base_spec.rb deleted file mode 100644 index 7cdc68c927f..00000000000 --- a/spec/lib/gitlab/ci/status/extended/base_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Ci::Status::Extended::Base do - subject do - Class.new.extend(described_class) - end - - it 'requires subclass to implement matcher' do - expect { subject.matches?(double) } - .to raise_error(NotImplementedError) - end -end diff --git a/spec/lib/gitlab/ci/status/extended/pipeline/common_spec.rb b/spec/lib/gitlab/ci/status/extended/pipeline/common_spec.rb deleted file mode 100644 index 32939800c70..00000000000 --- a/spec/lib/gitlab/ci/status/extended/pipeline/common_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Ci::Status::Extended::Pipeline::Common do - let(:pipeline) { create(:ci_pipeline) } - - subject do - Gitlab::Ci::Status::Core::Success - .new(pipeline).extend(described_class) - end - - it 'does not have action' do - expect(subject).not_to have_action - end - - it 'has details' do - expect(subject).to have_details - end - - it 'links to the pipeline details page' do - expect(subject.details_path) - .to include "pipelines/#{pipeline.id}" - end -end diff --git a/spec/lib/gitlab/ci/status/extended/pipeline/success_with_warnings_spec.rb b/spec/lib/gitlab/ci/status/extended/pipeline/success_with_warnings_spec.rb deleted file mode 100644 index b1a63c5f2f9..00000000000 --- a/spec/lib/gitlab/ci/status/extended/pipeline/success_with_warnings_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Ci::Status::Extended::Pipeline::SuccessWithWarnings do - subject do - described_class.new(double('status')) - end - - describe '#test' do - it { expect(subject.text).to eq 'passed' } - end - - describe '#label' do - it { expect(subject.label).to eq 'passed with warnings' } - end - - describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_warning' } - end - - describe '.matches?' do - context 'when pipeline is successful' do - let(:pipeline) do - create(:ci_pipeline, status: :success) - end - - context 'when pipeline has warnings' do - before do - allow(pipeline).to receive(:has_warnings?).and_return(true) - end - - it 'is a correct match' do - expect(described_class.matches?(pipeline)).to eq true - end - end - - context 'when pipeline does not have warnings' do - it 'does not match' do - expect(described_class.matches?(pipeline)).to eq false - end - end - end - - context 'when pipeline is not successful' do - let(:pipeline) do - create(:ci_pipeline, status: :skipped) - end - - context 'when pipeline has warnings' do - before do - allow(pipeline).to receive(:has_warnings?).and_return(true) - end - - it 'does not match' do - expect(described_class.matches?(pipeline)).to eq false - end - end - - context 'when pipeline does not have warnings' do - it 'does not match' do - expect(described_class.matches?(pipeline)).to eq false - end - end - end - end -end diff --git a/spec/lib/gitlab/ci/status/extended_spec.rb b/spec/lib/gitlab/ci/status/extended_spec.rb new file mode 100644 index 00000000000..120e121aae5 --- /dev/null +++ b/spec/lib/gitlab/ci/status/extended_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Extended do + subject do + Class.new.extend(described_class) + end + + it 'requires subclass to implement matcher' do + expect { subject.matches?(double) } + .to raise_error(NotImplementedError) + end +end diff --git a/spec/lib/gitlab/ci/status/factory_spec.rb b/spec/lib/gitlab/ci/status/factory_spec.rb deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb new file mode 100644 index 00000000000..0b3cb8168e6 --- /dev/null +++ b/spec/lib/gitlab/ci/status/failed_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Failed do + subject { described_class.new(double('subject')) } + + describe '#text' do + it { expect(subject.label).to eq 'failed' } + end + + describe '#label' do + it { expect(subject.label).to eq 'failed' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_failed' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: failed' } + end +end diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb new file mode 100644 index 00000000000..57c901c1202 --- /dev/null +++ b/spec/lib/gitlab/ci/status/pending_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Pending do + subject { described_class.new(double('subject')) } + + describe '#text' do + it { expect(subject.label).to eq 'pending' } + end + + describe '#label' do + it { expect(subject.label).to eq 'pending' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_pending' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: pending' } + end +end diff --git a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb new file mode 100644 index 00000000000..21adee3f8e7 --- /dev/null +++ b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Pipeline::Common do + let(:pipeline) { create(:ci_pipeline) } + + subject do + Class.new(Gitlab::Ci::Status::Core) + .new(pipeline).extend(described_class) + end + + it 'does not have action' do + expect(subject).not_to have_action + end + + it 'has details' do + expect(subject).to have_details + end + + it 'links to the pipeline details page' do + expect(subject.details_path) + .to include "pipelines/#{pipeline.id}" + end +end diff --git a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb new file mode 100644 index 00000000000..02e526e3de2 --- /dev/null +++ b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do + subject do + described_class.new(double('status')) + end + + describe '#test' do + it { expect(subject.text).to eq 'passed' } + end + + describe '#label' do + it { expect(subject.label).to eq 'passed with warnings' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_warning' } + end + + describe '.matches?' do + context 'when pipeline is successful' do + let(:pipeline) do + create(:ci_pipeline, status: :success) + end + + context 'when pipeline has warnings' do + before do + allow(pipeline).to receive(:has_warnings?).and_return(true) + end + + it 'is a correct match' do + expect(described_class.matches?(pipeline)).to eq true + end + end + + context 'when pipeline does not have warnings' do + it 'does not match' do + expect(described_class.matches?(pipeline)).to eq false + end + end + end + + context 'when pipeline is not successful' do + let(:pipeline) do + create(:ci_pipeline, status: :skipped) + end + + context 'when pipeline has warnings' do + before do + allow(pipeline).to receive(:has_warnings?).and_return(true) + end + + it 'does not match' do + expect(described_class.matches?(pipeline)).to eq false + end + end + + context 'when pipeline does not have warnings' do + it 'does not match' do + expect(described_class.matches?(pipeline)).to eq false + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb new file mode 100644 index 00000000000..c023f1872cc --- /dev/null +++ b/spec/lib/gitlab/ci/status/running_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Running do + subject { described_class.new(double('subject')) } + + describe '#text' do + it { expect(subject.label).to eq 'running' } + end + + describe '#label' do + it { expect(subject.label).to eq 'running' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_running' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: running' } + end +end diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb new file mode 100644 index 00000000000..d4f7f4b3b70 --- /dev/null +++ b/spec/lib/gitlab/ci/status/skipped_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Skipped do + subject { described_class.new(double('subject')) } + + describe '#text' do + it { expect(subject.label).to eq 'skipped' } + end + + describe '#label' do + it { expect(subject.label).to eq 'skipped' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_skipped' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: skipped' } + end +end diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb new file mode 100644 index 00000000000..9e261a3aa5f --- /dev/null +++ b/spec/lib/gitlab/ci/status/success_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Success do + subject { described_class.new(double('subject')) } + + describe '#text' do + it { expect(subject.label).to eq 'passed' } + end + + describe '#label' do + it { expect(subject.label).to eq 'passed' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_success' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: passed' } + end +end -- cgit v1.2.1 From d28f5e776b90a648a83246beac94518cd8183af4 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 5 Dec 2016 12:07:14 +0100 Subject: Implement pipeline status factory with extended status --- lib/gitlab/ci/status/pipeline/factory.rb | 39 ++++++++++++++++++++++ spec/lib/gitlab/ci/status/pipeline/factory_spec.rb | 37 ++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 lib/gitlab/ci/status/pipeline/factory.rb create mode 100644 spec/lib/gitlab/ci/status/pipeline/factory_spec.rb diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb new file mode 100644 index 00000000000..71d27bf7cf5 --- /dev/null +++ b/lib/gitlab/ci/status/pipeline/factory.rb @@ -0,0 +1,39 @@ +module Gitlab + module Ci + module Status + module Pipeline + class Factory + EXTENDED_STATUSES = [Pipeline::SuccessWithWarnings] + + def initialize(pipeline) + @pipeline = pipeline + @status = pipeline.status || :created + end + + def fabricate! + if extended_status + extended_status.new(core_status) + else + core_status + end + end + + private + + def core_status + Gitlab::Ci::Status + .const_get(@status.capitalize) + .new(@pipeline) + .extend(Status::Pipeline::Common) + end + + def extended_status + @extended ||= EXTENDED_STATUSES.find do |status| + status.matches?(@pipeline) + end + end + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb new file mode 100644 index 00000000000..9f251735067 --- /dev/null +++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Pipeline::Factory do + subject do + described_class.new(pipeline) + end + + context 'when pipeline has a core status' do + HasStatus::AVAILABLE_STATUSES.each do |core_status| + context "when core status is #{core_status}" do + let(:pipeline) do + create(:ci_pipeline, status: core_status) + end + + it "fabricates a core status #{core_status}" do + expect(subject.fabricate!) + .to be_a Gitlab::Ci::Status.const_get(core_status.capitalize) + end + end + end + end + + context 'when pipeline has warnings' do + let(:pipeline) do + create(:ci_pipeline, status: :success) + end + + before do + create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline) + end + + it 'fabricates extended "success with warnings" status' do + expect(subject.fabricate!) + .to be_a Gitlab::Ci::Status::Pipeline::SuccessWithWarnings + end + end +end -- cgit v1.2.1 From 4f5c020a3548094ae4fc1a6a55de0aaa2f4431fe Mon Sep 17 00:00:00 2001 From: Semyon Pupkov <mail@semyonpupkov.com> Date: Mon, 5 Dec 2016 16:21:25 +0500 Subject: Use pry-byebug instead byebug https://github.com/deivid-rodriguez/pry-byebug --- Gemfile | 2 +- Gemfile.lock | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 83edf420798..f783ba07ba4 100644 --- a/Gemfile +++ b/Gemfile @@ -264,7 +264,7 @@ group :development do end group :development, :test do - gem 'byebug', '~> 8.2.1', platform: :mri + gem 'pry-byebug', '~> 3.4.1', platform: :mri gem 'pry-rails', '~> 0.3.4' gem 'awesome_print', '~> 1.2.0', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 5a14ed6fede..c614ae19ae1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -91,7 +91,7 @@ GEM bundler-audit (0.5.0) bundler (~> 1.2) thor (~> 0.18) - byebug (8.2.1) + byebug (9.0.6) capybara (2.6.2) addressable mime-types (>= 1.16) @@ -483,6 +483,9 @@ GEM coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) + pry-byebug (3.4.1) + byebug (~> 9.0) + pry (~> 0.10) pry-rails (0.3.4) pry (>= 0.9.10) pyu-ruby-sasl (0.0.3.3) @@ -796,7 +799,6 @@ DEPENDENCIES browser (~> 2.2) bullet (~> 5.2.0) bundler-audit (~> 0.5.0) - byebug (~> 8.2.1) capybara (~> 2.6.2) capybara-screenshot (~> 1.0.0) carrierwave (~> 0.10.0) @@ -891,6 +893,7 @@ DEPENDENCIES pg (~> 0.18.2) poltergeist (~> 1.9.0) premailer-rails (~> 1.9.0) + pry-byebug (~> 3.4.1) pry-rails (~> 0.3.4) rack-attack (~> 4.4.1) rack-cors (~> 0.4.0) -- cgit v1.2.1 From b2ab11a91785ffe0316e38687837e165fad3eb65 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 5 Dec 2016 13:10:20 +0100 Subject: Extend tests for pipeline status factory --- spec/lib/gitlab/ci/status/pipeline/factory_spec.rb | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb index 9f251735067..543dae0640d 100644 --- a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb @@ -5,6 +5,10 @@ describe Gitlab::Ci::Status::Pipeline::Factory do described_class.new(pipeline) end + let(:status) do + subject.fabricate! + end + context 'when pipeline has a core status' do HasStatus::AVAILABLE_STATUSES.each do |core_status| context "when core status is #{core_status}" do @@ -13,8 +17,13 @@ describe Gitlab::Ci::Status::Pipeline::Factory do end it "fabricates a core status #{core_status}" do - expect(subject.fabricate!) - .to be_a Gitlab::Ci::Status.const_get(core_status.capitalize) + expect(status).to be_a( + Gitlab::Ci::Status.const_get(core_status.capitalize)) + end + + it 'extends core status with common pipeline methods' do + expect(status).to have_details + expect(status.details_path).to include "pipelines/#{pipeline.id}" end end end @@ -30,8 +39,12 @@ describe Gitlab::Ci::Status::Pipeline::Factory do end it 'fabricates extended "success with warnings" status' do - expect(subject.fabricate!) + expect(status) .to be_a Gitlab::Ci::Status::Pipeline::SuccessWithWarnings end + + it 'extends core status with common pipeline methods' do + expect(status).to have_details + end end end -- cgit v1.2.1 From 5e3cfe2f091a1803aa7d08238232aea846ebfb5a Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 5 Dec 2016 13:23:58 +0100 Subject: Expose pipeline detailed status using status factory --- app/models/ci/pipeline.rb | 4 +++ spec/models/ci/pipeline_spec.rb | 70 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index fabbf97d4db..caf6908505e 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -320,6 +320,10 @@ module Ci .select { |merge_request| merge_request.head_pipeline.try(:id) == self.id } end + def detailed_status + Gitlab::Ci::Status::Pipeline::Factory.new(self).fabricate! + end + private def pipeline_data diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 0d2b4920835..3f93d9ddf19 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -404,6 +404,76 @@ describe Ci::Pipeline, models: true do end end + describe '#detailed_status' do + context 'when pipeline is created' do + let(:pipeline) { create(:ci_pipeline, status: :created) } + + it 'returns detailed status for created pipeline' do + expect(pipeline.detailed_status.text).to eq 'created' + end + end + + context 'when pipeline is pending' do + let(:pipeline) { create(:ci_pipeline, status: :pending) } + + it 'returns detailed status for pending pipeline' do + expect(pipeline.detailed_status.text).to eq 'pending' + end + end + + context 'when pipeline is running' do + let(:pipeline) { create(:ci_pipeline, status: :running) } + + it 'returns detailed status for running pipeline' do + expect(pipeline.detailed_status.text).to eq 'running' + end + end + + context 'when pipeline is successful' do + let(:pipeline) { create(:ci_pipeline, status: :success) } + + it 'returns detailed status for successful pipeline' do + expect(pipeline.detailed_status.text).to eq 'passed' + end + end + + context 'when pipeline is failed' do + let(:pipeline) { create(:ci_pipeline, status: :failed) } + + it 'returns detailed status for failed pipeline' do + expect(pipeline.detailed_status.text).to eq 'failed' + end + end + + context 'when pipeline is canceled' do + let(:pipeline) { create(:ci_pipeline, status: :canceled) } + + it 'returns detailed status for canceled pipeline' do + expect(pipeline.detailed_status.text).to eq 'canceled' + end + end + + context 'when pipeline is skipped' do + let(:pipeline) { create(:ci_pipeline, status: :skipped) } + + it 'returns detailed status for skipped pipeline' do + expect(pipeline.detailed_status.text).to eq 'skipped' + end + end + + context 'when pipeline is successful but with warnings' do + let(:pipeline) { create(:ci_pipeline, status: :success) } + + before do + create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline) + end + + it 'retruns detailed status for successful pipeline with warnings' do + expect(pipeline.detailed_status.label).to eq 'passed with warnings' + end + end + end + describe '#cancelable?' do %i[created running pending].each do |status0| context "when there is a build #{status0}" do -- cgit v1.2.1 From 3ce2ba1afb1dad707e1e3ac2bb5ff856d06f621e Mon Sep 17 00:00:00 2001 From: basyura <basyura@gmail.com> Date: Fri, 25 Nov 2016 23:09:50 +0900 Subject: fix display hook error message --- app/assets/javascripts/merge_request_widget.js.es6 | 2 +- changelogs/unreleased/issue_24020.yml | 4 ++++ spec/javascripts/merge_request_widget_spec.js | 12 ++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/issue_24020.yml diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index a55fe9df0b3..d9495e50388 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -101,7 +101,7 @@ urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : ''; return window.location.href = window.location.pathname + urlSuffix; } else if (data.merge_error) { - return this.$widgetBody.html("<h4>" + data.merge_error + "</h4>"); + return _this.$widgetBody.html("<h4>" + data.merge_error + "</h4>"); } else { callback = function() { return merge_request_widget.mergeInProgress(deleteSourceBranch); diff --git a/changelogs/unreleased/issue_24020.yml b/changelogs/unreleased/issue_24020.yml new file mode 100644 index 00000000000..87310b75296 --- /dev/null +++ b/changelogs/unreleased/issue_24020.yml @@ -0,0 +1,4 @@ +--- +title: "fix display hook error message" +merge_request: 7775 +author: basyura diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js index 62890f1ca96..6f91529db00 100644 --- a/spec/javascripts/merge_request_widget_spec.js +++ b/spec/javascripts/merge_request_widget_spec.js @@ -106,6 +106,18 @@ }); }); + describe('mergeInProgress', function() { + it('should display error with h4 tag', function() { + spyOn(this.class.$widgetBody, 'html').and.callFake(function(html) { + expect(html).toBe('<h4>Sorry, something went wrong.</h4>'); + }); + spyOn($, 'ajax').and.callFake(function(e) { + e.success({ merge_error: 'Sorry, something went wrong.' }); + }); + this.class.mergeInProgress(null); + }); + }); + return describe('getCIStatus', function() { beforeEach(function() { this.ciStatusData = { -- cgit v1.2.1 From ed4ec0dab7df042d53696ad118c9f0a829990802 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 5 Dec 2016 14:19:12 +0100 Subject: Add support for detailed status to status helpers --- app/helpers/ci_status_helper.rb | 53 +++++++++++++--------- .../projects/ci/pipelines/_pipeline.html.haml | 5 +- app/views/projects/pipelines/_info.html.haml | 2 +- 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index abcf84b4d15..ff507765255 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -6,7 +6,8 @@ module CiStatusHelper def ci_status_with_icon(status, target = nil) content = ci_icon_for_status(status) + ci_label_for_status(status) - klass = "ci-status ci-#{status}" + klass = "ci-status ci-#{status}" # TODO, add support for detailed status + if target link_to content, target, class: klass else @@ -15,11 +16,13 @@ module CiStatusHelper end def ci_label_for_status(status) + if detailed_status?(status) + return status.label + end + case status when 'success' 'passed' - when 'success_with_warnings' - 'passed with warnings' else status end @@ -32,25 +35,27 @@ module CiStatusHelper def ci_icon_for_status(status) icon_name = - case status - when 'success' - 'icon_status_success' - when 'success_with_warnings' - 'icon_status_warning' - when 'failed' - 'icon_status_failed' - when 'pending' - 'icon_status_pending' - when 'running' - 'icon_status_running' - when 'play' - 'icon_play' - when 'created' - 'icon_status_created' - when 'skipped' - 'icon_status_skipped' + if detailed_status?(status) + status.icon else - 'icon_status_canceled' + case status + when 'success' + 'icon_status_success' + when 'failed' + 'icon_status_failed' + when 'pending' + 'icon_status_pending' + when 'running' + 'icon_status_running' + when 'play' + 'icon_play' + when 'created' + 'icon_status_created' + when 'skipped' + 'icon_status_skipped' + else + 'icon_status_canceled' + end end custom_icon(icon_name) @@ -94,4 +99,10 @@ module CiStatusHelper class: klass, title: title, data: data end end + + def detailed_status?(status) + status.respond_to?(:text) && + status.respond_to?(:label) && + status.respond_to?(:icon) + end end diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 4c7b14a04db..00a30870dad 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -1,12 +1,13 @@ - status = pipeline.status +- detailed_status = pipeline.detailed_status - show_commit = local_assigns.fetch(:show_commit, true) - show_branch = local_assigns.fetch(:show_branch, true) %tr.commit %td.commit-link = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "ci-status ci-#{status}" do - = ci_icon_for_status(status) - = ci_label_for_status(status) + = ci_icon_for_status(detailed_status) + = ci_label_for_status(detailed_status) %td = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 095bd254d6b..229bdfb0e8d 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -1,6 +1,6 @@ .page-content-header .header-main-content - = ci_status_with_icon(@pipeline.status) + = ci_status_with_icon(@pipeline.detailed_status) %strong Pipeline ##{@commit.pipelines.last.id} triggered #{time_ago_with_tooltip(@commit.authored_date)} by = author_avatar(@commit, size: 24) -- cgit v1.2.1 From c1f3cbb5498574bcf686347a774c1fef88131974 Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Mon, 5 Dec 2016 14:37:44 +0000 Subject: NIGNX -> Nginx This shouldn't be in all caps, and it should be spelled correctly! --- doc/integration/shibboleth.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md index eb9bbb67e7d..696c1011eeb 100644 --- a/doc/integration/shibboleth.md +++ b/doc/integration/shibboleth.md @@ -2,7 +2,7 @@ This documentation is for enabling shibboleth with omnibus-gitlab package. -In order to enable Shibboleth support in gitlab we need to use Apache instead of Nginx (It may be possible to use Nginx, however this is difficult to configure using the bundled NIGNX provided in the omnibus-gitlab package). Apache uses mod_shib2 module for shibboleth authentication and can pass attributes as headers to omniauth-shibboleth provider. +In order to enable Shibboleth support in gitlab we need to use Apache instead of Nginx (It may be possible to use Nginx, however this is difficult to configure using the bundled Nginx provided in the omnibus-gitlab package). Apache uses mod_shib2 module for shibboleth authentication and can pass attributes as headers to omniauth-shibboleth provider. To enable the Shibboleth OmniAuth provider you must: -- cgit v1.2.1 From c73a5d596f8b239a8f43d9825d893b96a2f7457a Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Mon, 5 Dec 2016 09:26:22 -0600 Subject: Truncate fix, remove unneeded admin-specific css --- app/assets/stylesheets/framework/blocks.scss | 5 ---- app/assets/stylesheets/framework/common.scss | 10 ++++++- app/assets/stylesheets/framework/wells.scss | 13 +++++++++ app/assets/stylesheets/pages/admin.scss | 42 ---------------------------- app/views/admin/dashboard/index.html.haml | 12 ++++---- 5 files changed, 28 insertions(+), 54 deletions(-) diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 57db5eaa2b3..fbcb2659585 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -1,8 +1,3 @@ -.light-well { - background-color: $background-color; - padding: 15px; -} - .centered-light-block { text-align: center; color: $gl-gray; diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 16646e33a4b..b662282aa1d 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -379,7 +379,9 @@ table { border-top: 1px solid $border-color; } -.hide-bottom-border { border-bottom: none !important; } +.hide-bottom-border { + border-bottom: none !important; +} .gl-accessibility { &:focus { @@ -396,3 +398,9 @@ table { z-index: 1; } } + +.str-truncated { + &-60 { + @include str-truncated(60%); + } +} diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss index 192939f4527..f2860dfe84d 100644 --- a/app/assets/stylesheets/framework/wells.scss +++ b/app/assets/stylesheets/framework/wells.scss @@ -43,3 +43,16 @@ background-color: $well-expand-item; } } + +.light-well { + background-color: $background-color; + padding: 15px; +} + +.well-centered { + h1 { + font-weight: normal; + text-align: center; + font-size: 48px; + } +} diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss index 44eac21b143..dbf9db39651 100644 --- a/app/assets/stylesheets/pages/admin.scss +++ b/app/assets/stylesheets/pages/admin.scss @@ -2,48 +2,6 @@ * Admin area * */ -.admin-dashboard { - .data { - a { - h1 { - line-height: 48px; - font-size: 48px; - padding: 20px; - text-align: center; - font-weight: normal; - } - } - } - - .str-truncated { - max-width: 60%; - } -} - -.admin-filter form { - .select2-container { - width: 100%; - } - - .controls { - margin-left: 130px; - } - - .form-actions { - padding-left: 130px; - background: $white-light; - } - - .visibility-levels { - .controls { - margin-bottom: 9px; - } - - i { - color: inherit; - } - } -} .broadcast-messages { .message { diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 1db2150f336..e51f4ac1d93 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -113,7 +113,7 @@ %hr .row .col-sm-4 - .light-well + .light-well.well-centered %h4 Projects .data = link_to admin_namespaces_projects_path do @@ -121,7 +121,7 @@ %hr = link_to('New Project', new_project_path, class: "btn btn-new") .col-sm-4 - .light-well + .light-well.well-centered %h4 Users .data = link_to admin_users_path do @@ -129,7 +129,7 @@ %hr = link_to 'New User', new_admin_user_path, class: "btn btn-new" .col-sm-4 - .light-well + .light-well.well-centered %h4 Groups .data = link_to admin_groups_path do @@ -143,7 +143,7 @@ %hr - @projects.each do |project| %p - = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated' + = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60' %span.light.pull-right #{time_ago_with_tooltip(project.created_at)} @@ -152,7 +152,7 @@ %hr - @users.each do |user| %p - = link_to [:admin, user], class: 'str-truncated' do + = link_to [:admin, user], class: 'str-truncated-60' do = user.name %span.light.pull-right #{time_ago_with_tooltip(user.created_at)} @@ -162,7 +162,7 @@ %hr - @groups.each do |group| %p - = link_to [:admin, group], class: 'str-truncated' do + = link_to [:admin, group], class: 'str-truncated-60' do = group.name %span.light.pull-right #{time_ago_with_tooltip(group.created_at)} -- cgit v1.2.1 From b93a7dc101d15b0a31aa10fd3335291e1348e5ca Mon Sep 17 00:00:00 2001 From: PotHix <pothix@pothix.com> Date: Fri, 2 Dec 2016 20:13:11 -0200 Subject: Add excoveralls example There was no example for Elixir in gitlab-ci examples, this commit adds the missing example. excoveralls repository was pointing to an old example that doesn't exist anymore. --- app/views/projects/pipelines_settings/show.html.haml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/show.html.haml index 96221a20502..1f698558bce 100644 --- a/app/views/projects/pipelines_settings/show.html.haml +++ b/app/views/projects/pipelines_settings/show.html.haml @@ -86,6 +86,9 @@ %li tap --coverage-report=text-summary (NodeJS) - %code ^Statements\s*:\s*([^%]+) + %li + excoveralls (Elixir) - + %code \[TOTAL\]\s+(\d+\.\d+)% = f.submit 'Save changes', class: "btn btn-save" -- cgit v1.2.1 From 9a1c8aa34239f4a6c40df2f75225ef058798ee98 Mon Sep 17 00:00:00 2001 From: Ryan Harris <harrisryan1@gmail.com> Date: Fri, 2 Dec 2016 09:47:19 -0500 Subject: Fixed MR widget content wrapping for XS viewports Update merge_requests.scss Update merge_requests.scss to use padding shorthand Removed padding from Accept Merge Request button --- app/assets/stylesheets/pages/merge_requests.scss | 5 +++++ changelogs/unreleased/25202-fix-mr-widget-content-wrapping.yml | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 changelogs/unreleased/25202-fix-mr-widget-content-wrapping.yml diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index da1187af41c..740881487c3 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -129,6 +129,11 @@ margin-bottom: 4px; } + .btn-grouped { + float: none; + margin-right: 0; + } + .accept-action { width: 100%; text-align: center; diff --git a/changelogs/unreleased/25202-fix-mr-widget-content-wrapping.yml b/changelogs/unreleased/25202-fix-mr-widget-content-wrapping.yml new file mode 100644 index 00000000000..7afc794866b --- /dev/null +++ b/changelogs/unreleased/25202-fix-mr-widget-content-wrapping.yml @@ -0,0 +1,5 @@ +--- +title: Centered Accept Merge Request button within MR widget and added padding for + viewports smaller than 768px +merge_request: +author: Ryan Harris -- cgit v1.2.1 From cae90506fb3bdf8412d241a21a1f0fe2d96393b6 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Mon, 5 Dec 2016 16:24:56 +0000 Subject: Fixed if statement to use global group & project variables --- app/views/shared/members/_member.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index aa5b39151e6..e67f7d5a352 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -45,7 +45,7 @@ = time_ago_with_tooltip(member.created_at) - if show_roles .controls.member-controls - - if show_controls && (member.respond_to?(:group) && @members) || (member.respond_to?(:project) && @project_members) + - if show_controls && (member.respond_to?(:group) && @group) || (member.respond_to?(:project) && @project) - if user != current_user = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{member.id}", disabled: !can_admin_member -- cgit v1.2.1 From 4d26ab28a955885cfe8ae08917395cc7fc252ebf Mon Sep 17 00:00:00 2001 From: Jacopo <beschi.jacopo@gmail.com> Date: Sun, 27 Nov 2016 11:33:15 +0100 Subject: Fix Archived project merge requests add to group's Merge Requests counter This is done by: - Extending the IssuableFinder adding the non_archived option to the params - Overriding the #filter_params in the MergeRequestsAction - Passing the non_archived param in the nav/_group.html.haml navbar partial from the groups/merge_requests.html.haml --- app/controllers/concerns/merge_requests_action.rb | 7 ++++- app/finders/issuable_finder.rb | 6 +++++ app/finders/merge_requests_finder.rb | 1 + app/views/layouts/nav/_group.html.haml | 2 +- app/views/shared/issuable/_nav.html.haml | 10 ++++---- .../24733-archived-project-merge-request-count.yml | 4 +++ spec/features/groups/merge_requests_spec.rb | 30 +++++++++++++++++++++- spec/finders/merge_requests_finder_spec.rb | 11 +++++++- 8 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 changelogs/unreleased/24733-archived-project-merge-request-count.yml diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb index 6546a07b41c..fdb05bb3228 100644 --- a/app/controllers/concerns/merge_requests_action.rb +++ b/app/controllers/concerns/merge_requests_action.rb @@ -6,7 +6,12 @@ module MergeRequestsAction @label = merge_requests_finder.labels.first @merge_requests = merge_requests_collection - .non_archived .page(params[:page]) end + + private + + def filter_params + super.merge(non_archived: true) + end end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 001c83ccb4b..7cf75cd6fc7 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -15,6 +15,7 @@ # search: string # label_name: string # sort: string +# non_archived: boolean # class IssuableFinder NONE = '0' @@ -38,6 +39,7 @@ class IssuableFinder items = by_author(items) items = by_label(items) items = by_due_date(items) + items = by_non_archived(items) sort(items) end @@ -353,6 +355,10 @@ class IssuableFinder end end + def by_non_archived(items) + params[:non_archived].present? ? items.non_archived : items + end + def current_user_related? params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' end diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index 3b254e7d9d5..8b82255445e 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -14,6 +14,7 @@ # search: string # label_name: string # sort: string +# non_archived: boolean # class MergeRequestsFinder < IssuableFinder def klass diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index f7edb47b666..f3539fd372d 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -31,7 +31,7 @@ = link_to merge_requests_group_path(@group), title: 'Merge Requests' do %span Merge Requests - - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened').execute + - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute %span.badge.count= number_with_delimiter(merge_requests.count) = nav_link(controller: [:group_members]) do = link_to group_group_members_path(@group), title: 'Members' do diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index 5527a2f889a..0af92b59584 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -4,22 +4,22 @@ %ul.nav-links.issues-state-filters %li{class: ("active" if params[:state] == 'opened')} - = link_to page_filter_path(state: 'opened', label: true), title: "Filter by #{page_context_word} that are currently opened." do + = link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened." do #{issuables_state_counter_text(type, :opened)} - if type == :merge_requests %li{class: ("active" if params[:state] == 'merged')} - = link_to page_filter_path(state: 'merged', label: true), title: 'Filter by merge requests that are currently merged.' do + = link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.' do #{issuables_state_counter_text(type, :merged)} %li{class: ("active" if params[:state] == 'closed')} - = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by merge requests that are currently closed and unmerged.' do + = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.' do #{issuables_state_counter_text(type, :closed)} - else %li{class: ("active" if params[:state] == 'closed')} - = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by issues that are currently closed.' do + = link_to page_filter_path(state: 'closed', label: true), id: 'state-all', title: 'Filter by issues that are currently closed.' do #{issuables_state_counter_text(type, :closed)} %li{class: ("active" if params[:state] == 'all')} - = link_to page_filter_path(state: 'all', label: true), title: "Show all #{page_context_word}." do + = link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}." do #{issuables_state_counter_text(type, :all)} diff --git a/changelogs/unreleased/24733-archived-project-merge-request-count.yml b/changelogs/unreleased/24733-archived-project-merge-request-count.yml new file mode 100644 index 00000000000..2bc7e91825a --- /dev/null +++ b/changelogs/unreleased/24733-archived-project-merge-request-count.yml @@ -0,0 +1,4 @@ +--- +title: Fix Archived project merge requests add to group's Merge Requests +merge_request: 7790 +author: Jacopo Beschi @jacopo-beschi diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb index a2791b57544..30b80aa82b0 100644 --- a/spec/features/groups/merge_requests_spec.rb +++ b/spec/features/groups/merge_requests_spec.rb @@ -2,7 +2,35 @@ require 'spec_helper' feature 'Group merge requests page', feature: true do let(:path) { merge_requests_group_path(group) } - let(:issuable) { create(:merge_request, source_project: project, target_project: project, title: "this is my created issuable")} + let(:issuable) { create(:merge_request, source_project: project, target_project: project, title: 'this is my created issuable') } include_examples 'project features apply to issuables', MergeRequest + + context 'archived issuable' do + let(:project_archived) { create(:project, group: group, merge_requests_access_level: ProjectFeature::ENABLED, archived: true) } + let(:issuable_archived) { create(:merge_request, source_project: project_archived, target_project: project_archived, title: 'issuable of an archived project') } + let(:access_level) { ProjectFeature::ENABLED } + let(:user) { user_in_group } + + before do + issuable_archived + visit path + end + + it 'hides archived merge requests' do + expect(page).to have_content(issuable.title) + expect(page).not_to have_content(issuable_archived.title) + end + + it 'ignores archived merge request count badges in navbar' do + expect( page.find('[title="Merge Requests"] span.badge.count').text).to eq("1") + end + + it 'ignores archived merge request count badges in state-filters' do + expect(page.find('#state-opened span.badge').text).to eq("1") + expect(page.find('#state-merged span.badge').text).to eq("0") + expect(page.find('#state-closed span.badge').text).to eq("0") + expect(page.find('#state-all span.badge').text).to eq("1") + end + end end diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 535aabfc18d..88361e27102 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -6,14 +6,17 @@ describe MergeRequestsFinder do let(:project1) { create(:project) } let(:project2) { create(:project, forked_from_project: project1) } + let(:project3) { create(:project, forked_from_project: project1, archived: true) } let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) } let!(:merge_request2) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1, state: 'closed') } let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2) } + let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3) } before do project1.team << [user, :master] project2.team << [user, :developer] + project3.team << [user, :developer] project2.team << [user2, :developer] end @@ -21,7 +24,7 @@ describe MergeRequestsFinder do it 'filters by scope' do params = { scope: 'authored', state: 'opened' } merge_requests = MergeRequestsFinder.new(user, params).execute - expect(merge_requests.size).to eq(2) + expect(merge_requests.size).to eq(3) end it 'filters by project' do @@ -29,5 +32,11 @@ describe MergeRequestsFinder do merge_requests = MergeRequestsFinder.new(user, params).execute expect(merge_requests.size).to eq(1) end + + it 'filters by non_archived' do + params = { non_archived: true } + merge_requests = MergeRequestsFinder.new(user, params).execute + expect(merge_requests.size).to eq(3) + end end end -- cgit v1.2.1 From a219158e77b70bdae43b707dedc7b2bc4f930280 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Mon, 5 Dec 2016 09:59:30 -0600 Subject: Remove admin.scss --- app/assets/stylesheets/framework.scss | 1 + app/assets/stylesheets/framework/blocks.scss | 4 + .../stylesheets/framework/broadcast-messages.scss | 21 ++++ app/assets/stylesheets/framework/common.scss | 4 + app/assets/stylesheets/framework/lists.scss | 39 ++++++- app/assets/stylesheets/framework/tables.scss | 17 +++ app/assets/stylesheets/pages/admin.scss | 126 --------------------- app/assets/stylesheets/pages/pipelines.scss | 6 + app/views/admin/abuse_reports/index.html.haml | 10 +- app/views/admin/users/_user.html.haml | 8 +- app/views/admin/users/index.html.haml | 2 +- 11 files changed, 99 insertions(+), 139 deletions(-) create mode 100644 app/assets/stylesheets/framework/broadcast-messages.scss delete mode 100644 app/assets/stylesheets/pages/admin.scss diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 4aaff7d04f1..694fcd4b59d 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -40,3 +40,4 @@ @import "framework/blank"; @import "framework/wells.scss"; @import "framework/page-header.scss"; +@import "framework/broadcast-messages"; diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index fbcb2659585..95c02499271 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -269,6 +269,10 @@ } } + .emoji-icon { + display: inline-block; + } + @media(max-width: $screen-xs-max) { margin-top: 50px; text-align: center; diff --git a/app/assets/stylesheets/framework/broadcast-messages.scss b/app/assets/stylesheets/framework/broadcast-messages.scss new file mode 100644 index 00000000000..9b54fb94cdc --- /dev/null +++ b/app/assets/stylesheets/framework/broadcast-messages.scss @@ -0,0 +1,21 @@ +.broadcast-message { + @extend .alert-warning; + padding: 10px; + text-align: center; + + div, + p { + display: inline; + margin: 0; + + a { + color: inherit; + text-decoration: underline; + } + } +} + +.broadcast-message-preview { + @extend .broadcast-message; + margin-bottom: 20px; +} diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index b662282aa1d..600bf17259b 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -403,4 +403,8 @@ table { &-60 { @include str-truncated(60%); } + + &-100 { + @include str-truncated(100%); + } } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index db8677433bb..ed4b60faf92 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -106,13 +106,13 @@ ul.task-list { } } +// Generic content list ul.content-list { @include basic-list; - margin: 0; padding: 0; - > li { + li { border-color: $table-border-color; font-size: $list-font-size; color: $list-text-color; @@ -193,6 +193,41 @@ ul.content-list { } } +// Content list using flexbox +.flex-list { + .flex-row { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + white-space: nowrap; + } + + .row-main-content { + flex: 1 1 auto; + overflow: hidden; + padding-right: 8px; + } + + .row-title { + font-weight: 600; + } + + .row-second-line { + display: block; + } + + .dropdown { + .btn-block { + margin-bottom: 0; + line-height: inherit; + } + } + + .label-default { + color: $btn-transparent-color; + } +} + .panel > .content-list > li { padding: $gl-padding-top $gl-padding; diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index a5f36c177fc..5d0ca63ea08 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -34,6 +34,10 @@ table { background-color: $background-color; font-weight: normal; border-bottom: none; + + &.wide { + width: 55%; + } } td { @@ -42,3 +46,16 @@ table { } } } + +.responsive-table { + @media (max-width: $screen-sm-max) { + th { + width: 100%; + } + + td { + width: 100%; + float: left; + } + } +} diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss deleted file mode 100644 index dbf9db39651..00000000000 --- a/app/assets/stylesheets/pages/admin.scss +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Admin area - * - */ - -.broadcast-messages { - .message { - line-height: 2; - } -} - -.broadcast-message { - @extend .alert-warning; - padding: 10px; - text-align: center; - - > div, - p { - display: inline; - margin: 0; - - a { - color: inherit; - text-decoration: underline; - } - } -} - -.broadcast-message-preview { - @extend .broadcast-message; - margin-bottom: 20px; -} - -// Users List - -.users-list { - .user-row { - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - white-space: nowrap; - } - - .user-details { - flex: 1 1 auto; - overflow: hidden; - padding-right: 8px; - } - - .user-name { - display: inline-block; - font-weight: 600; - } - - .user-name, - .user-email { - overflow: hidden; - text-overflow: ellipsis; - } - - .dropdown { - .btn-block { - margin-bottom: 0; - line-height: inherit; - } - } - - .label-default { - color: $btn-transparent-color; - } -} - -.abuse-reports { - .table { - table-layout: fixed; - } - - .subheading { - padding-bottom: $gl-padding; - } - - .message { - word-wrap: break-word; - } - - .btn { - white-space: normal; - padding: $gl-btn-padding; - } - - th { - width: 15%; - - &.wide { - width: 55%; - } - } - - @media (max-width: $screen-sm-max) { - th { - width: 100%; - } - - td { - width: 100%; - float: left; - } - } - - .no-reports { - .emoji-icon { - margin-left: $btn-side-margin; - margin-top: 3px; - } - - span { - font-size: 18px; - } - } -} - -.admin-builds-table { - .ci-table td:last-child { - min-width: 120px; - } -} diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 0027d2caf22..08062b85504 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -280,6 +280,12 @@ } } +.admin-builds-table { + .ci-table td:last-child { + min-width: 120px; + } +} + // Pipeline visualization .toggle-pipeline-btn { diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml index 7bbc75db9ff..c4b748d0ab8 100644 --- a/app/views/admin/abuse_reports/index.html.haml +++ b/app/views/admin/abuse_reports/index.html.haml @@ -4,7 +4,7 @@ .abuse-reports - if @abuse_reports.present? .table-holder - %table.table + %table.table.responsive-table %thead.hidden-sm.hidden-xs %tr %th User @@ -13,8 +13,6 @@ %th Action = render @abuse_reports - else - .no-reports - %span.pull-left - There are no abuse reports! - .pull-left - = emoji_icon 'tada' + .empty-state + .text-center + %h4 There are no abuse reports! #{emoji_icon 'tada'} diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml index 4bf1c9cde3c..2d9588f9d27 100644 --- a/app/views/admin/users/_user.html.haml +++ b/app/views/admin/users/_user.html.haml @@ -1,8 +1,8 @@ -%li.user-row +%li.flex-row .user-avatar = image_tag avatar_icon(user), class: "avatar", alt: '' - .user-details - .user-name + .row-main-content + .user-name.row-title.str-truncated-100 = link_to user.name, [:admin, user] - if user.blocked? %span.label.label-danger blocked @@ -12,7 +12,7 @@ %span.label.label-default External - if user == current_user %span It's you! - .user-email + .row-second-line.str-truncated-100 = mail_to user.email, user.email .controls = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn' diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index d3038ae644f..4dc44225d49 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -68,7 +68,7 @@ %small.badge= number_with_delimiter(User.without_projects.count) .fade-right - %ul.users-list.content-list + %ul.flex-list.content-list - if @users.empty? %li .nothing-here-block No users found. -- cgit v1.2.1 From f18a3f8b5b7120d5a4b7ccf5fd8fc930f4596e13 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Mon, 5 Dec 2016 12:26:50 -0600 Subject: Remove unused errors css --- app/assets/stylesheets/pages/errors.scss | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 app/assets/stylesheets/pages/errors.scss diff --git a/app/assets/stylesheets/pages/errors.scss b/app/assets/stylesheets/pages/errors.scss deleted file mode 100644 index 11309817d31..00000000000 --- a/app/assets/stylesheets/pages/errors.scss +++ /dev/null @@ -1,16 +0,0 @@ -.error-page { - max-width: 400px; - margin: 0 auto; - - h1, - h2, - h3 { - text-align: center; - } - - h1 { - font-size: 56px; - line-height: 100px; - font-weight: 300; - } -} -- cgit v1.2.1 From 00dc6b8bcbed021c1a2d8477d8a51e429014f7be Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Mon, 5 Dec 2016 12:31:36 -0600 Subject: Remove unused votes.scss --- app/assets/stylesheets/pages/votes.scss | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 app/assets/stylesheets/pages/votes.scss diff --git a/app/assets/stylesheets/pages/votes.scss b/app/assets/stylesheets/pages/votes.scss deleted file mode 100644 index dc9a7d71e8b..00000000000 --- a/app/assets/stylesheets/pages/votes.scss +++ /dev/null @@ -1,4 +0,0 @@ -.votes-inline { - display: inline-block; - margin: 0 8px; -} -- cgit v1.2.1 From 54221b5a3b9a2489f979944c77298c4adf004984 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Mon, 5 Dec 2016 20:34:11 +0200 Subject: Fix inline comment importing for 1:1 diff type --- .../representation/pull_request_comment.rb | 2 +- lib/gitlab/bitbucket_import/importer.rb | 26 ++++++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/lib/bitbucket/representation/pull_request_comment.rb b/lib/bitbucket/representation/pull_request_comment.rb index 94719edbf38..38090188919 100644 --- a/lib/bitbucket/representation/pull_request_comment.rb +++ b/lib/bitbucket/representation/pull_request_comment.rb @@ -14,7 +14,7 @@ module Bitbucket end def new_pos - inline.fetch('to', nil) || 1 + inline.fetch('to', nil) || old_pos || 1 end def parent_id diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 08705afcabb..6438c8a52e4 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -81,10 +81,10 @@ module Gitlab description: description, source_project: project, source_branch: pull_request.source_branch_name, - source_branch_sha: pull_request.source_branch_sha, + source_branch_sha: project.repository.rugged.lookup(pull_request.source_branch_sha).oid, target_project: project, target_branch: pull_request.target_branch_name, - target_branch_sha: pull_request.target_branch_sha, + target_branch_sha: project.repository.rugged.lookup(pull_request.target_branch_sha).oid, state: pull_request.state, author_id: gitlab_user_id(project, pull_request.author), assignee_id: nil, @@ -94,7 +94,7 @@ module Gitlab import_pull_request_comments(pull_request, merge_request) if merge_request.persisted? rescue ActiveRecord::RecordInvalid - Rails.log.error("Bitbucket importer ERROR in #{project.path_with_namespace}: Invalid pull request #{e.message}") + Rails.logger.error("Bitbucket importer ERROR in #{project.path_with_namespace}: Invalid pull request #{e.message}") end end end @@ -128,24 +128,36 @@ module Gitlab begin attributes = pull_request_comment_attributes(comment) attributes.merge!( - commit_id: pull_request.source_branch_sha, + position: build_position(merge_request, comment), line_code: line_code_map.fetch(comment.iid), - type: 'LegacyDiffNote') + type: 'DiffNote') merge_request.notes.create!(attributes) rescue ActiveRecord::RecordInvalid => e - Rails.log.error("Bitbucket importer ERROR in #{project.path_with_namespace}: Invalid pull request comment #{e.message}") + Rails.logger.error("Bitbucket importer ERROR in #{project.path_with_namespace}: Invalid pull request comment #{e.message}") nil end end end + def build_position(merge_request, pr_comment) + params = { + diff_refs: merge_request.diff_refs, + old_path: pr_comment.file_path, + new_path: pr_comment.file_path, + old_line: pr_comment.old_pos, + new_line: pr_comment.new_pos + } + + Gitlab::Diff::Position.new(params) + end + def import_standalone_pr_comments(pr_comments, merge_request) pr_comments.each do |comment| begin merge_request.notes.create!(pull_request_comment_attributes(comment)) rescue ActiveRecord::RecordInvalid => e - Rails.log.error("Bitbucket importer ERROR in #{project.path_with_namespace}: Invalid standalone pull request comment #{e.message}") + Rails.logger.error("Bitbucket importer ERROR in #{project.path_with_namespace}: Invalid standalone pull request comment #{e.message}") nil end end -- cgit v1.2.1 From 1123057ab792ac73b1611f4d3a9faf79369dd6da Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt <bob@vanlanduyt.co> Date: Wed, 26 Oct 2016 23:21:50 +0200 Subject: Feature: delegate all open discussions to Issue When a merge request can only be merged when all discussions are resolved. This feature allows to easily delegate those discussions to a new issue, while marking them as resolved in the merge request. The user is presented with a new issue, prepared with mentions of all unresolved discussions, including the first unresolved note of the discussion, time and link to the note. When the issue is created, the discussions in the merge request will get a system note directing the user to the newly created issue. --- app/controllers/projects/discussions_controller.rb | 4 +- app/controllers/projects/issues_controller.rb | 15 ++- app/models/discussion.rb | 4 + app/models/merge_request.rb | 8 ++ app/models/note.rb | 2 +- app/services/discussions/base_service.rb | 4 + app/services/discussions/resolve_service.rb | 24 ++++ app/services/issuable_base_service.rb | 5 +- app/services/issues/base_service.rb | 8 ++ app/services/issues/build_service.rb | 50 ++++++++ app/services/issues/create_service.rb | 14 ++- app/services/system_note_service.rb | 8 ++ .../widget/open/_unresolved_discussions.html.haml | 6 +- app/views/shared/issuable/_form.html.haml | 15 +++ changelogs/unreleased/23589-open-issue-for-mr.yml | 5 + doc/api/issues.md | 3 +- .../img/preview_issue_for_discussions.png | Bin 0 -> 178361 bytes .../merge_request_discussion_resolution.md | 21 +++- lib/api/issues.rb | 32 +++-- .../controllers/projects/issues_controller_spec.rb | 60 ++++++++++ ..._issue_for_discussions_in_merge_request_spec.rb | 76 ++++++++++++ spec/models/discussion_spec.rb | 9 ++ spec/models/merge_request_spec.rb | 40 +++++++ spec/requests/api/issues_spec.rb | 26 +++++ spec/services/discussions/resolve_service_spec.rb | 52 +++++++++ spec/services/issues/build_service_spec.rb | 130 +++++++++++++++++++++ spec/services/issues/create_service_spec.rb | 43 +++++++ spec/services/system_note_service_spec.rb | 28 +++++ 28 files changed, 670 insertions(+), 22 deletions(-) create mode 100644 app/services/discussions/base_service.rb create mode 100644 app/services/discussions/resolve_service.rb create mode 100644 app/services/issues/build_service.rb create mode 100644 changelogs/unreleased/23589-open-issue-for-mr.yml create mode 100644 doc/user/project/merge_requests/img/preview_issue_for_discussions.png create mode 100644 spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb create mode 100644 spec/services/discussions/resolve_service_spec.rb create mode 100644 spec/services/issues/build_service_spec.rb diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb index d174e1145a7..148e39630e3 100644 --- a/app/controllers/projects/discussions_controller.rb +++ b/app/controllers/projects/discussions_controller.rb @@ -5,9 +5,7 @@ class Projects::DiscussionsController < Projects::ApplicationController before_action :authorize_resolve_discussion! def resolve - discussion.resolve!(current_user) - - MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request) + Discussions::ResolveService.new(project, current_user, merge_request: merge_request).execute(discussion) render json: { resolved_by: discussion.resolved_by.try(:name), diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 4aea7bb62c4..4f66e01e0f7 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -46,8 +46,9 @@ class Projects::IssuesController < Projects::ApplicationController params[:issue] ||= ActionController::Parameters.new( assignee_id: "" ) + build_params = issue_params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions) + @issue = @noteable = Issues::BuildService.new(project, current_user, build_params).execute - @issue = @noteable = @project.issues.new(issue_params) respond_with(@issue) end @@ -75,7 +76,9 @@ class Projects::IssuesController < Projects::ApplicationController end def create - @issue = Issues::CreateService.new(project, current_user, issue_params.merge(request: request)).execute + extra_params = { request: request, + merge_request_for_resolving_discussions: merge_request_for_resolving_discussions } + @issue = Issues::CreateService.new(project, current_user, issue_params.merge(extra_params)).execute respond_to do |format| format.html do @@ -169,6 +172,14 @@ class Projects::IssuesController < Projects::ApplicationController alias_method :awardable, :issue alias_method :spammable, :issue + def merge_request_for_resolving_discussions + return unless merge_request_iid = params[:merge_request_for_resolving_discussions] + + @merge_request_for_resolving_discussions ||= MergeRequestsFinder.new(current_user, project_id: project.id). + execute. + find_by(iid: merge_request_iid) + end + def authorize_read_issue! return render_404 unless can?(current_user, :read_issue, @issue) end diff --git a/app/models/discussion.rb b/app/models/discussion.rb index 75a85563235..bbe813db823 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -88,6 +88,10 @@ class Discussion @first_note ||= @notes.first end + def first_note_to_resolve + @first_note_to_resolve ||= notes.detect(&:to_be_resolved?) + end + def last_note @last_note ||= @notes.last end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index bfb016df46d..a6fc9bb120d 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -480,6 +480,14 @@ class MergeRequest < ActiveRecord::Base @diff_discussions ||= self.notes.diff_notes.discussions end + def resolvable_discussions + @resolvable_discussions ||= diff_discussions.select(&:to_be_resolved?) + end + + def discussions_can_be_resolved_by?(user) + resolvable_discussions.all? { |discussion| discussion.can_resolve?(user) } + end + def find_diff_discussion(discussion_id) notes = self.notes.diff_notes.where(discussion_id: discussion_id).fresh.to_a return if notes.empty? diff --git a/app/models/note.rb b/app/models/note.rb index 5b50ca285c3..08bd08743ef 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -99,7 +99,7 @@ class Note < ActiveRecord::Base end def discussions - Discussion.for_notes(all) + Discussion.for_notes(fresh) end def grouped_diff_discussions diff --git a/app/services/discussions/base_service.rb b/app/services/discussions/base_service.rb new file mode 100644 index 00000000000..e4dfe6e71bb --- /dev/null +++ b/app/services/discussions/base_service.rb @@ -0,0 +1,4 @@ +module Discussions + class BaseService < ::BaseService + end +end diff --git a/app/services/discussions/resolve_service.rb b/app/services/discussions/resolve_service.rb new file mode 100644 index 00000000000..0437195f588 --- /dev/null +++ b/app/services/discussions/resolve_service.rb @@ -0,0 +1,24 @@ +module Discussions + class ResolveService < Discussions::BaseService + def execute(one_or_more_discussions) + Array(one_or_more_discussions).each { |discussion| resolve_discussion(discussion) } + end + + def resolve_discussion(discussion) + return unless discussion.can_resolve?(current_user) + + discussion.resolve!(current_user) + + MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request) + SystemNoteService.discussion_continued_in_issue(discussion, project, current_user, follow_up_issue) if follow_up_issue + end + + def merge_request + params[:merge_request] + end + + def follow_up_issue + params[:follow_up_issue] + end + end +end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index ce68e433ab8..b5f63cc5a1a 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -120,9 +120,10 @@ class IssuableBaseService < BaseService def merge_slash_commands_into_params!(issuable) description, command_params = SlashCommands::InterpretService.new(project, current_user). - execute(params[:description], issuable) + execute(params[:description], issuable) - params[:description] = description + # Avoid a description already set on an issuable to be overwritten by a nil + params[:description] = description if params.has_key?(:description) params.merge!(command_params) end diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 9ea3ce084ba..742e834df97 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -1,5 +1,13 @@ module Issues class BaseService < ::IssuableBaseService + attr_reader :merge_request_for_resolving_discussions + + def initialize(*args) + super + + @merge_request_for_resolving_discussions ||= params.delete(:merge_request_for_resolving_discussions) + end + def hook_data(issue, action) issue_data = issue.to_hook_data(current_user) issue_url = Gitlab::UrlBuilder.build(issue) diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb new file mode 100644 index 00000000000..a63982f60c8 --- /dev/null +++ b/app/services/issues/build_service.rb @@ -0,0 +1,50 @@ +module Issues + class BuildService < Issues::BaseService + def execute + @issue = project.issues.new(issue_params) + end + + def issue_params_with_info_from_merge_request + return {} unless merge_request_for_resolving_discussions + + { title: title_from_merge_request, description: description_from_merge_request } + end + + def title_from_merge_request + "Follow-up from \"#{merge_request_for_resolving_discussions.title}\"" + end + + def description_from_merge_request + if merge_request_for_resolving_discussions.resolvable_discussions.empty? + return "There are no unresolved discussions. "\ + "Review the conversation in #{merge_request_for_resolving_discussions.to_reference}" + end + + description = "The following discussions from #{merge_request_for_resolving_discussions.to_reference} should be addressed:" + [description, *items_for_discussions].join("\n\n") + end + + def items_for_discussions + merge_request_for_resolving_discussions.resolvable_discussions.map { |discussion| item_for_discussion(discussion) } + end + + def item_for_discussion(discussion) + first_note = discussion.first_note_to_resolve + other_note_count = discussion.notes.size - 1 + creation_time = first_note.created_at.to_s(:medium) + note_url = Gitlab::UrlBuilder.build(first_note) + + discussion_info = "- [ ] #{first_note.author.to_reference} commented in a discussion on [#{creation_time}](#{note_url}): " + discussion_info << " (+#{other_note_count} #{'comment'.pluralize(other_note_count)})" if other_note_count > 0 + + note_without_block_quotes = Banzai::Filter::BlockquoteFenceFilter.new(first_note.note).call + quote = ">>>\n#{note_without_block_quotes}\n>>>" + + [discussion_info, quote].join("\n\n") + end + + def issue_params + @issue_params ||= issue_params_with_info_from_merge_request.merge(params.slice(:title, :description)) + end + end +end diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index ea1690f3e38..d2eb46ac41b 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -4,7 +4,8 @@ module Issues @request = params.delete(:request) @api = params.delete(:api) - @issue = project.issues.new + issue_attributes = params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions) + @issue = BuildService.new(project, current_user, issue_attributes).execute create(@issue) end @@ -18,6 +19,17 @@ module Issues notification_service.new_issue(issuable, current_user) todo_service.new_issue(issuable, current_user) user_agent_detail_service.create + + if merge_request_for_resolving_discussions.try(:discussions_can_be_resolved_by?, current_user) + resolve_discussions_in_merge_request(issuable) + end + end + + def resolve_discussions_in_merge_request(issue) + Discussions::ResolveService.new(project, current_user, + merge_request: merge_request_for_resolving_discussions, + follow_up_issue: issue). + execute(merge_request_for_resolving_discussions.resolvable_discussions) end private diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 3cf6467804f..8b48d90f60b 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -163,6 +163,14 @@ module SystemNoteService create_note(noteable: merge_request, project: project, author: author, note: body) end + def discussion_continued_in_issue(discussion, project, author, issue) + body = "Added #{issue.to_reference} to continue this discussion" + note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body) + note_attributes[:type] = note_attributes.delete(:note_type) + + create_note(note_attributes) + end + # Called when the title of a Noteable is changed # # noteable - Noteable object that responds to `title` diff --git a/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml b/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml index 35d5677ee37..e094f97f3b6 100644 --- a/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml +++ b/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml @@ -3,4 +3,8 @@ This merge request has unresolved discussions %p - Please resolve these discussions to allow this merge request to be merged. \ No newline at end of file + Please resolve these discussions + - if @project.issues_enabled? && can?(current_user, :create_issue, @project) + or + = link_to "open an issue to resolve them later", new_namespace_project_issue_path(@project.namespace, @project, merge_request_for_resolving_discussions: @merge_request.iid) + to allow this merge request to be merged. diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 2f05093f435..bdb00bfa33c 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -42,6 +42,21 @@ = render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form +- if @merge_request_for_resolving_discussions + .form-group + .col-sm-10.col-sm-offset-2 + - if @merge_request_for_resolving_discussions.discussions_can_be_resolved_by?(current_user) + = icon('exclamation-triangle') + Creating this issue will mark all discussions in + = link_to @merge_request_for_resolving_discussions.to_reference, merge_request_path(@merge_request_for_resolving_discussions) + as resolved. + = hidden_field_tag 'merge_request_for_resolving_discussions', @merge_request_for_resolving_discussions.iid + - else + = icon('exclamation-triangle') + You can't automatically mark all discussions in + = link_to @merge_request_for_resolving_discussions.to_reference, merge_request_path(@merge_request_for_resolving_discussions) + as resolved. Ask someone with sufficient rights to resolve the them. + - is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?) .row-content-block{class: (is_footer ? "footer-block" : "middle-block")} - if issuable.new_record? diff --git a/changelogs/unreleased/23589-open-issue-for-mr.yml b/changelogs/unreleased/23589-open-issue-for-mr.yml new file mode 100644 index 00000000000..cea48b85254 --- /dev/null +++ b/changelogs/unreleased/23589-open-issue-for-mr.yml @@ -0,0 +1,5 @@ +--- +title: Resolve all discussions in a merge request by creating an issue collecting + them +merge_request: 7180 +author: Bob Van Landuyt diff --git a/doc/api/issues.md b/doc/api/issues.md index 16f8e32c82a..119125bcd3d 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -330,6 +330,7 @@ POST /projects/:id/issues | `labels` | string | no | Comma-separated label names for an issue | | `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) | | `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` | +| `merge_request_for_resolving_discussions` | integer | no | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. When passing a description or title, these values will take precedence over the default values. | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues?title=Issues%20with%20auth&labels=bug @@ -506,7 +507,7 @@ Example response: ## Subscribe to an issue -Subscribes the authenticated user to an issue to receive notifications. +Subscribes the authenticated user to an issue to receive notifications. If the user is already subscribed to the issue, the status code `304` is returned. diff --git a/doc/user/project/merge_requests/img/preview_issue_for_discussions.png b/doc/user/project/merge_requests/img/preview_issue_for_discussions.png new file mode 100644 index 00000000000..9fdd387676c Binary files /dev/null and b/doc/user/project/merge_requests/img/preview_issue_for_discussions.png differ diff --git a/doc/user/project/merge_requests/merge_request_discussion_resolution.md b/doc/user/project/merge_requests/merge_request_discussion_resolution.md index 285b1798ac5..f37f1ce4d21 100644 --- a/doc/user/project/merge_requests/merge_request_discussion_resolution.md +++ b/doc/user/project/merge_requests/merge_request_discussion_resolution.md @@ -37,7 +37,8 @@ resolved discussions tracker. > [Introduced][ce-7125] in GitLab 8.14. -You can prevent merge requests from being merged until all discussions are resolved. +You can prevent merge requests from being merged until all discussions are +resolved. Navigate to your project's settings page, select the **Only allow merge requests to be merged if all discussions are resolved** check @@ -50,8 +51,26 @@ are resolved. ![Only allow merge if all the discussions are resolved message](img/only_allow_merge_if_all_discussions_are_resolved_msg.png) +### Move all unresolved discussions in a merge request to an issue + +> [Introduced][ce-7180] (Currently on Backlog) + +To delegate unresolved discussions to a new issue you can click the link **open +an issue to resolve them later**. + +This will prepare an issue with content referring to the merge request and +discussions. + +![Issue mentioning discussions in a merge request](img/preview_issue_for_discussions.png) + +Hitting **Submit issue** will cause all discussions to be marked as resolved and +add a note referring to the newly created issue. + +You can now proceed to merge the merge request from the UI. + [ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022 [ce-7125]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7125 +[ce-7180]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7180 [resolve-discussion-button]: img/resolve_discussion_button.png [resolve-comment-button]: img/resolve_comment_button.png [discussion-view]: img/discussion_view.png diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 049b4fb214c..cfb7c45de8e 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -28,6 +28,14 @@ module API new_params end + + def merge_request_for_resolving_discussions + return unless merge_request_iid = params[:merge_request_for_resolving_discussions] + + @merge_request_for_resolving_discussions ||= MergeRequestsFinder.new(current_user, project_id: user_project.id). + execute. + find_by(iid: merge_request_iid) + end end resource :issues do @@ -151,24 +159,28 @@ module API # Create a new project issue # # Parameters: - # id (required) - The ID of a project - # title (required) - The title of an issue - # description (optional) - The description of an issue - # assignee_id (optional) - The ID of a user to assign issue - # milestone_id (optional) - The ID of a milestone to assign issue - # labels (optional) - The labels of an issue - # created_at (optional) - Date time string, ISO 8601 formatted - # due_date (optional) - Date time string in the format YEAR-MONTH-DAY - # confidential (optional) - Boolean parameter if the issue should be confidential + # id (required) - The ID of a project + # title (required) - The title of an issue + # description (optional) - The description of an issue + # assignee_id (optional) - The ID of a user to assign issue + # milestone_id (optional) - The ID of a milestone to assign issue + # labels (optional) - The labels of an issue + # created_at (optional) - Date time string, ISO 8601 formatted + # due_date (optional) - Date time string in the format YEAR-MONTH-DAY + # confidential (optional) - Boolean parameter if the issue should be confidential + # merge_request_for_resolving_discussions (optional) - The IID of a merge request for which to resolve discussions # Example Request: # POST /projects/:id/issues post ':id/issues' do required_attributes! [:title] - keys = [:title, :description, :assignee_id, :milestone_id, :due_date, :confidential, :labels] + keys = [:title, :description, :assignee_id, :milestone_id, :due_date, :confidential, :labels, :merge_request_for_resolving_discussions] keys << :created_at if current_user.admin? || user_project.owner == current_user attrs = attributes_for_keys(keys) + attrs[:labels] = params[:labels] if params[:labels] + attrs[:merge_request_for_resolving_discussions] = merge_request_for_resolving_discussions if params[:merge_request_for_resolving_discussions] + # Convert and filter out invalid confidential flags attrs['confidential'] = to_boolean(attrs['confidential']) attrs.delete('confidential') if attrs['confidential'].nil? diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 90419368f22..dbe5ddccbcf 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -55,6 +55,30 @@ describe Projects::IssuesController do end describe 'GET #new' do + context 'internal issue tracker' do + before do + sign_in(user) + project.team << [user, :developer] + end + + it 'builds a new issue' do + get :new, namespace_id: project.namespace.path, project_id: project + + expect(assigns(:issue)).to be_a_new(Issue) + end + + it 'fills in an issue for a merge request' do + project_with_repository = create(:project) + project_with_repository.team << [user, :developer] + mr = create(:merge_request_with_diff_notes, source_project: project_with_repository) + + get :new, namespace_id: project_with_repository.namespace.path, project_id: project_with_repository, merge_request_for_resolving_discussions: mr.iid + + expect(assigns(:issue).title).not_to be_empty + expect(assigns(:issue).description).not_to be_empty + end + end + context 'external issue tracker' do it 'redirects to the external issue tracker' do external = double(new_issue_path: 'https://example.com/issues/new') @@ -272,6 +296,42 @@ describe Projects::IssuesController do end describe 'POST #create' do + context 'resolving discussions in MergeRequest' do + let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first } + let(:merge_request) { discussion.noteable } + let(:project) { merge_request.source_project } + + before do + project.team << [user, :master] + sign_in user + end + + let(:merge_request_params) do + { merge_request_for_resolving_discussions: merge_request.iid } + end + + def post_issue(issue_params) + post :create, namespace_id: project.namespace.to_param, project_id: project.to_param, issue: issue_params, merge_request_for_resolving_discussions: merge_request.iid + end + + it 'creates an issue for the project' do + expect { post_issue({ title: 'Hello' }) }.to change { project.issues.reload.size }.by(1) + end + + it "doesn't overwrite given params" do + post_issue(description: 'Manually entered description') + + expect(assigns(:issue).description).to eq('Manually entered description') + end + + it 'resolves the discussion in the merge_request' do + post_issue(title: 'Hello') + discussion.first_note.reload + + expect(discussion.resolved?).to eq(true) + end + end + context 'Akismet is enabled' do before do allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true) diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb new file mode 100644 index 00000000000..762cab0c0e1 --- /dev/null +++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb @@ -0,0 +1,76 @@ +require 'rails_helper' + +feature 'Resolving all open discussions in a merge request from an issue', feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, only_allow_merge_if_all_discussions_are_resolved: true) } + let(:merge_request) { create(:merge_request, source_project: project) } + let!(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request, noteable: merge_request, project: project)]).first } + + before do + project.team << [user, :master] + login_as user + end + + context 'with the internal tracker disabled' do + before do + project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'does not show a link to create a new issue' do + expect(page).not_to have_link 'open an issue to resolve them later' + end + end + + context 'merge request has discussions that need to be resolved' do + before do + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'shows a warning that the merge request contains unresolved discussions' do + expect(page).to have_content 'This merge request has unresolved discussions' + end + + it 'has a link to resolve all discussions by creating an issue' do + page.within '.mr-widget-body' do + expect(page).to have_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_for_resolving_discussions: merge_request.iid) + end + end + + context 'creating an issue for discussions' do + before do + page.click_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_for_resolving_discussions: merge_request.iid) + end + + it 'shows an issue with the title filled in' do + title_field = page.find_field('issue[title]') + + expect(title_field.value).to include(merge_request.title) + end + + it 'has a mention of the discussion in the description' do + description_field = page.find_field('issue[description]') + + expect(description_field.value).to include(discussion.first_note.note) + end + + it 'has a hidden field for the merge request' do + merge_request_field = find('#merge_request_for_resolving_discussions', visible: false) + + expect(merge_request_field.value).to eq(merge_request.iid.to_s) + end + + it 'can create a new issue for the project' do + expect { click_button 'Submit issue' }.to change { project.issues.reload.size }.by(1) + end + + it 'resolves the discussion in the merge request' do + click_button 'Submit issue' + + discussion.first_note.reload + + expect(discussion.resolved?).to eq(true) + end + end + end +end diff --git a/spec/models/discussion_spec.rb b/spec/models/discussion_spec.rb index 2a67c60b978..bc32fadd391 100644 --- a/spec/models/discussion_spec.rb +++ b/spec/models/discussion_spec.rb @@ -521,6 +521,15 @@ describe Discussion, model: true do end end + describe "#first_note_to_resolve" do + it "returns the first not that still needs to be resolved" do + allow(first_note).to receive(:to_be_resolved?).and_return(false) + allow(second_note).to receive(:to_be_resolved?).and_return(true) + + expect(subject.first_note_to_resolve).to eq(second_note) + end + end + describe "#collapsed?" do context "when a diff discussion" do before do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 2cc818af6c7..925232169f1 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1124,6 +1124,46 @@ describe MergeRequest, models: true do allow(subject).to receive(:diff_discussions).and_return([first_discussion, second_discussion, third_discussion]) end + describe '#resolvable_discussions' do + before do + allow(first_discussion).to receive(:to_be_resolved?).and_return(true) + allow(second_discussion).to receive(:to_be_resolved?).and_return(false) + allow(third_discussion).to receive(:to_be_resolved?).and_return(false) + end + + it 'includes only discussions that need to be resolved' do + expect(subject.resolvable_discussions).to eq([first_discussion]) + end + end + + describe '#discussions_can_be_resolved_by? user' do + let(:user) { build(:user) } + + context 'all discussions can be resolved by the user' do + before do + allow(first_discussion).to receive(:can_resolve?).with(user).and_return(true) + allow(second_discussion).to receive(:can_resolve?).with(user).and_return(true) + allow(third_discussion).to receive(:can_resolve?).with(user).and_return(true) + end + + it 'allows a user to resolve the discussions' do + expect(subject.discussions_can_be_resolved_by?(user)).to be(true) + end + end + + context 'one discussion cannot be resolved by the user' do + before do + allow(first_discussion).to receive(:can_resolve?).with(user).and_return(true) + allow(second_discussion).to receive(:can_resolve?).with(user).and_return(true) + allow(third_discussion).to receive(:can_resolve?).with(user).and_return(false) + end + + it 'allows a user to resolve the discussions' do + expect(subject.discussions_can_be_resolved_by?(user)).to be(false) + end + end + end + describe "#discussions_resolvable?" do context "when all discussions are unresolvable" do before do diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 5700f800c2e..553983575c4 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -692,6 +692,32 @@ describe API::Issues, api: true do ]) end + context 'resolving issues in a merge request' do + let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first } + let(:merge_request) { discussion.noteable } + let(:project) { merge_request.source_project } + before do + project.team << [user, :master] + post api("/projects/#{project.id}/issues", user), + title: 'New Issue', + merge_request_for_resolving_discussions: merge_request.iid + end + + it 'creates a new project issue' do + expect(response).to have_http_status(:created) + end + + it 'resolves the discussions in a merge request' do + discussion.first_note.reload + + expect(discussion.resolved?).to be(true) + end + + it 'assigns a description to the issue mentioning the merge request' do + expect(json_response['description']).to include(merge_request.to_reference) + end + end + context 'with due date' do it 'creates a new project issue' do due_date = 2.weeks.from_now.strftime('%Y-%m-%d') diff --git a/spec/services/discussions/resolve_service_spec.rb b/spec/services/discussions/resolve_service_spec.rb new file mode 100644 index 00000000000..12c3cdf28c6 --- /dev/null +++ b/spec/services/discussions/resolve_service_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe Discussions::ResolveService do + describe '#execute' do + let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first } + let(:project) { merge_request.project } + let(:merge_request) { discussion.noteable } + let(:user) { create(:user) } + let(:service) { described_class.new(discussion.noteable.project, user, merge_request: merge_request) } + + before do + project.team << [user, :master] + end + + it "doesn't resolve discussions the user can't resolve" do + expect(discussion).to receive(:can_resolve?).with(user).and_return(false) + + service.execute(discussion) + + expect(discussion.resolved?).to be(false) + end + + it 'resolves the discussion' do + service.execute(discussion) + + expect(discussion.resolved?).to be(true) + end + + it 'executes the notification service' do + expect_any_instance_of(MergeRequests::ResolvedDiscussionNotificationService).to receive(:execute).with(discussion.noteable) + + service.execute(discussion) + end + + it 'adds a system note to the discussion' do + issue = create(:issue, project: project) + + expect(SystemNoteService).to receive(:discussion_continued_in_issue).with(discussion, project, user, issue) + service = described_class.new(project, user, merge_request: merge_request, follow_up_issue: issue) + service.execute(discussion) + end + + it 'can resolve multiple discussions at once' do + other_discussion = Discussion.for_diff_notes([create(:diff_note_on_merge_request, noteable: discussion.noteable, project: discussion.noteable.source_project)]).first + + service.execute([discussion, other_discussion]) + + expect(discussion.resolved?).to be(true) + expect(other_discussion.resolved?).to be(true) + end + end +end diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb new file mode 100644 index 00000000000..4cfba35c830 --- /dev/null +++ b/spec/services/issues/build_service_spec.rb @@ -0,0 +1,130 @@ +require 'spec_helper.rb' + +describe Issues::BuildService, services: true do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.team << [user, :developer] + end + + context 'for discussions in a merge request' do + let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project) } + let(:issue) { described_class.new(project, user, merge_request_for_resolving_discussions: merge_request).execute } + + def position_on_line(line_number) + Gitlab::Diff::Position.new( + old_path: "files/ruby/popen.rb", + new_path: "files/ruby/popen.rb", + old_line: nil, + new_line: line_number, + diff_refs: merge_request.diff_refs + ) + end + + describe '#items_for_discussions' do + it 'has an item for each discussion' do + create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.source_project, position: position_on_line(13)) + service = described_class.new(project, user, merge_request_for_resolving_discussions: merge_request) + + service.execute + + expect(service.items_for_discussions.size).to eq(2) + end + end + + describe '#item_for_discussion' do + let(:service) { described_class.new(project, user, merge_request_for_resolving_discussions: merge_request) } + + it 'mentions the author of the note' do + discussion = Discussion.new([create(:diff_note_on_merge_request, author: create(:user, username: 'author'))]) + expect(service.item_for_discussion(discussion)).to include('@author') + end + + it 'wraps the note in a blockquote' do + note_text = "This is a string\n"\ + ">>>\n"\ + "with a blockquote\n"\ + "> That has a quote\n"\ + ">>>\n" + note_result = "This is a string\n"\ + "> with a blockquote\n"\ + "> > That has a quote\n" + discussion = Discussion.new([create(:diff_note_on_merge_request, note: note_text)]) + expect(service.item_for_discussion(discussion)).to include(">>>\n#{note_result}\n>>>") + end + end + + describe '#execute' do + it 'has the merge request reference in the title' do + expect(issue.title).to include(merge_request.title) + end + + it 'has the reference of the merge request in the description' do + expect(issue.description).to include(merge_request.to_reference) + end + + it 'does not assign title when a title was given' do + issue = described_class.new(project, user, + merge_request_for_resolving_discussions: merge_request, + title: 'What an issue').execute + + expect(issue.title).to eq('What an issue') + end + + it 'does not assign description when a description was given' do + issue = described_class.new(project, user, + merge_request_for_resolving_discussions: merge_request, + description: 'Fix at your earliest conveignance').execute + + expect(issue.description).to eq('Fix at your earliest conveignance') + end + + describe 'with multiple discussions' do + before do + create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.target_project, position: position_on_line(15)) + end + + it 'mentions all the authors in the description' do + authors = merge_request.diff_discussions.map(&:author) + + expect(issue.description).to include(*authors.map(&:to_reference)) + end + + it 'has a link for each unresolved discussion in the description' do + notes = merge_request.diff_discussions.map(&:first_note) + links = notes.map { |note| Gitlab::UrlBuilder.build(note) } + + expect(issue.description).to include(*links) + end + + it 'mentions additional notes' do + create_list(:diff_note_on_merge_request, 2, noteable: merge_request, project: merge_request.target_project, position: position_on_line(15)) + + expect(issue.description).to include('(+2 comments)') + end + end + end + end + + context 'For a merge request without discussions' do + let(:merge_request) { create(:merge_request, source_project: project) } + + describe '#execute' do + it 'mentions the merge request in the description' do + issue = described_class.new(project, user, merge_request_for_resolving_discussions: merge_request).execute + + expect(issue.description).to include("Review the conversation in #{merge_request.to_reference}") + end + end + end + + describe '#execute' do + it 'builds a new issues with given params' do + issue = described_class.new(project, user, title: 'Issue #1', description: 'Issue description').execute + + expect(issue.title).to eq('Issue #1') + expect(issue.description).to eq('Issue description') + end + end +end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 5c0331ebe66..8bde61ee336 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -136,5 +136,48 @@ describe Issues::CreateService, services: true do end it_behaves_like 'new issuable record that supports slash commands' + + context 'for a merge request' do + let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first } + let(:merge_request) { discussion.noteable } + let(:project) { merge_request.source_project } + let(:opts) { { merge_request_for_resolving_discussions: merge_request } } + + before do + project.team << [user, :master] + end + + it 'resolves the discussion for the merge request' do + described_class.new(project, user, opts).execute + discussion.first_note.reload + + expect(discussion.resolved?).to be(true) + end + + it 'added a system note to the discussion' do + described_class.new(project, user, opts).execute + + reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first + + expect(reloaded_discussion.last_note.system).to eq(true) + end + + it 'assigns the title and description for the issue' do + issue = described_class.new(project, user, opts).execute + + expect(issue.title).not_to be_nil + expect(issue.description).not_to be_nil + end + + it 'can set nil explicityly to the title and description' do + issue = described_class.new(project, user, + merge_request_for_resolving_discussions: merge_request, + description: nil, + title: nil).execute + + expect(issue.description).to be_nil + expect(issue.title).to be_nil + end + end end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 435cfb07292..07a9d8e1997 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -712,4 +712,32 @@ describe SystemNoteService, services: true do end end end + + describe '.discussion_continued_in_issue' do + let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first } + let(:merge_request) { discussion.noteable } + let(:project) { merge_request.source_project } + let(:issue) { create(:issue, project: project) } + let(:user) { create(:user) } + + def reloaded_merge_request + MergeRequest.find(merge_request.id) + end + + before do + project.team << [user, :developer] + end + + it 'creates a new note in the discussion' do + # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded. + expect { SystemNoteService.discussion_continued_in_issue(discussion, project, user, issue) }. + to change { reloaded_merge_request.discussions.first.notes.size }.by(1) + end + + it 'mentions the created issue in the system note' do + note = SystemNoteService.discussion_continued_in_issue(discussion, project, user, issue) + + expect(note.note).to include(issue.to_reference) + end + end end -- cgit v1.2.1 From 877c225c4a5c3a794a5ff1af058bc7581e299bac Mon Sep 17 00:00:00 2001 From: Chris Peressini <cperessini@gitlab.com> Date: Mon, 5 Dec 2016 11:10:35 +0000 Subject: Document button secondary states. Update icons and color section Describe hover, focus and active states for primary and secondary buttons. Organized the icons section in a table. Replaced images for icons so they are all the same size. Organized the color section in a table. Replaced the images with new ones so the table would have a more manageable size. Updated the values for some colors to show the $color-light shade. Remove commented sections Remove `db/schema.rb` Add db/schema.rb from origin/master Fix typos --- doc/development/ux_guide/basics.md | 50 +++++++-------------- doc/development/ux_guide/components.md | 21 ++++++++- .../ux_guide/img/button-close--active.png | Bin 0 -> 1385 bytes .../ux_guide/img/button-close--hover.png | Bin 0 -> 1015 bytes .../ux_guide/img/button-close--resting.png | Bin 0 -> 1271 bytes .../ux_guide/img/button-danger--active.png | Bin 0 -> 1450 bytes .../ux_guide/img/button-danger--hover.png | Bin 0 -> 1095 bytes .../ux_guide/img/button-danger--resting.png | Bin 0 -> 1376 bytes .../ux_guide/img/button-info--active.png | Bin 0 -> 1442 bytes .../ux_guide/img/button-info--hover.png | Bin 0 -> 1079 bytes .../ux_guide/img/button-info--resting.png | Bin 0 -> 1296 bytes .../ux_guide/img/button-spam--active.png | Bin 0 -> 1435 bytes .../ux_guide/img/button-spam--hover.png | Bin 0 -> 1108 bytes .../ux_guide/img/button-spam--resting.png | Bin 0 -> 1377 bytes .../ux_guide/img/button-success--active.png | Bin 0 -> 1510 bytes .../ux_guide/img/button-success--hover.png | Bin 0 -> 1151 bytes .../ux_guide/img/button-success--resting.png | Bin 0 -> 1447 bytes .../img/button-success-secondary--active.png | Bin 0 -> 1466 bytes .../img/button-success-secondary--hover.png | Bin 0 -> 1091 bytes .../img/button-success-secondary--resting.png | Bin 0 -> 1394 bytes .../ux_guide/img/button-warning--active.png | Bin 0 -> 1388 bytes .../ux_guide/img/button-warning--hover.png | Bin 0 -> 1040 bytes .../ux_guide/img/button-warning--resting.png | Bin 0 -> 1296 bytes doc/development/ux_guide/img/color-blue.png | Bin 2725 -> 3555 bytes doc/development/ux_guide/img/color-green.png | Bin 3008 -> 3852 bytes doc/development/ux_guide/img/color-grey.png | Bin 2384 -> 3523 bytes doc/development/ux_guide/img/color-orange.png | Bin 3470 -> 4480 bytes doc/development/ux_guide/img/color-red.png | Bin 2628 -> 3550 bytes doc/development/ux_guide/img/icon-add.png | Bin 155 -> 317 bytes doc/development/ux_guide/img/icon-close.png | Bin 225 -> 501 bytes doc/development/ux_guide/img/icon-edit.png | Bin 231 -> 546 bytes doc/development/ux_guide/img/icon-notification.png | Bin 259 -> 543 bytes doc/development/ux_guide/img/icon-rss.png | Bin 307 -> 834 bytes doc/development/ux_guide/img/icon-subscribe.png | Bin 348 -> 760 bytes doc/development/ux_guide/img/icon-trash.png | Bin 380 -> 398 bytes 35 files changed, 36 insertions(+), 35 deletions(-) create mode 100644 doc/development/ux_guide/img/button-close--active.png create mode 100644 doc/development/ux_guide/img/button-close--hover.png create mode 100644 doc/development/ux_guide/img/button-close--resting.png create mode 100644 doc/development/ux_guide/img/button-danger--active.png create mode 100644 doc/development/ux_guide/img/button-danger--hover.png create mode 100644 doc/development/ux_guide/img/button-danger--resting.png create mode 100644 doc/development/ux_guide/img/button-info--active.png create mode 100644 doc/development/ux_guide/img/button-info--hover.png create mode 100644 doc/development/ux_guide/img/button-info--resting.png create mode 100644 doc/development/ux_guide/img/button-spam--active.png create mode 100644 doc/development/ux_guide/img/button-spam--hover.png create mode 100644 doc/development/ux_guide/img/button-spam--resting.png create mode 100644 doc/development/ux_guide/img/button-success--active.png create mode 100644 doc/development/ux_guide/img/button-success--hover.png create mode 100644 doc/development/ux_guide/img/button-success--resting.png create mode 100644 doc/development/ux_guide/img/button-success-secondary--active.png create mode 100644 doc/development/ux_guide/img/button-success-secondary--hover.png create mode 100644 doc/development/ux_guide/img/button-success-secondary--resting.png create mode 100644 doc/development/ux_guide/img/button-warning--active.png create mode 100644 doc/development/ux_guide/img/button-warning--hover.png create mode 100644 doc/development/ux_guide/img/button-warning--resting.png diff --git a/doc/development/ux_guide/basics.md b/doc/development/ux_guide/basics.md index 62ac56a6bce..a29cfa096b2 100644 --- a/doc/development/ux_guide/basics.md +++ b/doc/development/ux_guide/basics.md @@ -35,26 +35,15 @@ This is the typeface used for code blocks. GitLab uses the OS default font. ## Icons GitLab uses Font Awesome icons throughout our interface. -![Trash icon](img/icon-trash.png) -The trash icon is used for destructive actions that deletes information. - -![Edit icon](img/icon-edit.png) -The pencil icon is used for editing content such as comments. - -![Notification icon](img/icon-notification.png) -The bell icon is for notifications, such as Todos. - -![Subscribe icon](img/icon-subscribe.png) -The eye icon is for subscribing to updates. For example, you can subscribe to a label and get updated on issues with that label. - -![RSS icon](img/icon-rss.png) -The standard RSS icon is used for linking to RSS/atom feeds. - -![Close icon](img/icon-close.png) -An 'x' is used for closing UI elements such as dropdowns. - -![Add icon](img/icon-add.png) -A plus is used when creating new objects, such as issues, projects, etc. +| | | +| :-----------: | :---- | +| ![Trash icon](img/icon-trash.png) | The trash icon is used for destructive actions that deletes information. | +| ![Edit icon](img/icon-edit.png) | The pencil icon is used for editing content such as comments.| +| ![Notification icon](img/icon-notification.png) | The bell icon is for notifications, such as Todos. | +| ![Subscribe icon](img/icon-subscribe.png) | The eye icon is for subscribing to updates. For example, you can subscribe to a label and get updated on issues with that label. | +| ![RSS icon](img/icon-rss.png) | The standard RSS icon is used for linking to RSS/atom feeds. | +| ![Close icon](img/icon-close.png) | An 'x' is used for closing UI elements such as dropdowns. | +| ![Add icon](img/icon-add.png) | A plus is used when creating new objects, such as issues, projects, etc. | > TODO: update this section, add more general guidance to icon usage and personality, etc. @@ -62,20 +51,13 @@ A plus is used when creating new objects, such as issues, projects, etc. ## Color -![Blue](img/color-blue.png) -Blue is used to highlight primary active elements (such as current tab), as well as other organization and managing commands. - -![Green](img/color-green.png) -Green is for actions that create new objects. - -![Orange](img/color-orange.png) -Orange is used for warnings - -![Red](img/color-red.png) -Red is reserved for delete and other destructive commands - -![Grey](img/color-grey.png) -Grey, and white (depending on context) is used for netral, secondary elements +| | | +| :------: | :------- | +| ![Blue](img/color-blue.png) | Blue is used to highlight primary active elements (such as the current tab), as well as other organizational and managing commands.| +| ![Green](img/color-green.png) | Green is for actions that create new objects. | +| ![Orange](img/color-orange.png) | Orange is used for warnings | +| ![Red](img/color-red.png) | Red is reserved for delete and other destructive commands | +| ![Grey](img/color-grey.png) | Grey is used for neutral secondary elements. Depending on context, white is sometimes used instead. | > TODO: Establish a perspective for color in terms of our personality and rationalize with Marketing usage. diff --git a/doc/development/ux_guide/components.md b/doc/development/ux_guide/components.md index 8e51edd23ef..28383ad7dfc 100644 --- a/doc/development/ux_guide/components.md +++ b/doc/development/ux_guide/components.md @@ -75,10 +75,29 @@ Text should be in sentence case, where only the first word is capitalized. "Crea > TODO: Rationalize this. Ensure that we still believe this. ### Colors -Follow the color guidance on the [basics](basics.md#color) page. The default color treatment is the white/grey button. +The default color treatment is the white/grey button. Follow the guidance on the [basics](basics.md#color) page to add meaningful color to a button. + +### Secondary states + +Primary buttons darken the color of their background and border for hover, focus and active states. An inner shadow is added to the active state to denote the button being pressed. + +| Values | Info | Success | Warning | Danger | +| :------ | :------: | :------: | :------: | :------: | +| Background: `$color-light` <br> Border: `$border-color-light` | ![](img/button-info--resting.png) | ![](img/button-success--resting.png) | ![](img/button-warning--resting.png) | ![](img/button-danger--resting.png) | +| Background: `$color-normal` <br> Border: `$border-color-normal` | ![](img/button-info--hover.png) | ![](img/button-success--hover.png) | ![](img/button-warning--hover.png) | ![](img/button-danger--hover.png) | +| Background: `$color-dark` <br> Border: `$border-color-dark` | ![](img/button-info--active.png) | ![](img/button-success--active.png) | ![](img/button-warning--active.png) | ![](img/button-danger--active.png) | + +Since secondary buttons only have a border on their resting state, their hover and focus states add a background color, which gets darkened on active. + +| Values | Success Secondary | Close | Spam | +| :------ | :------: | :------: | :------: | +| Font: `$border-color-light` <br> Border: `$border-color-light` | ![](img/button-success-secondary--resting.png) | ![](img/button-close--resting.png) | ![](img/button-spam--resting.png) | +| Background: `$color-light` <br> Border: `$border-color-light` | ![](img/button-success-secondary--hover.png) | ![](img/button-close--hover.png) | ![](img/button-spam--hover.png) | +| Background: `$color-normal` <br> Border: `$border-color-normal` | ![](img/button-success-secondary--active.png) | ![](img/button-close--active.png) | ![](img/button-spam--active.png) | --- + ## Dropdowns Dropdowns are used to allow users to choose one (or many) options from a list of options. If this list of options is more 20, there should generally be a way to search through and filter the options (see the complex filter dropdowns below.) diff --git a/doc/development/ux_guide/img/button-close--active.png b/doc/development/ux_guide/img/button-close--active.png new file mode 100644 index 00000000000..824bfc8f31b Binary files /dev/null and b/doc/development/ux_guide/img/button-close--active.png differ diff --git a/doc/development/ux_guide/img/button-close--hover.png b/doc/development/ux_guide/img/button-close--hover.png new file mode 100644 index 00000000000..0291e121894 Binary files /dev/null and b/doc/development/ux_guide/img/button-close--hover.png differ diff --git a/doc/development/ux_guide/img/button-close--resting.png b/doc/development/ux_guide/img/button-close--resting.png new file mode 100644 index 00000000000..986d7174ce7 Binary files /dev/null and b/doc/development/ux_guide/img/button-close--resting.png differ diff --git a/doc/development/ux_guide/img/button-danger--active.png b/doc/development/ux_guide/img/button-danger--active.png new file mode 100644 index 00000000000..d3c64424b26 Binary files /dev/null and b/doc/development/ux_guide/img/button-danger--active.png differ diff --git a/doc/development/ux_guide/img/button-danger--hover.png b/doc/development/ux_guide/img/button-danger--hover.png new file mode 100644 index 00000000000..8506e093306 Binary files /dev/null and b/doc/development/ux_guide/img/button-danger--hover.png differ diff --git a/doc/development/ux_guide/img/button-danger--resting.png b/doc/development/ux_guide/img/button-danger--resting.png new file mode 100644 index 00000000000..69ad6bb796b Binary files /dev/null and b/doc/development/ux_guide/img/button-danger--resting.png differ diff --git a/doc/development/ux_guide/img/button-info--active.png b/doc/development/ux_guide/img/button-info--active.png new file mode 100644 index 00000000000..23be20b225c Binary files /dev/null and b/doc/development/ux_guide/img/button-info--active.png differ diff --git a/doc/development/ux_guide/img/button-info--hover.png b/doc/development/ux_guide/img/button-info--hover.png new file mode 100644 index 00000000000..4cb4e38558c Binary files /dev/null and b/doc/development/ux_guide/img/button-info--hover.png differ diff --git a/doc/development/ux_guide/img/button-info--resting.png b/doc/development/ux_guide/img/button-info--resting.png new file mode 100644 index 00000000000..5883340aa83 Binary files /dev/null and b/doc/development/ux_guide/img/button-info--resting.png differ diff --git a/doc/development/ux_guide/img/button-spam--active.png b/doc/development/ux_guide/img/button-spam--active.png new file mode 100644 index 00000000000..55b44898684 Binary files /dev/null and b/doc/development/ux_guide/img/button-spam--active.png differ diff --git a/doc/development/ux_guide/img/button-spam--hover.png b/doc/development/ux_guide/img/button-spam--hover.png new file mode 100644 index 00000000000..3dc8ed34c54 Binary files /dev/null and b/doc/development/ux_guide/img/button-spam--hover.png differ diff --git a/doc/development/ux_guide/img/button-spam--resting.png b/doc/development/ux_guide/img/button-spam--resting.png new file mode 100644 index 00000000000..b6bf10a5b64 Binary files /dev/null and b/doc/development/ux_guide/img/button-spam--resting.png differ diff --git a/doc/development/ux_guide/img/button-success--active.png b/doc/development/ux_guide/img/button-success--active.png new file mode 100644 index 00000000000..895a52831cb Binary files /dev/null and b/doc/development/ux_guide/img/button-success--active.png differ diff --git a/doc/development/ux_guide/img/button-success--hover.png b/doc/development/ux_guide/img/button-success--hover.png new file mode 100644 index 00000000000..e4c74bd9778 Binary files /dev/null and b/doc/development/ux_guide/img/button-success--hover.png differ diff --git a/doc/development/ux_guide/img/button-success--resting.png b/doc/development/ux_guide/img/button-success--resting.png new file mode 100644 index 00000000000..2fa971b5347 Binary files /dev/null and b/doc/development/ux_guide/img/button-success--resting.png differ diff --git a/doc/development/ux_guide/img/button-success-secondary--active.png b/doc/development/ux_guide/img/button-success-secondary--active.png new file mode 100644 index 00000000000..e7383b36946 Binary files /dev/null and b/doc/development/ux_guide/img/button-success-secondary--active.png differ diff --git a/doc/development/ux_guide/img/button-success-secondary--hover.png b/doc/development/ux_guide/img/button-success-secondary--hover.png new file mode 100644 index 00000000000..4af2a68cf1b Binary files /dev/null and b/doc/development/ux_guide/img/button-success-secondary--hover.png differ diff --git a/doc/development/ux_guide/img/button-success-secondary--resting.png b/doc/development/ux_guide/img/button-success-secondary--resting.png new file mode 100644 index 00000000000..a5a4ec512c8 Binary files /dev/null and b/doc/development/ux_guide/img/button-success-secondary--resting.png differ diff --git a/doc/development/ux_guide/img/button-warning--active.png b/doc/development/ux_guide/img/button-warning--active.png new file mode 100644 index 00000000000..5877d46c94d Binary files /dev/null and b/doc/development/ux_guide/img/button-warning--active.png differ diff --git a/doc/development/ux_guide/img/button-warning--hover.png b/doc/development/ux_guide/img/button-warning--hover.png new file mode 100644 index 00000000000..308e1adc8a3 Binary files /dev/null and b/doc/development/ux_guide/img/button-warning--hover.png differ diff --git a/doc/development/ux_guide/img/button-warning--resting.png b/doc/development/ux_guide/img/button-warning--resting.png new file mode 100644 index 00000000000..28e5e601520 Binary files /dev/null and b/doc/development/ux_guide/img/button-warning--resting.png differ diff --git a/doc/development/ux_guide/img/color-blue.png b/doc/development/ux_guide/img/color-blue.png index 2ca360173eb..844e926f1f5 100644 Binary files a/doc/development/ux_guide/img/color-blue.png and b/doc/development/ux_guide/img/color-blue.png differ diff --git a/doc/development/ux_guide/img/color-green.png b/doc/development/ux_guide/img/color-green.png index 489db8f4343..5c4c23c7067 100644 Binary files a/doc/development/ux_guide/img/color-green.png and b/doc/development/ux_guide/img/color-green.png differ diff --git a/doc/development/ux_guide/img/color-grey.png b/doc/development/ux_guide/img/color-grey.png index 58c474d5ce9..5247649a0ce 100644 Binary files a/doc/development/ux_guide/img/color-grey.png and b/doc/development/ux_guide/img/color-grey.png differ diff --git a/doc/development/ux_guide/img/color-orange.png b/doc/development/ux_guide/img/color-orange.png index 4c4b772d438..1103c715225 100644 Binary files a/doc/development/ux_guide/img/color-orange.png and b/doc/development/ux_guide/img/color-orange.png differ diff --git a/doc/development/ux_guide/img/color-red.png b/doc/development/ux_guide/img/color-red.png index 3440ad48f05..77ecbbc0a20 100644 Binary files a/doc/development/ux_guide/img/color-red.png and b/doc/development/ux_guide/img/color-red.png differ diff --git a/doc/development/ux_guide/img/icon-add.png b/doc/development/ux_guide/img/icon-add.png index 0d4c1a7692a..bcad5e84591 100644 Binary files a/doc/development/ux_guide/img/icon-add.png and b/doc/development/ux_guide/img/icon-add.png differ diff --git a/doc/development/ux_guide/img/icon-close.png b/doc/development/ux_guide/img/icon-close.png index 88d2b3b0c6d..dfe1495f5fa 100644 Binary files a/doc/development/ux_guide/img/icon-close.png and b/doc/development/ux_guide/img/icon-close.png differ diff --git a/doc/development/ux_guide/img/icon-edit.png b/doc/development/ux_guide/img/icon-edit.png index f73be7a10fb..50f6f841868 100644 Binary files a/doc/development/ux_guide/img/icon-edit.png and b/doc/development/ux_guide/img/icon-edit.png differ diff --git a/doc/development/ux_guide/img/icon-notification.png b/doc/development/ux_guide/img/icon-notification.png index 4758632edd7..6ddfaa44f66 100644 Binary files a/doc/development/ux_guide/img/icon-notification.png and b/doc/development/ux_guide/img/icon-notification.png differ diff --git a/doc/development/ux_guide/img/icon-rss.png b/doc/development/ux_guide/img/icon-rss.png index c7ac9fb1349..b766488b32d 100644 Binary files a/doc/development/ux_guide/img/icon-rss.png and b/doc/development/ux_guide/img/icon-rss.png differ diff --git a/doc/development/ux_guide/img/icon-subscribe.png b/doc/development/ux_guide/img/icon-subscribe.png index 5cb277bfd5d..650168296c6 100644 Binary files a/doc/development/ux_guide/img/icon-subscribe.png and b/doc/development/ux_guide/img/icon-subscribe.png differ diff --git a/doc/development/ux_guide/img/icon-trash.png b/doc/development/ux_guide/img/icon-trash.png index 357289a6fff..b02178ca992 100644 Binary files a/doc/development/ux_guide/img/icon-trash.png and b/doc/development/ux_guide/img/icon-trash.png differ -- cgit v1.2.1 From 4e0a0d093d7a4290a59523238990bf96b1b89313 Mon Sep 17 00:00:00 2001 From: Ahmad Sherif <me@ahmadsherif.com> Date: Mon, 5 Dec 2016 21:01:38 +0200 Subject: Authorize users into imported GitLab project --- app/models/member.rb | 1 + .../fix-authorize-users-into-imported-gitlab-project.yml | 4 ++++ spec/lib/gitlab/import_export/members_mapper_spec.rb | 11 +++++++++-- 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/fix-authorize-users-into-imported-gitlab-project.yml diff --git a/app/models/member.rb b/app/models/member.rb index df93aaee847..3b65587c66b 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -63,6 +63,7 @@ class Member < ActiveRecord::Base after_create :send_request, if: :request?, unless: :importing? after_create :create_notification_setting, unless: [:pending?, :importing?] after_create :post_create_hook, unless: [:pending?, :importing?] + after_create :refresh_member_authorized_projects, if: :importing? after_update :post_update_hook, unless: [:pending?, :importing?] after_destroy :post_destroy_hook, unless: :pending? diff --git a/changelogs/unreleased/fix-authorize-users-into-imported-gitlab-project.yml b/changelogs/unreleased/fix-authorize-users-into-imported-gitlab-project.yml new file mode 100644 index 00000000000..9f14463fdd1 --- /dev/null +++ b/changelogs/unreleased/fix-authorize-users-into-imported-gitlab-project.yml @@ -0,0 +1,4 @@ +--- +title: Authorize users into imported GitLab project +merge_request: +author: diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb index 770e8b0c2f4..1cb02f8e318 100644 --- a/spec/lib/gitlab/import_export/members_mapper_spec.rb +++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' describe Gitlab::ImportExport::MembersMapper, services: true do describe 'map members' do - let(:user) { create(:user) } + let(:user) { create(:user, authorized_projects_populated: true) } let(:project) { create(:project, :public, name: 'searchable_project') } - let(:user2) { create(:user) } + let(:user2) { create(:user, authorized_projects_populated: true) } let(:exported_user_id) { 99 } let(:exported_members) do [{ @@ -67,5 +67,12 @@ describe Gitlab::ImportExport::MembersMapper, services: true do expect(ProjectMember.find_by_invite_email('invite@test.com')).not_to be_nil end + + it 'authorizes the users to the project' do + members_mapper.map + + expect(user.authorized_project?(project)).to be true + expect(user2.authorized_project?(project)).to be true + end end end -- cgit v1.2.1 From 4c7c66a93e656dea6d3357261f253f470467bb34 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 5 Dec 2016 22:46:13 +0100 Subject: Fix dead links, add example of debug trace output, simplify titles [ci skip] --- doc/ci/README.md | 3 +- doc/ci/variables/README.md | 171 ++++++++++++++++++++++++++++++++++++++------- 2 files changed, 147 insertions(+), 27 deletions(-) diff --git a/doc/ci/README.md b/doc/ci/README.md index f7fb56cb76a..daa5405d0f2 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -11,7 +11,8 @@ - [Configure a Runner, the application that runs your builds](runners/README.md) - [Use Docker images with GitLab Runner](docker/using_docker_images.md) - [Use CI to build Docker images](docker/using_docker_build.md) -- [Learn how to use variables in your build scripts](variables/README.md) +- [CI Variables](variables/README.md) - Learn how to use variables defined in + your `.gitlab-ci.yml` or secured ones defined in your project's settings - [Use SSH keys in your build environment](ssh_keys/README.md) - [Trigger builds through the API](triggers/README.md) - [Build artifacts](../user/project/builds/artifacts.md) diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index bb0b171838b..0a0ea2e96e1 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -1,27 +1,30 @@ # Variables -When receiving a build from GitLab CI, the runner prepares the build environment. -It starts by setting a list of **predefined variables** (Environment variables) +When receiving a build from GitLab CI, the [Runner] prepares the build environment. +It starts by setting a list of **predefined variables** (environment variables) and a list of **user-defined variables**. -The variables can be overwritten. They take precedence over each other in this -order: +## Priority of variables -1. Trigger variables (take precedence over all) -1. Secure variables -1. YAML-defined job-level variables -1. YAML-defined global variables -1. Predefined variables (are the lowest in the chain) +The variables can be overwritten and they take precedence over each other in +this order: + +1. [Trigger variables][triggers] (take precedence over all) +1. [Secure variables](#secure-variables) +1. YAML-defined [job-level variables](../yaml/README.md#job-variables) +1. YAML-defined [global variables](../yaml/README.md#variables) +1. [Predefined variables](#predefined-variables-environment-variables) (are the + lowest in the chain) For example, if you define `API_TOKEN=secure` as a secure variable and -`API_TOKEN=yaml` as YAML-defined variable, the `API_TOKEN` will take the value +`API_TOKEN=yaml` in your `.gitlab-ci.yml`, the `API_TOKEN` will take the value `secure` as the secure variables are higher in the chain. ## Predefined variables (Environment variables) ->**Note:** -Some of the variables are available only if a minimum version of [GitLab Runner] -is used. +Some of the predefined environment variables are available only if a minimum +version of [GitLab Runner][runner] is used. Consult the table below to find the +version of Runner required. | Variable | GitLab | Runner | Description | |-------------------------|--------|--------|-------------| @@ -64,7 +67,7 @@ Example values: export CI_BUILD_ID="50" export CI_BUILD_REF="1ecfd275763eff1d6b4844ea3168962458c9f27a" export CI_BUILD_REF_NAME="master" -export CI_BUILD_REPO="https://gitab-ci-token:abcde-1234ABCD5678ef@gitlab.com/gitlab-org/gitlab-ce.git" +export CI_BUILD_REPO="https://gitab-ci-token:abcde-1234ABCD5678ef@example.com/gitlab-org/gitlab-ce.git" export CI_BUILD_TAG="1.0.0" export CI_BUILD_NAME="spec:other" export CI_BUILD_STAGE="test" @@ -77,9 +80,9 @@ export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-ce" export CI_PROJECT_NAME="gitlab-ce" export CI_PROJECT_NAMESPACE="gitlab-org" export CI_PROJECT_PATH="gitlab-org/gitlab-ce" -export CI_PROJECT_URL="https://gitlab.com/gitlab-org/gitlab-ce" -export CI_REGISTRY="registry.gitlab.com" -export CI_REGISTRY_IMAGE="registry.gitlab.com/gitlab-org/gitlab-ce" +export CI_PROJECT_URL="https://example.com/gitlab-org/gitlab-ce" +export CI_REGISTRY="registry.example.com" +export CI_REGISTRY_IMAGE="registry.example.com/gitlab-org/gitlab-ce" export CI_RUNNER_ID="10" export CI_RUNNER_DESCRIPTION="my runner" export CI_RUNNER_TAGS="docker, linux" @@ -88,10 +91,10 @@ export CI_SERVER_NAME="GitLab" export CI_SERVER_REVISION="70606bf" export CI_SERVER_VERSION="8.9.0" export GITLAB_USER_ID="42" -export GITLAB_USER_EMAIL="alexzander@sporer.com" +export GITLAB_USER_EMAIL="user@example.com" ``` -## YAML-defined variables +## `.gitlab-ci.yaml` defined variables >**Note:** This feature requires GitLab Runner 0.5.0 or higher and GitLab CI 7.14 or higher. @@ -121,7 +124,7 @@ job_name: variables: [] ``` -## User-defined variables (secure variables) +## Secure variables >**Notes:** - This feature requires GitLab Runner 0.4.0 or higher. @@ -137,7 +140,7 @@ the build environment. The secure variables are stored out of the repository available in the build environment. It's the recommended method to use for storing things like passwords, secret keys and credentials. -Secure Variables can added by going to your project's +Secure variables can be added by going to your project's **Settings ➔ Variables ➔ Add variable**. Once you set them, they will be available for all subsequent builds. @@ -162,8 +165,8 @@ trace, resulting in a verbose build trace listing all commands that were run, variables that were set, etc. Before enabling this, you should ensure builds are visible to -[team members only](../../../user/permissions.md#project-features). You should -also [erase](../pipelines.md#seeing-build-traces) all generated build traces +[team members only](../../user/permissions.md#project-features). You should +also [erase](../pipelines.md#seeing-build-status) all generated build traces before making them visible again. To enable debug traces, set the `CI_DEBUG_TRACE` variable to `true`: @@ -174,8 +177,123 @@ job_name: CI_DEBUG_TRACE: "true" ``` -The [example project](https://gitlab.com/gitlab-examples/ci-debug-trace) -demonstrates a working configuration, including build trace examples. +Example truncated output with debug trace set to true: + +```bash +... + +export CI_SERVER_TLS_CA_FILE="/builds/gitlab-examples/ci-debug-trace.tmp/CI_SERVER_TLS_CA_FILE" +if [[ -d "/builds/gitlab-examples/ci-debug-trace/.git" ]]; then + echo $'\''\x1b[32;1mFetching changes...\x1b[0;m'\'' + $'\''cd'\'' "/builds/gitlab-examples/ci-debug-trace" + $'\''git'\'' "config" "fetch.recurseSubmodules" "false" + $'\''rm'\'' "-f" ".git/index.lock" + $'\''git'\'' "clean" "-ffdx" + $'\''git'\'' "reset" "--hard" + $'\''git'\'' "remote" "set-url" "origin" "https://gitlab-ci-token:xxxxxxxxxxxxxxxxxxxx@example.com/gitlab-examples/ci-debug-trace.git" + $'\''git'\'' "fetch" "origin" "--prune" "+refs/heads/*:refs/remotes/origin/*" "+refs/tags/*:refs/tags/*" +else + $'\''mkdir'\'' "-p" "/builds/gitlab-examples/ci-debug-trace.tmp/git-template" + $'\''rm'\'' "-r" "-f" "/builds/gitlab-examples/ci-debug-trace" + $'\''git'\'' "config" "-f" "/builds/gitlab-examples/ci-debug-trace.tmp/git-template/config" "fetch.recurseSubmodules" "false" + echo $'\''\x1b[32;1mCloning repository...\x1b[0;m'\'' + $'\''git'\'' "clone" "--no-checkout" "https://gitlab-ci-token:xxxxxxxxxxxxxxxxxxxx@example.com/gitlab-examples/ci-debug-trace.git" "/builds/gitlab-examples/ci-debug-trace" "--template" "/builds/gitlab-examples/ci-debug-trace.tmp/git-template" + $'\''cd'\'' "/builds/gitlab-examples/ci-debug-trace" +fi +echo $'\''\x1b[32;1mChecking out dd648b2e as master...\x1b[0;m'\'' +$'\''git'\'' "checkout" "-f" "-q" "dd648b2e48ce6518303b0bb580b2ee32fadaf045" +' ++++ hostname +++ echo 'Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-machine-1480971377-317a7d0f-digital-ocean-4gb...' +Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-machine-1480971377-317a7d0f-digital-ocean-4gb... +++ export CI=true +++ CI=true +++ export CI_DEBUG_TRACE=false +++ CI_DEBUG_TRACE=false +++ export CI_BUILD_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045 +++ CI_BUILD_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045 +++ export CI_BUILD_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045 +++ CI_BUILD_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045 +++ export CI_BUILD_REF_NAME=master +++ CI_BUILD_REF_NAME=master +++ export CI_BUILD_ID=7046507 +++ CI_BUILD_ID=7046507 +++ export CI_BUILD_REPO=https://gitlab-ci-token:xxxxxxxxxxxxxxxxxxxx@example.com/gitlab-examples/ci-debug-trace.git +++ CI_BUILD_REPO=https://gitlab-ci-token:xxxxxxxxxxxxxxxxxxxx@example.com/gitlab-examples/ci-debug-trace.git +++ export CI_BUILD_TOKEN=xxxxxxxxxxxxxxxxxxxx +++ CI_BUILD_TOKEN=xxxxxxxxxxxxxxxxxxxx +++ export CI_PROJECT_ID=1796893 +++ CI_PROJECT_ID=1796893 +++ export CI_PROJECT_DIR=/builds/gitlab-examples/ci-debug-trace +++ CI_PROJECT_DIR=/builds/gitlab-examples/ci-debug-trace +++ export CI_SERVER=yes +++ CI_SERVER=yes +++ export 'CI_SERVER_NAME=GitLab CI' +++ CI_SERVER_NAME='GitLab CI' +++ export CI_SERVER_VERSION= +++ CI_SERVER_VERSION= +++ export CI_SERVER_REVISION= +++ CI_SERVER_REVISION= +++ export GITLAB_CI=true +++ GITLAB_CI=true +++ export CI=true +++ CI=true +++ export GITLAB_CI=true +++ GITLAB_CI=true +++ export CI_BUILD_ID=7046507 +++ CI_BUILD_ID=7046507 +++ export CI_BUILD_TOKEN=xxxxxxxxxxxxxxxxxxxx +++ CI_BUILD_TOKEN=xxxxxxxxxxxxxxxxxxxx +++ export CI_BUILD_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045 +++ CI_BUILD_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045 +++ export CI_BUILD_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045 +++ CI_BUILD_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045 +++ export CI_BUILD_REF_NAME=master +++ CI_BUILD_REF_NAME=master +++ export CI_BUILD_NAME=debug_trace +++ CI_BUILD_NAME=debug_trace +++ export CI_BUILD_STAGE=test +++ CI_BUILD_STAGE=test +++ export CI_SERVER_NAME=GitLab +++ CI_SERVER_NAME=GitLab +++ export CI_SERVER_VERSION=8.14.3-ee +++ CI_SERVER_VERSION=8.14.3-ee +++ export CI_SERVER_REVISION=82823 +++ CI_SERVER_REVISION=82823 +++ export CI_PROJECT_ID=17893 +++ CI_PROJECT_ID=17893 +++ export CI_PROJECT_NAME=ci-debug-trace +++ CI_PROJECT_NAME=ci-debug-trace +++ export CI_PROJECT_PATH=gitlab-examples/ci-debug-trace +++ CI_PROJECT_PATH=gitlab-examples/ci-debug-trace +++ export CI_PROJECT_NAMESPACE=gitlab-examples +++ CI_PROJECT_NAMESPACE=gitlab-examples +++ export CI_PROJECT_URL=https://example.com/gitlab-examples/ci-debug-trace +++ CI_PROJECT_URL=https://example.com/gitlab-examples/ci-debug-trace +++ export CI_PIPELINE_ID=52666 +++ CI_PIPELINE_ID=52666 +++ export CI_RUNNER_ID=1337 +++ CI_RUNNER_ID=1337 +++ export CI_RUNNER_DESCRIPTION=shared-runners-manager-1.example.com +++ CI_RUNNER_DESCRIPTION=shared-runners-manager-1.example.com +++ export 'CI_RUNNER_TAGS=shared, docker, linux, ruby, mysql, postgres, mongo, git-annex' +++ CI_RUNNER_TAGS='shared, docker, linux, ruby, mysql, postgres, mongo, git-annex' +++ export CI_REGISTRY=registry.example.com +++ CI_REGISTRY=registry.example.com +++ export CI_DEBUG_TRACE=true +++ CI_DEBUG_TRACE=true +++ export GITLAB_USER_ID=42 +++ GITLAB_USER_ID=42 +++ export GITLAB_USER_EMAIL=user@example.com +++ GITLAB_USER_EMAIL=axilleas@axilleas.me +++ export VERY_SECURE_VARIABLE=imaverysecurevariable +++ VERY_SECURE_VARIABLE=imaverysecurevariable +++ mkdir -p /builds/gitlab-examples/ci-debug-trace.tmp +++ echo -n '-----BEGIN CERTIFICATE----- +MIIFQzCCBCugAwIBAgIRAL/ElDjuf15xwja1ZnCocWAwDQYJKoZIhvcNAQELBQAw' + +... +``` ## Using the CI variables in your job scripts @@ -203,5 +321,6 @@ job_name: ``` [ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784 -[gitlab runner]: https://docs.gitlab.com/runner/ +[runner]: https://docs.gitlab.com/runner/ [triggered]: ../triggers/README.md +[triggers]: ../triggers/README.md#pass-build-variables-to-a-trigger -- cgit v1.2.1 From 2a459e7e40443e3bd3f3ea7e1136cdfaf2a82545 Mon Sep 17 00:00:00 2001 From: Ryan Harris <harrisryan1@gmail.com> Date: Mon, 5 Dec 2016 16:46:18 -0500 Subject: Changed cursor for stage-nav-item instead of just stage-name --- app/assets/stylesheets/pages/cycle_analytics.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 7c558445674..ce708106490 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -215,7 +215,7 @@ border-bottom: 1px solid transparent; border-right: 1px solid $border-color; background-color: $gray-light; - cursor: default; + cursor: pointer; &.active { background-color: transparent; @@ -247,7 +247,6 @@ &.stage-name { width: 70%; - cursor: pointer; } &.stage-median { -- cgit v1.2.1 From cfb35cd7fd757495e332f449b725494ae8db349b Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 5 Dec 2016 22:55:57 +0100 Subject: It's secret variables, not secure [ci skip] --- doc/ci/variables/README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 0a0ea2e96e1..d142fe266a2 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -10,15 +10,15 @@ The variables can be overwritten and they take precedence over each other in this order: 1. [Trigger variables][triggers] (take precedence over all) -1. [Secure variables](#secure-variables) +1. [Secret variables](#secret-variables) 1. YAML-defined [job-level variables](../yaml/README.md#job-variables) 1. YAML-defined [global variables](../yaml/README.md#variables) 1. [Predefined variables](#predefined-variables-environment-variables) (are the lowest in the chain) -For example, if you define `API_TOKEN=secure` as a secure variable and +For example, if you define `API_TOKEN=secure` as a secret variable and `API_TOKEN=yaml` in your `.gitlab-ci.yml`, the `API_TOKEN` will take the value -`secure` as the secure variables are higher in the chain. +`secure` as the secret variables are higher in the chain. ## Predefined variables (Environment variables) @@ -124,23 +124,23 @@ job_name: variables: [] ``` -## Secure variables +## Secret variables >**Notes:** - This feature requires GitLab Runner 0.4.0 or higher. -- Be aware that secure variables are not masked, and their values can be shown +- Be aware that secret variables are not masked, and their values can be shown in the build logs if explicitly asked to do so. If your project is public or internal, you can set the pipelines private from your project's Pipelines settings. Follow the discussion in issue [#13784][ce-13784] for masking the - secure variables. + secret variables. -GitLab CI allows you to define per-project **Secure variables** that are set in -the build environment. The secure variables are stored out of the repository +GitLab CI allows you to define per-project **secret variables** that are set in +the build environment. The secret variables are stored out of the repository (`.gitlab-ci.yml`) and are securely passed to GitLab Runner making them available in the build environment. It's the recommended method to use for storing things like passwords, secret keys and credentials. -Secure variables can be added by going to your project's +Secret variables can be added by going to your project's **Settings ➔ Variables ➔ Add variable**. Once you set them, they will be available for all subsequent builds. @@ -150,7 +150,7 @@ Once you set them, they will be available for all subsequent builds. > Introduced in GitLab Runner 1.7. > > **WARNING:** Enabling debug tracing can have severe security implications. The - output **will** contain the content of all your secure variables and any other + output **will** contain the content of all your secret variables and any other secrets! The output **will** be uploaded to the GitLab server and made visible in build traces! @@ -311,7 +311,7 @@ job_name: ``` You can also list all environment variables with the `export` command, -but be aware that this will also expose the values of all the secure variables +but be aware that this will also expose the values of all the secret variables you set, in the build log: ``` -- cgit v1.2.1 From 7a9ba9bb85c1ab0e4bb4f116ce45b9a82aea3096 Mon Sep 17 00:00:00 2001 From: winniehell <git@winniehell.de> Date: Mon, 5 Dec 2016 23:02:23 +0100 Subject: Add failing test for #25191 --- spec/lib/banzai/filter/relative_link_filter_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index 2bfa51deb20..df2dd173b57 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -175,7 +175,7 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do allow_any_instance_of(described_class).to receive(:uri_type).and_return(:raw) doc = filter(image(escaped)) - expect(doc.at_css('img')['src']).to match '/raw/' + expect(doc.at_css('img')['src']).to eq "/#{project_path}/raw/#{Addressable::URI.escape(ref)}/#{escaped}" end context 'when requested path is a file in the repo' do -- cgit v1.2.1 From 30f050491694e27232ddab679785ddac2dd46514 Mon Sep 17 00:00:00 2001 From: awhildy <allison@gitlab.com> Date: Sun, 27 Nov 2016 21:25:45 -0800 Subject: Create animation page and clean up index Add guidance on timings and easing [ci skip] Detail when not to use easing Add dropdown and hover examples Add quick update animation --- doc/development/ux_guide/animation.md | 42 +++++++++++++++++++++ doc/development/ux_guide/basics.md | 15 -------- doc/development/ux_guide/copy.md | 4 +- .../ux_guide/img/animation-dropdown.gif | Bin 0 -> 22483 bytes doc/development/ux_guide/img/animation-hover.gif | Bin 0 -> 247388 bytes .../ux_guide/img/animation-quickupdate.gif | Bin 0 -> 6441 bytes doc/development/ux_guide/index.md | 17 ++++++--- 7 files changed, 56 insertions(+), 22 deletions(-) create mode 100644 doc/development/ux_guide/animation.md create mode 100644 doc/development/ux_guide/img/animation-dropdown.gif create mode 100644 doc/development/ux_guide/img/animation-hover.gif create mode 100644 doc/development/ux_guide/img/animation-quickupdate.gif diff --git a/doc/development/ux_guide/animation.md b/doc/development/ux_guide/animation.md new file mode 100644 index 00000000000..daeb15460c2 --- /dev/null +++ b/doc/development/ux_guide/animation.md @@ -0,0 +1,42 @@ +# Animation + +Motion is a tool to help convey important relationships, changes or transitions between elements. It should be used sparingly and intentionally, highlighting the right elements at the right moment. + +## Timings + +The longer distance an object travel, the timing should be longer for the animation. However, when in doubt, we should avoid large, full screen animations. + +Subtle animations, or objects leaving the screen should take **100-200 milliseconds**. Objects entering the screen, or motion we want to use to direct user attention can take between **200-400 milliseconds**. We should avoid animations of longer than 400 milliseconds as they will make the experience appear sluggish. If a specific animation feels like it will need more than 400 milliseconds, revisit the animation to see if there is a simpler, easier, shorter animation to implement. + +## Easing + +Easing specifies the rate of change of a parameter over time (see [easings.net](http://easings.net/)). Adding an easing curve will make the motion feel more natural. Being consistent with the easing curves will make the whole experience feel more cohesive and connected. + +* When an object is entering the screen, or transforming the scale, position, or shape, use the **easeOutQuint** curve (`cubic-bezier(0.23, 1, 0.32, 1)`) +* When an object is leaving the screen, or transforming the opacity or color, no easing curve is needed. It shouldn't _slow down_ as it is exiting the screen, as that draws attention on the leaving object, where we don't want it. Adding easing to opacity and color transitions will make the motion appear less smooth. Therefore, for these cases, motion should just be **linear**. + +## Types of animations + +### Hover + +Interactive elements (links, buttons, etc.) should have a hover state. A subtle animation for this transition adds a polished feel. We should target a `200ms linear` transition for a color hover effect. + +View the [interactive example](http://codepen.io/awhildy/full/GNyEvM/) here. + +![Hover animation](img/animation-hover.gif) + +### Dropdowns + +The dropdown menu should feel like it is appearing from the triggering element. Combining a position shift `400ms cubic-bezier(0.23, 1, 0.32, 1)` with a opacity animation `200ms linear` on the second half of the motion achieves this affect. + +View the [interactive example](http://codepen.io/awhildy/full/jVLJpb/) here. + +![Dropdown animation](img/animation-dropdown.gif) + +### Quick update + +When information is updating in place, a quick, subtle animation is needed. The previous content should cut out, and the new content should have a quick, `200ms linear` fade in. + +![Quick update animation](img/animation-quickupdate.gif) + +> TODO: Add guidance for other kinds of animation \ No newline at end of file diff --git a/doc/development/ux_guide/basics.md b/doc/development/ux_guide/basics.md index a29cfa096b2..76b739386a5 100644 --- a/doc/development/ux_guide/basics.md +++ b/doc/development/ux_guide/basics.md @@ -5,8 +5,6 @@ * [Typography](#typography) * [Icons](#icons) * [Color](#color) -* [Motion](#motion) -* [Voice and tone](#voice-and-tone) --- @@ -61,16 +59,3 @@ GitLab uses Font Awesome icons throughout our interface. > TODO: Establish a perspective for color in terms of our personality and rationalize with Marketing usage. ---- - -## Motion - -Motion is a tool to help convey important relationships, changes or transitions between elements. It should be used sparingly and intentionally, highlighting the right elements at the right moment. - -> TODO: Determine a more concrete perspective on motion, create consistent easing/timing curves to follow. - ---- - -## Voice and tone - -The copy for GitLab is clear and direct. We strike a clear balance between professional and friendly. We can empathesize with users (such as celebrating completing all Todos), and remain respectful of the importance of the work. We are that trusted, friendly coworker that is helpful and understanding. diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md index b557fb47120..8896d450f14 100644 --- a/doc/development/ux_guide/copy.md +++ b/doc/development/ux_guide/copy.md @@ -1,6 +1,8 @@ # Copy -The copy and messaging is a core part of the experience of GitLab and the conversation with our users. Follow the below conventions throughout GitLab. +The copy for GitLab is clear and direct. We strike a clear balance between professional and friendly. We can empathesize with users (such as celebrating completing all Todos), and remain respectful of the importance of the work. We are that trusted, friendly coworker that is helpful and understanding. + +The copy and messaging is a core part of the experience of GitLab and the conversation with our users. Follow the below conventions throughout GitLab. >**Note:** We are currently inconsistent with this guidance. Images below are created to illustrate the point. As this guidance is refined, we will ensure that our experiences align. diff --git a/doc/development/ux_guide/img/animation-dropdown.gif b/doc/development/ux_guide/img/animation-dropdown.gif new file mode 100644 index 00000000000..c9b31d26165 Binary files /dev/null and b/doc/development/ux_guide/img/animation-dropdown.gif differ diff --git a/doc/development/ux_guide/img/animation-hover.gif b/doc/development/ux_guide/img/animation-hover.gif new file mode 100644 index 00000000000..37ad9c76828 Binary files /dev/null and b/doc/development/ux_guide/img/animation-hover.gif differ diff --git a/doc/development/ux_guide/img/animation-quickupdate.gif b/doc/development/ux_guide/img/animation-quickupdate.gif new file mode 100644 index 00000000000..8db70bc3d24 Binary files /dev/null and b/doc/development/ux_guide/img/animation-quickupdate.gif differ diff --git a/doc/development/ux_guide/index.md b/doc/development/ux_guide/index.md index 8aed11ebac3..8a849f239dc 100644 --- a/doc/development/ux_guide/index.md +++ b/doc/development/ux_guide/index.md @@ -12,7 +12,17 @@ These guiding principles set a solid foundation for our design system, and shoul --- ### [Basics](basics.md) -The basic ingredients of our experience establish our personality and feel. This section includes details about typography, color, and motion. +The basic ingredients of our experience establish our personality and feel. This section includes details about typography, iconography, and color. + +--- + +### [Animation](animation.md) +Guidance on the timing, curving and motion for GitLab. + +--- + +### [Copy](copy.md) +Conventions on text and messaging within labels, buttons, and other components. --- @@ -26,11 +36,6 @@ The GitLab experience is broken apart into several surfaces. Each of these surfa --- -### [Copy](copy.md) -Conventions on text and messaging within labels, buttons, and other components. - ---- - ### [Features](features.md) The previous building blocks are combined into complete features in the GitLab UX. Examples include our navigation, filters, search results, and empty states. -- cgit v1.2.1 From 29ceb98b5162677601702704e89d845580372078 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@gitlab.com> Date: Wed, 30 Nov 2016 08:11:43 +0000 Subject: Merge branch 'issue_25064' into 'security' Ensure state param has a valid value when filtering issuables. Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/25064 This fix makes sure we only call safe methods on issuable when filtering by state. See merge request !2038 --- app/finders/issuable_finder.rb | 13 +++++--- ...lidate-state-param-when-filtering-issuables.yml | 4 +++ spec/finders/issues_finder_spec.rb | 37 +++++++++++++++++++++- 3 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 changelogs/unreleased/validate-state-param-when-filtering-issuables.yml diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 001c83ccb4b..9560e9d518e 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -7,7 +7,7 @@ # current_user - which user use # params: # scope: 'created-by-me' or 'assigned-to-me' or 'all' -# state: 'open' or 'closed' or 'all' +# state: 'opened' or 'closed' or 'all' # group_id: integer # project_id: integer # milestone_title: string @@ -207,10 +207,13 @@ class IssuableFinder end def by_state(items) - params[:state] ||= 'all' - - if items.respond_to?(params[:state]) - items.public_send(params[:state]) + case params[:state].to_s + when 'closed' + items.closed + when 'merged' + items.respond_to?(:merged) ? items.merged : items.closed + when 'opened' + items.opened else items end diff --git a/changelogs/unreleased/validate-state-param-when-filtering-issuables.yml b/changelogs/unreleased/validate-state-param-when-filtering-issuables.yml new file mode 100644 index 00000000000..3fb025806b0 --- /dev/null +++ b/changelogs/unreleased/validate-state-param-when-filtering-issuables.yml @@ -0,0 +1,4 @@ +--- +title: Validate state param when filtering issuables +merge_request: +author: diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 40bccb8e50b..7f69e888f32 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -10,6 +10,7 @@ describe IssuesFinder do let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone, title: 'gitlab') } let(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'gitlab') } let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2) } + let(:closed_issue) { create(:issue, author: user2, assignee: user2, project: project2, state: 'closed') } let!(:label_link) { create(:label_link, label: label, target: issue2) } before do @@ -25,7 +26,7 @@ describe IssuesFinder do describe '#execute' do let(:search_user) { user } let(:params) { {} } - let(:issues) { IssuesFinder.new(search_user, params.merge(scope: scope, state: 'opened')).execute } + let(:issues) { IssuesFinder.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute } context 'scope: all' do let(:scope) { 'all' } @@ -143,6 +144,40 @@ describe IssuesFinder do end end + context 'filtering by state' do + context 'with opened' do + let(:params) { { state: 'opened' } } + + it 'returns only opened issues' do + expect(issues).to contain_exactly(issue1, issue2, issue3) + end + end + + context 'with closed' do + let(:params) { { state: 'closed' } } + + it 'returns only closed issues' do + expect(issues).to contain_exactly(closed_issue) + end + end + + context 'with all' do + let(:params) { { state: 'all' } } + + it 'returns all issues' do + expect(issues).to contain_exactly(issue1, issue2, issue3, closed_issue) + end + end + + context 'with invalid state' do + let(:params) { { state: 'invalid_state' } } + + it 'returns all issues' do + expect(issues).to contain_exactly(issue1, issue2, issue3, closed_issue) + end + end + end + context 'when the user is unauthorized' do let(:search_user) { nil } -- cgit v1.2.1 From d6c2f5f5699495260aa2a99ea09a1764e9ec5e9c Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Tue, 6 Dec 2016 15:46:48 +1100 Subject: Use `--extended-regexp` in lint-doc for compatibility with Darwin grep --- scripts/lint-doc.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh index 7c4e8276902..62236ed539a 100755 --- a/scripts/lint-doc.sh +++ b/scripts/lint-doc.sh @@ -3,7 +3,7 @@ cd "$(dirname "$0")/.." # Use long options (e.g. --header instead of -H) for curl examples in documentation. -grep --perl-regexp --recursive --color=auto 'curl (.+ )?-[^- ].*' doc/ +grep --extended-regexp --recursive --color=auto 'curl (.+ )?-[^- ].*' doc/ if [ $? == 0 ] then echo '✖ ERROR: Short options should not be used in documentation!' >&2 -- cgit v1.2.1 From 4e249d5baea99a52915025fc2827d01ab2d77c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Fri, 2 Dec 2016 13:54:57 +0100 Subject: Use :maximum instead of :within for length validators with a 0..N range MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- app/models/ci/variable.rb | 4 ++-- app/models/concerns/issuable.rb | 2 +- app/models/environment.rb | 2 +- app/models/key.rb | 16 ++++++++++++---- app/models/namespace.rb | 14 +++++++------- app/models/project.rb | 4 ++-- app/models/snippet.rb | 8 ++++++-- spec/lib/gitlab/github_import/importer_spec.rb | 2 +- spec/models/ci/variable_spec.rb | 7 +++++++ spec/models/concerns/issuable_spec.rb | 2 +- spec/models/environment_spec.rb | 4 ++-- spec/models/key_spec.rb | 8 ++++++-- spec/models/namespace_spec.rb | 13 ++++++++++--- spec/models/project_spec.rb | 10 +++++++--- spec/models/snippet_spec.rb | 24 ++++++++++++++++++++++-- spec/models/user_spec.rb | 2 +- spec/requests/api/deploy_keys_spec.rb | 4 +--- spec/support/matchers/is_within.rb | 9 --------- 18 files changed, 89 insertions(+), 46 deletions(-) delete mode 100644 spec/support/matchers/is_within.rb diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index 94d9e2b3208..2c8698d8b5d 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -4,10 +4,10 @@ module Ci belongs_to :project, foreign_key: :gl_project_id - validates_uniqueness_of :key, scope: :gl_project_id validates :key, presence: true, - length: { within: 0..255 }, + uniqueness: { scope: :gl_project_id }, + length: { maximum: 255 }, format: { with: /\A[a-zA-Z0-9_]+\z/, message: "can contain only letters, digits and '_'." } diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 69d8afc45da..0ea7b1b1098 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -41,7 +41,7 @@ module Issuable has_one :metrics validates :author, presence: true - validates :title, presence: true, length: { within: 0..255 } + validates :title, presence: true, length: { maximum: 255 } scope :authored, ->(user) { where(author_id: user) } scope :assigned_to, ->(u) { where(assignee_id: u.id)} diff --git a/app/models/environment.rb b/app/models/environment.rb index a7f4156fc2e..96700143ddd 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -9,7 +9,7 @@ class Environment < ActiveRecord::Base validates :name, presence: true, uniqueness: { scope: :project_id }, - length: { within: 0..255 }, + length: { maximum: 255 }, format: { with: Gitlab::Regex.environment_name_regex, message: Gitlab::Regex.environment_name_regex_message } diff --git a/app/models/key.rb b/app/models/key.rb index ff8dda2dc89..a5d25409730 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -8,10 +8,18 @@ class Key < ActiveRecord::Base before_validation :generate_fingerprint - validates :title, presence: true, length: { within: 0..255 } - validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ } - validates :key, format: { without: /\n|\r/, message: 'should be a single line' } - validates :fingerprint, uniqueness: true, presence: { message: 'cannot be generated' } + validates :title, + presence: true, + length: { maximum: 255 } + validates :key, + presence: true, + length: { maximum: 5000 }, + format: { with: /\A(ssh|ecdsa)-.*\Z/ } + validates :key, + format: { without: /\n|\r/, message: 'should be a single line' } + validates :fingerprint, + uniqueness: true, + presence: { message: 'cannot be generated' } delegate :name, :email, to: :user, prefix: true diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 891dffac648..7a545f752b6 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -12,17 +12,17 @@ class Namespace < ActiveRecord::Base validates :owner, presence: true, unless: ->(n) { n.type == "Group" } validates :name, - length: { within: 0..255 }, - namespace_name: true, presence: true, - uniqueness: true + uniqueness: true, + length: { maximum: 255 }, + namespace_name: true - validates :description, length: { within: 0..255 } + validates :description, length: { maximum: 255 } validates :path, - length: { within: 1..255 }, - namespace: true, presence: true, - uniqueness: { case_sensitive: false } + uniqueness: { case_sensitive: false }, + length: { maximum: 255 }, + namespace: true delegate :name, to: :owner, allow_nil: true, prefix: true diff --git a/app/models/project.rb b/app/models/project.rb index f01cb613b85..e783e455a49 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -172,13 +172,13 @@ class Project < ActiveRecord::Base validates :description, length: { maximum: 2000 }, allow_blank: true validates :name, presence: true, - length: { within: 0..255 }, + length: { maximum: 255 }, format: { with: Gitlab::Regex.project_name_regex, message: Gitlab::Regex.project_name_regex_message } validates :path, presence: true, project_path: true, - length: { within: 0..255 }, + length: { maximum: 255 }, format: { with: Gitlab::Regex.project_path_regex, message: Gitlab::Regex.project_path_regex_message } validates :namespace, presence: true diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 8ff4e7ae718..99493e561ab 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -27,9 +27,9 @@ class Snippet < ActiveRecord::Base delegate :name, :email, to: :author, prefix: true, allow_nil: true validates :author, presence: true - validates :title, presence: true, length: { within: 0..255 } + validates :title, presence: true, length: { maximum: 255 } validates :file_name, - length: { within: 0..255 }, + length: { maximum: 255 }, format: { with: Gitlab::Regex.file_name_regex, message: Gitlab::Regex.file_name_regex_message } @@ -94,6 +94,10 @@ class Snippet < ActiveRecord::Base 0 end + def file_name + super.to_s + end + # alias for compatibility with blobs and highlighting def path file_name diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index 000b9aa6f83..9e027839f59 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -155,7 +155,7 @@ describe Gitlab::GithubImport::Importer, lib: true do message: 'The remote data could not be fully imported.', errors: [ { type: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" }, - { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank, Title is too short (minimum is 0 characters)" }, + { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank" }, { type: :wiki, errors: "Gitlab::Shell::Error" }, { type: :release, url: 'https://api.github.com/repos/octocat/Hello-World/releases/2', errors: "Validation failed: Description can't be blank" } ] diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb index 4e7833c3162..bee9f714849 100644 --- a/spec/models/ci/variable_spec.rb +++ b/spec/models/ci/variable_spec.rb @@ -5,6 +5,13 @@ describe Ci::Variable, models: true do let(:secret_value) { 'secret' } + it { is_expected.to validate_presence_of(:key) } + it { is_expected.to validate_uniqueness_of(:key).scoped_to(:gl_project_id) } + it { is_expected.to validate_length_of(:key).is_at_most(255) } + it { is_expected.to allow_value('foo').for(:key) } + it { is_expected.not_to allow_value('foo bar').for(:key) } + it { is_expected.not_to allow_value('foo/bar').for(:key) } + before :each do subject.value = secret_value end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 6f84bffe046..4fa06a8c60a 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -35,7 +35,7 @@ describe Issue, "Issuable" do it { is_expected.to validate_presence_of(:iid) } it { is_expected.to validate_presence_of(:author) } it { is_expected.to validate_presence_of(:title) } - it { is_expected.to validate_length_of(:title).is_at_least(0).is_at_most(255) } + it { is_expected.to validate_length_of(:title).is_at_most(255) } end describe "Scope" do diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index d06665197db..c8170164898 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -13,9 +13,9 @@ describe Environment, models: true do it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) } - it { is_expected.to validate_length_of(:name).is_within(0..255) } + it { is_expected.to validate_length_of(:name).is_at_most(255) } - it { is_expected.to validate_length_of(:external_url).is_within(0..255) } + it { is_expected.to validate_length_of(:external_url).is_at_most(255) } # To circumvent a not null violation of the name column: # https://github.com/thoughtbot/shoulda-matchers/issues/336 diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 90731f55470..2a33d819138 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -7,9 +7,13 @@ describe Key, models: true do describe "Validation" do it { is_expected.to validate_presence_of(:title) } + it { is_expected.to validate_length_of(:title).is_at_most(255) } + it { is_expected.to validate_presence_of(:key) } - it { is_expected.to validate_length_of(:title).is_within(0..255) } - it { is_expected.to validate_length_of(:key).is_within(0..5000) } + it { is_expected.to validate_length_of(:key).is_at_most(5000) } + it { is_expected.to allow_value('ssh-foo').for(:key) } + it { is_expected.to allow_value('ecdsa-foo').for(:key) } + it { is_expected.not_to allow_value('foo-bar').for(:key) } end describe "Methods" do diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 431b3e4435f..ba0ed4a3603 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -4,11 +4,18 @@ describe Namespace, models: true do let!(:namespace) { create(:namespace) } it { is_expected.to have_many :projects } - it { is_expected.to validate_presence_of :name } + + it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_uniqueness_of(:name) } - it { is_expected.to validate_presence_of :path } + it { is_expected.to validate_length_of(:name).is_at_most(255) } + + it { is_expected.to validate_length_of(:description).is_at_most(255) } + + it { is_expected.to validate_presence_of(:path) } it { is_expected.to validate_uniqueness_of(:path) } - it { is_expected.to validate_presence_of :owner } + it { is_expected.to validate_length_of(:path).is_at_most(255) } + + it { is_expected.to validate_presence_of(:owner) } describe "Mass assignment" do end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 8abcce42ce0..f7c8c97fdee 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -131,14 +131,18 @@ describe Project, models: true do it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_uniqueness_of(:name).scoped_to(:namespace_id) } - it { is_expected.to validate_length_of(:name).is_within(0..255) } + it { is_expected.to validate_length_of(:name).is_at_most(255) } it { is_expected.to validate_presence_of(:path) } it { is_expected.to validate_uniqueness_of(:path).scoped_to(:namespace_id) } - it { is_expected.to validate_length_of(:path).is_within(0..255) } - it { is_expected.to validate_length_of(:description).is_within(0..2000) } + it { is_expected.to validate_length_of(:path).is_at_most(255) } + + it { is_expected.to validate_length_of(:description).is_at_most(2000) } + it { is_expected.to validate_presence_of(:creator) } + it { is_expected.to validate_presence_of(:namespace) } + it { is_expected.to validate_presence_of(:repository_storage) } it 'does not allow new projects beyond user limits' do diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index f62f6bacbaa..279dc30c357 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -23,9 +23,9 @@ describe Snippet, models: true do it { is_expected.to validate_presence_of(:author) } it { is_expected.to validate_presence_of(:title) } - it { is_expected.to validate_length_of(:title).is_within(0..255) } + it { is_expected.to validate_length_of(:title).is_at_most(255) } - it { is_expected.to validate_length_of(:file_name).is_within(0..255) } + it { is_expected.to validate_length_of(:file_name).is_at_most(255) } it { is_expected.to validate_presence_of(:content) } @@ -46,6 +46,26 @@ describe Snippet, models: true do end end + describe '#file_name' do + let(:project) { create(:empty_project) } + + context 'file_name is nil' do + let(:snippet) { create(:snippet, project: project, file_name: nil) } + + it 'returns an empty string' do + expect(snippet.file_name).to eq '' + end + end + + context 'file_name is not nil' do + let(:snippet) { create(:snippet, project: project, file_name: 'foo.txt') } + + it 'returns the file_name' do + expect(snippet.file_name).to eq 'foo.txt' + end + end + end + describe "#content_html_invalidated?" do let(:snippet) { create(:snippet, content: "md", content_html: "html", file_name: "foo.md") } it "invalidates the HTML cache of content when the filename changes" do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 14c891994d0..95fe2dc65d9 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -79,7 +79,7 @@ describe User, models: true do it { is_expected.to allow_value(0).for(:projects_limit) } it { is_expected.not_to allow_value(-1).for(:projects_limit) } - it { is_expected.to validate_length_of(:bio).is_within(0..255) } + it { is_expected.to validate_length_of(:bio).is_at_most(255) } it_behaves_like 'an object with email-formated attributes', :email do subject { build(:user) } diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 5fa7299044e..aabab8e6ae6 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -75,7 +75,6 @@ describe API::DeployKeys, api: true do expect(response).to have_http_status(400) expect(json_response['message']['key']).to eq([ 'can\'t be blank', - 'is too short (minimum is 0 characters)', 'is invalid' ]) end @@ -85,8 +84,7 @@ describe API::DeployKeys, api: true do expect(response).to have_http_status(400) expect(json_response['message']['title']).to eq([ - 'can\'t be blank', - 'is too short (minimum is 0 characters)' + 'can\'t be blank' ]) end diff --git a/spec/support/matchers/is_within.rb b/spec/support/matchers/is_within.rb deleted file mode 100644 index 0c35fc7e899..00000000000 --- a/spec/support/matchers/is_within.rb +++ /dev/null @@ -1,9 +0,0 @@ -# Extend shoulda-matchers -module Shoulda::Matchers::ActiveModel - class ValidateLengthOfMatcher - # Shortcut for is_at_least and is_at_most - def is_within(range) - is_at_least(range.min) && is_at_most(range.max) - end - end -end -- cgit v1.2.1 From b1d6dd4c7479d01ac8b3392c55fb3c24dc18c4ae Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 6 Dec 2016 11:00:52 +0100 Subject: Restore legacy statuses support in ci status helpers --- app/helpers/ci_status_helper.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index ff507765255..da0ebc0040a 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -23,6 +23,8 @@ module CiStatusHelper case status when 'success' 'passed' + when 'success_with_warnings' + 'passed with warnings' else status end @@ -41,6 +43,8 @@ module CiStatusHelper case status when 'success' 'icon_status_success' + when 'success_with_warnings' + 'icon_status_warning' when 'failed' 'icon_status_failed' when 'pending' -- cgit v1.2.1 From 40a118072d82dab27588b7907b26642e9212f969 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 6 Dec 2016 11:02:12 +0100 Subject: Remove unsued variable from merge request widget --- app/views/projects/merge_requests/widget/_heading.html.haml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 6d9b91ad0e7..9ab7971b56c 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -8,14 +8,13 @@ = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline' = ci_label_for_status(status) for - - commit = @merge_request.diff_head_commit = succeed "." do = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace" %span.ci-coverage - 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 + - # TODO, remove in later versions when services like Jenkins will set CI status via Commit status API .mr-widget-heading - %w[success skipped canceled failed running pending].each do |status| .ci_widget{class: "ci-#{status}", style: "display:none"} -- cgit v1.2.1 From c8b2aa8de527099cf902d5fbfd058f9dee772f24 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 6 Dec 2016 11:05:01 +0100 Subject: Simplify ci status helper with detailed status --- app/helpers/ci_status_helper.rb | 44 ++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index da0ebc0040a..29ab1404f41 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -36,30 +36,30 @@ module CiStatusHelper end def ci_icon_for_status(status) + if detailed_status?(status) + return custom_icon(status.icon) + end + icon_name = - if detailed_status?(status) - status.icon + case status + when 'success' + 'icon_status_success' + when 'success_with_warnings' + 'icon_status_warning' + when 'failed' + 'icon_status_failed' + when 'pending' + 'icon_status_pending' + when 'running' + 'icon_status_running' + when 'play' + 'icon_play' + when 'created' + 'icon_status_created' + when 'skipped' + 'icon_status_skipped' else - case status - when 'success' - 'icon_status_success' - when 'success_with_warnings' - 'icon_status_warning' - when 'failed' - 'icon_status_failed' - when 'pending' - 'icon_status_pending' - when 'running' - 'icon_status_running' - when 'play' - 'icon_play' - when 'created' - 'icon_status_created' - when 'skipped' - 'icon_status_skipped' - else - 'icon_status_canceled' - end + 'icon_status_canceled' end custom_icon(icon_name) -- cgit v1.2.1 From bdc13c3142507650e9170cc7b9b63232bb1cfdad Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 6 Dec 2016 11:22:31 +0100 Subject: Untangle status label and text in ci status helper --- app/helpers/ci_status_helper.rb | 12 ++++++++++-- lib/gitlab/ci/status/core.rb | 11 +++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 29ab1404f41..8e19752a8a1 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -5,8 +5,8 @@ module CiStatusHelper end def ci_status_with_icon(status, target = nil) - content = ci_icon_for_status(status) + ci_label_for_status(status) - klass = "ci-status ci-#{status}" # TODO, add support for detailed status + content = ci_icon_for_status(status) + ci_text_for_status(status) + klass = "ci-status ci-#{status}" if target link_to content, target, class: klass @@ -15,6 +15,14 @@ module CiStatusHelper end end + def ci_text_for_status(status) + if detailed_status?(status) + status.text + else + status + end + end + def ci_label_for_status(status) if detailed_status?(status) return status.label diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb index fbfe257eeca..10e04d5be03 100644 --- a/lib/gitlab/ci/status/core.rb +++ b/lib/gitlab/ci/status/core.rb @@ -22,6 +22,17 @@ module Gitlab "#{@subject.class.name.demodulize}: #{label}" end + # Deprecation warning: this method is here because we need to maintain + # backwards compatibility with legacy statuses. We often do something + # like "ci-status ci-status-#{status}" to set CSS class. + # + # `to_s` method should be renamed to `group` at some point, after + # phasing legacy satuses out. + # + def to_s + self.class.name.demodulize.downcase + end + def has_details? raise NotImplementedError end -- cgit v1.2.1 From 79e132fad4a3428c1d65e877be65d094a559649c Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 6 Dec 2016 11:31:41 +0100 Subject: Add status label information to pipeline header --- app/views/projects/pipelines/_info.html.haml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 229bdfb0e8d..bbfaa6391a1 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -2,6 +2,7 @@ .header-main-content = ci_status_with_icon(@pipeline.detailed_status) %strong Pipeline ##{@commit.pipelines.last.id} + #{ci_label_for_status(@pipeline.detailed_status)}, triggered #{time_ago_with_tooltip(@commit.authored_date)} by = author_avatar(@commit, size: 24) = commit_author_link(@commit) -- cgit v1.2.1 From e94f378b619849c46bf6b736ea9c61c19956b57f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 6 Dec 2016 12:16:52 +0100 Subject: Improve support for icons in new detailed statuses --- app/views/projects/pipelines/_info.html.haml | 1 - lib/gitlab/ci/status/core.rb | 2 +- lib/gitlab/ci/status/pipeline/success_with_warnings.rb | 4 ++++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index bbfaa6391a1..229bdfb0e8d 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -2,7 +2,6 @@ .header-main-content = ci_status_with_icon(@pipeline.detailed_status) %strong Pipeline ##{@commit.pipelines.last.id} - #{ci_label_for_status(@pipeline.detailed_status)}, triggered #{time_ago_with_tooltip(@commit.authored_date)} by = author_avatar(@commit, size: 24) = commit_author_link(@commit) diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb index 10e04d5be03..ce4108fdcf2 100644 --- a/lib/gitlab/ci/status/core.rb +++ b/lib/gitlab/ci/status/core.rb @@ -30,7 +30,7 @@ module Gitlab # phasing legacy satuses out. # def to_s - self.class.name.demodulize.downcase + self.class.name.demodulize.downcase.underscore end def has_details? diff --git a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb index 97dfba81ff5..4b040d60df8 100644 --- a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb +++ b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb @@ -17,6 +17,10 @@ module Gitlab 'icon_status_warning' end + def to_s + 'success_with_warnings' + end + def self.matches?(pipeline) pipeline.success? && pipeline.has_warnings? end -- cgit v1.2.1 From 67cd3b36382ed2fafc29a620112a9de96f3899f6 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 6 Dec 2016 12:24:27 +0100 Subject: Use status text when redering pipelines list --- app/views/projects/ci/pipelines/_pipeline.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 00a30870dad..a037c7ab37c 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -7,7 +7,7 @@ %td.commit-link = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "ci-status ci-#{status}" do = ci_icon_for_status(detailed_status) - = ci_label_for_status(detailed_status) + = ci_text_for_status(detailed_status) %td = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do -- cgit v1.2.1 From 157f4fe17fdd0044229c6830bb85fd26759a69cc Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 6 Dec 2016 12:28:07 +0100 Subject: Use detailed status as CSS class in pipelines list --- app/views/projects/ci/pipelines/_pipeline.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index a037c7ab37c..0f08f4e8592 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -5,7 +5,7 @@ %tr.commit %td.commit-link - = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "ci-status ci-#{status}" do + = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "ci-status ci-#{detailed_status}" do = ci_icon_for_status(detailed_status) = ci_text_for_status(detailed_status) -- cgit v1.2.1 From 7b99b18643b443109d790940ffdf40c301e2c85d Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 6 Dec 2016 12:33:53 +0100 Subject: Add text example for pipeline status without action --- spec/lib/gitlab/ci/status/pipeline/factory_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb index 543dae0640d..d6243940f2e 100644 --- a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb @@ -23,7 +23,9 @@ describe Gitlab::Ci::Status::Pipeline::Factory do it 'extends core status with common pipeline methods' do expect(status).to have_details - expect(status.details_path).to include "pipelines/#{pipeline.id}" + expect(status).not_to have_action + expect(status.details_path) + .to include "pipelines/#{pipeline.id}" end end end -- cgit v1.2.1 From 84f2c219aa33de4890c7681372dd03309f216795 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Tue, 6 Dec 2016 13:46:59 +0200 Subject: Fix importing inline comment for any diff type --- lib/bitbucket/representation/pull_request_comment.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/bitbucket/representation/pull_request_comment.rb b/lib/bitbucket/representation/pull_request_comment.rb index 38090188919..c63d749cba7 100644 --- a/lib/bitbucket/representation/pull_request_comment.rb +++ b/lib/bitbucket/representation/pull_request_comment.rb @@ -10,11 +10,11 @@ module Bitbucket end def old_pos - inline.fetch('from', nil) || 1 + inline.fetch('from', nil) end def new_pos - inline.fetch('to', nil) || old_pos || 1 + inline.fetch('to', nil) end def parent_id -- cgit v1.2.1 From 5eb12bd75fff65bb2ea8677cc317877e45d3d6f8 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Tue, 6 Dec 2016 13:22:45 +0100 Subject: Remove caching of Repository#has_visible_content? This method already uses the cached method Repository#branch_count so there's no point in also caching has_visible_content?. Fixes gitlab-org/gitlab-ce#25278 --- app/models/repository.rb | 14 +--------- .../remove-has-visible-content-caching.yml | 4 +++ spec/models/repository_spec.rb | 31 ++-------------------- 3 files changed, 7 insertions(+), 42 deletions(-) create mode 100644 changelogs/unreleased/remove-has-visible-content-caching.yml diff --git a/app/models/repository.rb b/app/models/repository.rb index e2e7d08abac..3c4b0212af7 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -85,11 +85,7 @@ class Repository # This method return true if repository contains some content visible in project page. # def has_visible_content? - return @has_visible_content unless @has_visible_content.nil? - - @has_visible_content = cache.fetch(:has_visible_content?) do - branch_count > 0 - end + branch_count > 0 end def commit(ref = 'HEAD') @@ -374,12 +370,6 @@ class Repository return unless empty? expire_method_caches(%i(empty?)) - expire_has_visible_content_cache - end - - def expire_has_visible_content_cache - cache.expire(:has_visible_content?) - @has_visible_content = nil end def lookup_cache @@ -467,7 +457,6 @@ class Repository # Runs code after a new branch has been created. def after_create_branch expire_branches_cache - expire_has_visible_content_cache repository_event(:push_branch) end @@ -481,7 +470,6 @@ class Repository # Runs code after an existing branch has been removed. def after_remove_branch - expire_has_visible_content_cache expire_branches_cache end diff --git a/changelogs/unreleased/remove-has-visible-content-caching.yml b/changelogs/unreleased/remove-has-visible-content-caching.yml new file mode 100644 index 00000000000..e2940c60443 --- /dev/null +++ b/changelogs/unreleased/remove-has-visible-content-caching.yml @@ -0,0 +1,4 @@ +--- +title: Remove visible content caching +merge_request: +author: diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index b797d19161d..d9b0e63eeb6 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -768,7 +768,6 @@ describe Repository, models: true do expect(repository).not_to receive(:expire_root_ref_cache) expect(repository).not_to receive(:expire_emptiness_caches) expect(repository).to receive(:expire_branches_cache) - expect(repository).to receive(:expire_has_visible_content_cache) repository.update_branch_with_hooks(user, 'new-feature') { new_rev } end @@ -786,7 +785,6 @@ describe Repository, models: true do expect(empty_repository).to receive(:expire_root_ref_cache) expect(empty_repository).to receive(:expire_emptiness_caches) expect(empty_repository).to receive(:expire_branches_cache) - expect(empty_repository).to receive(:expire_has_visible_content_cache) empty_repository.commit_file(user, 'CHANGELOG', 'Changelog!', 'Updates file content', 'master', false) @@ -829,15 +827,6 @@ describe Repository, models: true do expect(subject).to eq(true) end - - it 'caches the output' do - expect(repository).to receive(:branch_count). - once. - and_return(3) - - repository.has_visible_content? - repository.has_visible_content? - end end end @@ -918,20 +907,6 @@ describe Repository, models: true do end end - describe '#expire_has_visible_content_cache' do - it 'expires the visible content cache' do - repository.has_visible_content? - - expect(repository).to receive(:branch_count). - once. - and_return(0) - - repository.expire_has_visible_content_cache - - expect(repository.has_visible_content?).to eq(false) - end - end - describe '#expire_branch_cache' do # This method is private but we need it for testing purposes. Sadly there's # no other proper way of testing caching operations. @@ -967,7 +942,6 @@ describe Repository, models: true do allow(repository).to receive(:empty?).and_return(true) expect(cache).to receive(:expire).with(:empty?) - expect(repository).to receive(:expire_has_visible_content_cache) repository.expire_emptiness_caches end @@ -976,7 +950,6 @@ describe Repository, models: true do allow(repository).to receive(:empty?).and_return(false) expect(cache).not_to receive(:expire).with(:empty?) - expect(repository).not_to receive(:expire_has_visible_content_cache) repository.expire_emptiness_caches end @@ -1204,7 +1177,7 @@ describe Repository, models: true do describe '#after_create_branch' do it 'flushes the visible content cache' do - expect(repository).to receive(:expire_has_visible_content_cache) + expect(repository).to receive(:expire_branches_cache) repository.after_create_branch end @@ -1212,7 +1185,7 @@ describe Repository, models: true do describe '#after_remove_branch' do it 'flushes the visible content cache' do - expect(repository).to receive(:expire_has_visible_content_cache) + expect(repository).to receive(:expire_branches_cache) repository.after_remove_branch end -- cgit v1.2.1 From 4e06818d330b4f15f334561500d506f0d24b9dfe Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 10 Nov 2016 15:32:23 +0100 Subject: Support pipelines API Pass `updated_at` to get only incremental changes since last update --- app/models/ci/pipeline.rb | 29 ++++++++++++++++++---- app/models/commit_status.rb | 11 +------- .../projects/ci/pipelines/_pipeline.html.haml | 7 +++--- app/views/projects/commit/_pipeline.html.haml | 2 +- app/views/projects/commit/_pipelines_list.haml | 2 +- app/views/projects/pipelines/index.html.haml | 2 +- 6 files changed, 32 insertions(+), 21 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index caf6908505e..859c6b483f4 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -98,19 +98,38 @@ module Ci sha[0...8] end - def self.stages - # We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries - CommitStatus.where(pipeline: pluck(:id)).stages - end - def self.total_duration where.not(duration: nil).sum(:duration) end + def stages + statuses.group('stage').select(:stage) + .order('max(stage_idx)') + end + + def stages_with_statuses + status_sql = statuses.latest.where('stage=sg.stage').status_sql + + stages_with_statuses = CommitStatus.from(self.stages, :sg). + pluck('sg.stage', status_sql) + + stages_with_statuses.map do |stage| + OpenStruct.new( + name: stage.first, + status: stage.last, + pipeline: self + ) + end + end + def stages_with_latest_statuses statuses.latest.includes(project: :namespace).order(:stage_idx).group_by(&:stage) end + def artifacts + builds.latest.with_artifacts_not_expired + end + def project_id project.id end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index c345bf293c9..d9021a38ce3 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -119,16 +119,7 @@ class CommitStatus < ActiveRecord::Base def self.stages # We group by stage name, but order stages by theirs' index - unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage') - end - - def self.stages_status - # We execute subquery for each stage to calculate a stage status - statuses = unscoped.from(all, :sg).group('stage').pluck('sg.stage', all.where('stage=sg.stage').status_sql) - statuses.inject({}) do |h, k| - h[k.first] = k.last - h - end + unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').select('sg.stage') end def failed_but_allowed? diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 0f08f4e8592..d42df00b47f 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -45,9 +45,10 @@ - stages_status = pipeline.statuses.latest.stages_status %td.stage-cell - - stages.each do |stage| - - status = stages_status[stage] - - tooltip = "#{stage.titleize}: #{status || 'not found'}" + - pipeline.statuses.latest.stages_status.each do |stage| + - name = stage.first + - status = stage.last + - tooltip = "#{name.titleize}: #{status || 'not found'}" - if status .stage-container = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index 1174158eb65..cd9ed46d2c1 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -62,5 +62,5 @@ - if pipeline.project.build_coverage_enabled? %th Coverage %th - - pipeline.statuses.relevant.stages.each do |stage| + - pipeline.stages.each do |stage| = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage) diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 2dc91a9b762..7f42fde0fea 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -12,4 +12,4 @@ %th Stages %th %th - = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, show_commit: false + = render pipelines, commit_sha: true, stage: true, allow_retry: true, show_commit: false diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 4bc49072f35..8340ce516db 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -51,6 +51,6 @@ %th Stages %th %th.hidden-xs - = render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages + = render @pipelines, commit_sha: true, stage: true, allow_retry: true = paginate @pipelines, theme: 'gitlab' -- cgit v1.2.1 From fa1105b10b4f5dbce46bd72eb6374fe7f8d51f56 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Tue, 15 Nov 2016 15:20:37 +0100 Subject: Fix broken pipeline rendering [ci skip] --- app/models/ci/pipeline.rb | 10 ++++++---- app/views/projects/ci/pipelines/_pipeline.html.haml | 13 +++++-------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 859c6b483f4..05303007625 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -21,8 +21,6 @@ module Ci after_create :keep_around_commits, unless: :importing? - delegate :stages, to: :statuses - state_machine :status, initial: :created do event :enqueue do transition created: :pending @@ -102,15 +100,19 @@ module Ci where.not(duration: nil).sum(:duration) end - def stages + def stages_query statuses.group('stage').select(:stage) .order('max(stage_idx)') end + def stages + self.stages_query.pluck(:stage) + end + def stages_with_statuses status_sql = statuses.latest.where('stage=sg.stage').status_sql - stages_with_statuses = CommitStatus.from(self.stages, :sg). + stages_with_statuses = CommitStatus.from(self.stages_query, :sg). pluck('sg.stage', status_sql) stages_with_statuses.map do |stage| diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index d42df00b47f..e4a963a278c 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -43,16 +43,13 @@ - else Cant find HEAD commit for this branch - - stages_status = pipeline.statuses.latest.stages_status %td.stage-cell - - pipeline.statuses.latest.stages_status.each do |stage| - - name = stage.first - - status = stage.last - - tooltip = "#{name.titleize}: #{status || 'not found'}" - - if status + - pipeline.stages_with_statuses.each do |stage| + - if stage.status + - tooltip = "#{stage.name.titleize}: #{stage.status || 'not found'}" .stage-container - = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do - = ci_icon_for_status(status) + = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage.name), class: "has-tooltip ci-status-icon-#{stage.status}", title: tooltip do + = ci_icon_for_status(stage.status) %td - if pipeline.duration -- cgit v1.2.1 From d865aedafc2282f898b4bd2fdfd3660c47203c37 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Mon, 5 Dec 2016 14:17:42 +0100 Subject: Introduce `Ci::Stage`, right now this is artificial object that is build dynamically. --- app/models/ci/pipeline.rb | 24 +++++++---------------- app/models/ci/stage.rb | 23 ++++++++++++++++++++++ app/models/commit_status.rb | 9 ++------- app/views/notify/pipeline_success_email.html.haml | 2 +- app/views/notify/pipeline_success_email.text.erb | 2 +- app/views/projects/builds/_sidebar.html.haml | 2 +- app/views/projects/commit/_ci_stage.html.haml | 15 -------------- app/views/projects/commit/_pipeline.html.haml | 14 ++++++------- app/views/projects/pipelines/_with_tabs.html.haml | 12 +++++------- app/views/projects/pipelines/index.html.haml | 1 - app/views/projects/stage/_stage.html.haml | 14 +++++++++++++ lib/gitlab/data_builder/pipeline.rb | 2 +- 12 files changed, 61 insertions(+), 59 deletions(-) create mode 100644 app/models/ci/stage.rb delete mode 100644 app/views/projects/commit/_ci_stage.html.haml create mode 100644 app/views/projects/stage/_stage.html.haml diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 05303007625..b6b9a90a589 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -100,34 +100,24 @@ module Ci where.not(duration: nil).sum(:duration) end - def stages_query - statuses.group('stage').select(:stage) - .order('max(stage_idx)') + def stages_count + statuses.select(:stage).distinct.count end def stages - self.stages_query.pluck(:stage) - end - - def stages_with_statuses status_sql = statuses.latest.where('stage=sg.stage').status_sql - stages_with_statuses = CommitStatus.from(self.stages_query, :sg). + stages_query = statuses.group('stage').select(:stage) + .order('max(stage_idx)') + + stages_with_statuses = CommitStatus.from(stages_query, :sg). pluck('sg.stage', status_sql) stages_with_statuses.map do |stage| - OpenStruct.new( - name: stage.first, - status: stage.last, - pipeline: self - ) + Ci::Stage.new(self, stage.first, status: stage.last) end end - def stages_with_latest_statuses - statuses.latest.includes(project: :namespace).order(:stage_idx).group_by(&:stage) - end - def artifacts builds.latest.with_artifacts_not_expired end diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb new file mode 100644 index 00000000000..f1cac09c4e9 --- /dev/null +++ b/app/models/ci/stage.rb @@ -0,0 +1,23 @@ +module Ci + class Stage < ActiveRecord::Base + include ActiveModel::Model + + attr_reader :pipeline, :name + + def initialize(pipeline, name: name, status: status = nil) + @pipeline, @name, @status = pipeline, name, status + end + + def status + @status ||= statuses.latest.status + end + + def statuses + pipeline.statuses.where(stage: stage) + end + + def builds + pipeline.builds.where(stage: stage) + end + end +end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index d9021a38ce3..2a537dc2a13 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -41,8 +41,8 @@ class CommitStatus < ActiveRecord::Base where("#{quoted_when} <> ? OR status <> ?", 'on_failure', 'skipped') end - scope :latest_ci_stages, -> { latest.ordered.includes(project: :namespace) } - scope :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) } + scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) } + scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) } state_machine :status do event :enqueue do @@ -117,11 +117,6 @@ class CommitStatus < ActiveRecord::Base name.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip end - def self.stages - # We group by stage name, but order stages by theirs' index - unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').select('sg.stage') - end - def failed_but_allowed? allow_failure? && (failed? || canceled?) end diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml index 697c8d19257..56c1949ab2b 100644 --- a/app/views/notify/pipeline_success_email.html.haml +++ b/app/views/notify/pipeline_success_email.html.haml @@ -133,7 +133,7 @@ %tr.success-message %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;"} - build_count = @pipeline.statuses.latest.size - - stage_count = @pipeline.stages.size + - stage_count = @pipeline.stages_count Pipeline %a{href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;"} = "\##{@pipeline.id}" diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb index ae22d474f2c..40e5e306426 100644 --- a/app/views/notify/pipeline_success_email.text.erb +++ b/app/views/notify/pipeline_success_email.text.erb @@ -16,7 +16,7 @@ Commit Author: <%= commit.author_name %> <% end -%> <% build_count = @pipeline.statuses.latest.size -%> -<% stage_count = @pipeline.stages.size -%> +<% stage_count = @pipeline.stages_count -%> Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>. You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>. diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index d5004f6a066..a45612fb28b 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -111,7 +111,7 @@ %span.label.label-primary = tag - - if @build.pipeline.stages.many? + - if @build.pipeline.stages_count.many? .dropdown.build-dropdown .title Stage %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'} diff --git a/app/views/projects/commit/_ci_stage.html.haml b/app/views/projects/commit/_ci_stage.html.haml deleted file mode 100644 index 3a3d750439f..00000000000 --- a/app/views/projects/commit/_ci_stage.html.haml +++ /dev/null @@ -1,15 +0,0 @@ -%tr - %th{colspan: 10} - %strong - %a{name: stage} - - status = statuses.latest.status - %span{class: "ci-status-link ci-status-icon-#{status}"} - = ci_icon_for_status(status) - - if stage -   - = stage.titleize - = render statuses.latest_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, allow_retry: true - = render statuses.retried_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, retried: true -%tr - %td{colspan: 10} -   diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index cd9ed46d2c1..2cd40bb1106 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -27,16 +27,15 @@ .row-content-block.build-content.middle-block.pipeline-graph.hidden .pipeline-visualization %ul.stage-column-list - - stages = pipeline.stages_with_latest_statuses - - stages.each do |stage, statuses| + - pipeline.stages.each do |stage| %li.stage-column .stage-name - %a{name: stage} - - if stage - = stage.titleize + %a{name: stage.name} + - if stage.name + = stage.name.titleize .builds-container %ul - = render "projects/commit/pipeline_stage", statuses: statuses + = render "projects/commit/pipeline_stage", statuses: stage.statuses - if pipeline.yaml_errors.present? @@ -62,5 +61,4 @@ - if pipeline.project.build_coverage_enabled? %th Coverage %th - - pipeline.stages.each do |stage| - = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage) + = render pipeline.stages diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 3464e155a1b..57e793d2e59 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -15,13 +15,12 @@ .build-content.middle-block.pipeline-graph .pipeline-visualization %ul.stage-column-list - - stages = pipeline.stages_with_latest_statuses - - stages.each do |stage, statuses| + - pipeline.stages.each do |stage| %li.stage-column .stage-name - %a{name: stage} - - if stage - = stage.titleize + %a{name: stage.name} + - if stage.name + = stage.name.titleize .builds-container %ul = render "projects/commit/pipeline_stage", statuses: statuses @@ -50,5 +49,4 @@ - if pipeline.project.build_coverage_enabled? %th Coverage %th - - pipeline.statuses.relevant.stages.each do |stage| - = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage) + = render pipeline.stages diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 8340ce516db..e1e787dbde4 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -37,7 +37,6 @@ %span CI Lint %div.content-list.pipelines - - stages = @pipelines.stages - if @pipelines.blank? %div .nothing-here-block No pipelines to show diff --git a/app/views/projects/stage/_stage.html.haml b/app/views/projects/stage/_stage.html.haml new file mode 100644 index 00000000000..717075620d9 --- /dev/null +++ b/app/views/projects/stage/_stage.html.haml @@ -0,0 +1,14 @@ +%tr + %th{colspan: 10} + %strong + %a{name: subject.name} + %span{class: "ci-status-link ci-status-icon-#{subject.status}"} + = ci_icon_for_status(subject.status) + - if subject.name +   + = subject.name.titleize + = render subject.statuses.latest_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, allow_retry: true + = render subject.statuses.retried_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, retried: true +%tr + %td{colspan: 10} +   diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb index 06a783ebc1c..e0284d55da8 100644 --- a/lib/gitlab/data_builder/pipeline.rb +++ b/lib/gitlab/data_builder/pipeline.rb @@ -22,7 +22,7 @@ module Gitlab sha: pipeline.sha, before_sha: pipeline.before_sha, status: pipeline.status, - stages: pipeline.stages, + stages: pipeline.stages.map(&:name), created_at: pipeline.created_at, finished_at: pipeline.finished_at, duration: pipeline.duration -- cgit v1.2.1 From 2f972ad47be9a0c1f9b6acefc6638751145b7078 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Mon, 5 Dec 2016 14:22:41 +0100 Subject: Preserve stage values and use StaticModel --- app/models/ci/stage.rb | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index f1cac09c4e9..8f7727aebaa 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -1,23 +1,29 @@ module Ci - class Stage < ActiveRecord::Base - include ActiveModel::Model + # Currently this is artificial object, constructed dynamically + # We should migrate this object to actual database record in the future + class Stage + include StaticModel attr_reader :pipeline, :name - def initialize(pipeline, name: name, status: status = nil) + def initialize(pipeline, name: name, status: nil) @pipeline, @name, @status = pipeline, name, status end + def to_param + name + end + def status @status ||= statuses.latest.status end def statuses - pipeline.statuses.where(stage: stage) + @statuses ||= pipeline.statuses.where(stage: stage) end def builds - pipeline.builds.where(stage: stage) + @builds ||= pipeline.builds.where(stage: stage) end end end -- cgit v1.2.1 From d47aef58cd88fb813390c904bd24525e24d41483 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Mon, 5 Dec 2016 14:28:02 +0100 Subject: Add Ci::Status::Stage --- app/models/ci/stage.rb | 2 ++ lib/gitlab/ci/status/stage/common.rb | 24 +++++++++++++++++++++ lib/gitlab/ci/status/stage/factory.rb | 39 +++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 lib/gitlab/ci/status/stage/common.rb create mode 100644 lib/gitlab/ci/status/stage/factory.rb diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index 8f7727aebaa..2e7f15a16d7 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -6,6 +6,8 @@ module Ci attr_reader :pipeline, :name + delegate :project, to: :pipeline + def initialize(pipeline, name: name, status: nil) @pipeline, @name, @status = pipeline, name, status end diff --git a/lib/gitlab/ci/status/stage/common.rb b/lib/gitlab/ci/status/stage/common.rb new file mode 100644 index 00000000000..a1513024c6c --- /dev/null +++ b/lib/gitlab/ci/status/stage/common.rb @@ -0,0 +1,24 @@ +module Gitlab + module Ci + module Status + module Stage + module Common + def has_details? + true + end + + def details_path + namespace_project_pipeline_path(@subject.project.namespace, + @subject.project, + @subject, + anchor: subject.name) + end + + def has_action? + false + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/stage/factory.rb b/lib/gitlab/ci/status/stage/factory.rb new file mode 100644 index 00000000000..2e485ee22a9 --- /dev/null +++ b/lib/gitlab/ci/status/stage/factory.rb @@ -0,0 +1,39 @@ +module Gitlab + module Ci + module Status + module Stage + class Factory + EXTENDED_STATUSES = [] + + def initialize(stage) + @stage = stage + @status = stage.status || :created + end + + def fabricate! + if extended_status + extended_status.new(core_status) + else + core_status + end + end + + private + + def core_status + Gitlab::Ci::Status + .const_get(@status.capitalize) + .new(@stage) + .extend(Status::Pipeline::Common) + end + + def extended_status + @extended ||= EXTENDED_STATUSES.find do |status| + status.matches?(@stage) + end + end + end + end + end + end +end -- cgit v1.2.1 From 10499677e2cbabc6331837d20afc0e0fe68ddc37 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Mon, 5 Dec 2016 14:38:01 +0100 Subject: Added Stage tests --- app/models/ci/stage.rb | 4 +++ lib/gitlab/ci/status/stage/factory.rb | 2 +- spec/lib/gitlab/ci/status/stage/common_spec.rb | 26 +++++++++++++++++++ spec/lib/gitlab/ci/status/stage/factory_spec.rb | 33 +++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 spec/lib/gitlab/ci/status/stage/common_spec.rb create mode 100644 spec/lib/gitlab/ci/status/stage/factory_spec.rb diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index 2e7f15a16d7..d5ff97c935a 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -20,6 +20,10 @@ module Ci @status ||= statuses.latest.status end + def detailed_status + Gitlab::Ci::Status::Stage::Factory.new(self).fabricate! + end + def statuses @statuses ||= pipeline.statuses.where(stage: stage) end diff --git a/lib/gitlab/ci/status/stage/factory.rb b/lib/gitlab/ci/status/stage/factory.rb index 2e485ee22a9..a2f7ad81d39 100644 --- a/lib/gitlab/ci/status/stage/factory.rb +++ b/lib/gitlab/ci/status/stage/factory.rb @@ -24,7 +24,7 @@ module Gitlab Gitlab::Ci::Status .const_get(@status.capitalize) .new(@stage) - .extend(Status::Pipeline::Common) + .extend(Status::Stage::Common) end def extended_status diff --git a/spec/lib/gitlab/ci/status/stage/common_spec.rb b/spec/lib/gitlab/ci/status/stage/common_spec.rb new file mode 100644 index 00000000000..c3cb30a35e4 --- /dev/null +++ b/spec/lib/gitlab/ci/status/stage/common_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Stage::Common do + let(:pipeline) { create(:ci_pipeline) } + let(:stage) { Ci::Stage.new(pipeline, 'test') } + + subject do + Class.new(Gitlab::Ci::Status::Core) + .new(pipeline).extend(described_class) + end + + it 'does not have action' do + expect(subject).not_to have_action + end + + it 'has details' do + expect(subject).to have_details + end + + it 'links to the pipeline details page' do + expect(subject.details_path) + .to include "pipelines/#{pipeline.id}" + expect(subject.details_path) + .to include "##{stage.name}" + end +end diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb new file mode 100644 index 00000000000..a04fd569fc5 --- /dev/null +++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Stage::Factory do + let(:pipeline) { create(:ci_pipeline) } + let(:stage) { Ci::Stage.new(pipeline, 'test') } + + subject do + described_class.new(stage) + end + + let(:status) do + subject.fabricate! + end + + context 'when stage has a core status' do + HasStatus::AVAILABLE_STATUSES.each do |core_status| + context "when core status is #{core_status}" do + let(:build) { create(:ci_build, pipeline: pipeline, stage: stage.name, status: core_status) } + + it "fabricates a core status #{core_status}" do + expect(status).to be_a( + Gitlab::Ci::Status.const_get(core_status.capitalize)) + end + + it 'extends core status with common pipeline methods' do + expect(status).to have_details + expect(status.details_path).to include "pipelines/#{pipeline.id}" + expect(status.details_path).to include "##{stage.name}" + end + end + end + end +end -- cgit v1.2.1 From 13cee6d7fc568f42a43dd78cc86c033d06faf2b3 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Mon, 5 Dec 2016 14:47:35 +0100 Subject: Fix test failures --- app/models/ci/stage.rb | 6 +++--- lib/gitlab/ci/status/stage/common.rb | 4 ++-- spec/lib/gitlab/ci/status/stage/common_spec.rb | 4 ++-- spec/lib/gitlab/ci/status/stage/factory_spec.rb | 6 ++++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index d5ff97c935a..fe1c5c642e1 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -8,7 +8,7 @@ module Ci delegate :project, to: :pipeline - def initialize(pipeline, name: name, status: nil) + def initialize(pipeline, name:, status: nil) @pipeline, @name, @status = pipeline, name, status end @@ -25,11 +25,11 @@ module Ci end def statuses - @statuses ||= pipeline.statuses.where(stage: stage) + @statuses ||= pipeline.statuses.where(stage: name) end def builds - @builds ||= pipeline.builds.where(stage: stage) + @builds ||= pipeline.builds.where(stage: name) end end end diff --git a/lib/gitlab/ci/status/stage/common.rb b/lib/gitlab/ci/status/stage/common.rb index a1513024c6c..14c437d2b98 100644 --- a/lib/gitlab/ci/status/stage/common.rb +++ b/lib/gitlab/ci/status/stage/common.rb @@ -10,8 +10,8 @@ module Gitlab def details_path namespace_project_pipeline_path(@subject.project.namespace, @subject.project, - @subject, - anchor: subject.name) + @subject.pipeline, + anchor: @subject.name) end def has_action? diff --git a/spec/lib/gitlab/ci/status/stage/common_spec.rb b/spec/lib/gitlab/ci/status/stage/common_spec.rb index c3cb30a35e4..f15d6047878 100644 --- a/spec/lib/gitlab/ci/status/stage/common_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/common_spec.rb @@ -2,11 +2,11 @@ require 'spec_helper' describe Gitlab::Ci::Status::Stage::Common do let(:pipeline) { create(:ci_pipeline) } - let(:stage) { Ci::Stage.new(pipeline, 'test') } + let(:stage) { Ci::Stage.new(pipeline, name: 'test') } subject do Class.new(Gitlab::Ci::Status::Core) - .new(pipeline).extend(described_class) + .new(stage).extend(described_class) end it 'does not have action' do diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb index a04fd569fc5..2d22bd1e2a0 100644 --- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::Ci::Status::Stage::Factory do let(:pipeline) { create(:ci_pipeline) } - let(:stage) { Ci::Stage.new(pipeline, 'test') } + let(:stage) { Ci::Stage.new(pipeline, name: 'test') } subject do described_class.new(stage) @@ -15,7 +15,9 @@ describe Gitlab::Ci::Status::Stage::Factory do context 'when stage has a core status' do HasStatus::AVAILABLE_STATUSES.each do |core_status| context "when core status is #{core_status}" do - let(:build) { create(:ci_build, pipeline: pipeline, stage: stage.name, status: core_status) } + let!(:build) do + create(:ci_build, pipeline: pipeline, stage: 'test', status: core_status) + end it "fabricates a core status #{core_status}" do expect(status).to be_a( -- cgit v1.2.1 From 6d80b94a89cd2151cbf37f6f98f79d23df7fa638 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Mon, 5 Dec 2016 14:48:37 +0100 Subject: Fix handling of skipped vs success status --- app/models/concerns/has_status.rb | 6 +++--- app/services/ci/process_pipeline_service.rb | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 2f5aa91a964..215367cc1dc 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -4,7 +4,7 @@ module HasStatus AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped] STARTED_STATUSES = %w[running success failed skipped] ACTIVE_STATUSES = %w[pending running] - COMPLETED_STATUSES = %w[success failed canceled] + COMPLETED_STATUSES = %w[success failed canceled skipped] ORDERED_STATUSES = %w[failed pending running canceled success skipped] class_methods do @@ -23,9 +23,9 @@ module HasStatus canceled = scope.canceled.select('count(*)').to_sql "(CASE - WHEN (#{builds})=(#{success}) THEN 'success' + WHEN (#{builds})=(#{skipped}) THEN 'skipped' WHEN (#{builds})=(#{created}) THEN 'created' - WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'skipped' + WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'success' WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled' WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending' WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running' diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 2e028c44d8b..79eb97b7b55 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -44,11 +44,11 @@ module Ci def valid_statuses_for_when(value) case value when 'on_success' - %w[success] + %w[success skipped] when 'on_failure' %w[failed] when 'always' - %w[success failed] + %w[success failed skipped] else [] end -- cgit v1.2.1 From 260d754ca89c14297e0e360d35d7914d57e290bf Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Mon, 5 Dec 2016 17:52:50 +0100 Subject: Fix handling of allowed to failure jobs --- app/models/ci/pipeline.rb | 2 +- app/models/commit_status.rb | 7 +-- app/models/concerns/has_status.rb | 3 +- .../projects/ci/pipelines/_pipeline.html.haml | 2 +- spec/models/ci/pipeline_spec.rb | 51 ++++++++++++++++++---- spec/models/commit_status_spec.rb | 45 +------------------ spec/models/concerns/has_status_spec.rb | 2 +- 7 files changed, 49 insertions(+), 63 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index b6b9a90a589..aa23b5cf97c 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -114,7 +114,7 @@ module Ci pluck('sg.stage', status_sql) stages_with_statuses.map do |stage| - Ci::Stage.new(self, stage.first, status: stage.last) + Ci::Stage.new(self, name: stage.first, status: stage.last) end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 2a537dc2a13..cf90475f4d4 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -31,14 +31,9 @@ class CommitStatus < ActiveRecord::Base end scope :exclude_ignored, -> do - quoted_when = connection.quote_column_name('when') # We want to ignore failed_but_allowed jobs where("allow_failure = ? OR status IN (?)", - false, all_state_names - [:failed, :canceled]). - # We want to ignore skipped manual jobs - where("#{quoted_when} <> ? OR status <> ?", 'manual', 'skipped'). - # We want to ignore skipped on_failure - where("#{quoted_when} <> ? OR status <> ?", 'on_failure', 'skipped') + false, all_state_names - [:failed, :canceled]) end scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) } diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 215367cc1dc..43f312d319e 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -24,8 +24,9 @@ module HasStatus "(CASE WHEN (#{builds})=(#{skipped}) THEN 'skipped' + WHEN (#{builds})=(#{success}) THEN 'success' WHEN (#{builds})=(#{created}) THEN 'created' - WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'success' + WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'skipped' WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled' WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending' WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running' diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index e4a963a278c..b58dceb58c9 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -44,7 +44,7 @@ Cant find HEAD commit for this branch %td.stage-cell - - pipeline.stages_with_statuses.each do |stage| + - pipeline.stages.each do |stage| - if stage.status - tooltip = "#{stage.name.titleize}: #{stage.status || 'not found'}" .stage-container diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 3f93d9ddf19..cdc858c13b4 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -20,8 +20,6 @@ describe Ci::Pipeline, models: true do it { is_expected.to respond_to :git_author_email } it { is_expected.to respond_to :short_sha } - it { is_expected.to delegate_method(:stages).to(:statuses) } - describe '#valid_commit_sha' do context 'commit.sha can not start with 00000000' do before do @@ -125,16 +123,51 @@ describe Ci::Pipeline, models: true do end describe '#stages' do - let(:pipeline2) { FactoryGirl.create :ci_pipeline, project: project } - subject { CommitStatus.where(pipeline: [pipeline, pipeline2]).stages } - before do - FactoryGirl.create :ci_build, pipeline: pipeline2, stage: 'test', stage_idx: 1 - FactoryGirl.create :ci_build, pipeline: pipeline, stage: 'build', stage_idx: 0 + create(:commit_status, pipeline: pipeline, stage: 'build', name: 'linux', stage_idx: 0, status: 'success') + create(:commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'failed') + create(:commit_status, pipeline: pipeline, stage: 'deploy', name: 'staging', stage_idx: 2, status: 'running') + create(:commit_status, pipeline: pipeline, stage: 'test', name: 'rspec', stage_idx: 1, status: 'success') end - it 'return all stages' do - is_expected.to eq(%w(build test)) + subject { pipeline.stages } + + context 'stages list' do + it 'returns ordered list of stages' do + expect(subject.map(&:name)).to eq(%w[build test deploy]) + end + end + + it 'returns a valid number of stages' do + expect(pipeline.stages_count).to eq(3) + end + + context 'stages with statuses' do + let(:statuses) do + subject.map do |stage| + [stage.name, stage.status] + end + end + + it 'returns list of stages with statuses' do + expect(statuses).to eq([['build', 'failed'], + ['test', 'success'], + ['deploy', 'running'] + ]) + end + + context 'when build is retried' do + before do + create(:commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'success') + end + + it 'ignores the previous state' do + expect(statuses).to eq([['build', 'success'], + ['test', 'success'], + ['deploy', 'running'] + ]) + end + end end end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 80c2a1bc7a9..1ec08c2a9d0 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -175,7 +175,7 @@ describe CommitStatus, models: true do end it 'returns statuses without what we want to ignore' do - is_expected.to eq(statuses.values_at(1, 2, 4, 5, 6, 8, 9)) + is_expected.to eq(statuses.values_at(0, 1, 2, 3, 4, 5, 6, 8, 9)) end end @@ -200,49 +200,6 @@ describe CommitStatus, models: true do end end - describe '#stages' do - before do - create :commit_status, pipeline: pipeline, stage: 'build', name: 'linux', stage_idx: 0, status: 'success' - create :commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'failed' - create :commit_status, pipeline: pipeline, stage: 'deploy', name: 'staging', stage_idx: 2, status: 'running' - create :commit_status, pipeline: pipeline, stage: 'test', name: 'rspec', stage_idx: 1, status: 'success' - end - - context 'stages list' do - subject { CommitStatus.where(pipeline: pipeline).stages } - - it 'returns ordered list of stages' do - is_expected.to eq(%w[build test deploy]) - end - end - - context 'stages with statuses' do - subject { CommitStatus.where(pipeline: pipeline).latest.stages_status } - - it 'returns list of stages with statuses' do - is_expected.to eq({ - 'build' => 'failed', - 'test' => 'success', - 'deploy' => 'running' - }) - end - - context 'when build is retried' do - before do - create :commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'success' - end - - it 'ignores a previous state' do - is_expected.to eq({ - 'build' => 'success', - 'test' => 'success', - 'deploy' => 'running' - }) - end - end - end - end - describe '#commit' do it 'returns commit pipeline has been created for' do expect(commit_status.commit).to eq project.commit diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb index 9defb17dc92..4d0f51fe82a 100644 --- a/spec/models/concerns/has_status_spec.rb +++ b/spec/models/concerns/has_status_spec.rb @@ -48,7 +48,7 @@ describe HasStatus do [create(type, status: :failed, allow_failure: true)] end - it { is_expected.to eq 'success' } + it { is_expected.to eq 'skipped' } end context 'success and canceled' do -- cgit v1.2.1 From 401c155e16e4966be538fd14f23e268cd3383aa7 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Mon, 5 Dec 2016 18:16:34 +0100 Subject: Update stage rendering views --- app/views/projects/builds/_sidebar.html.haml | 4 ++-- app/views/projects/commit/_pipeline.html.haml | 16 +++------------- .../projects/commit/_pipeline_stage.html.haml | 14 -------------- .../commit/_pipeline_status_group.html.haml | 13 ------------- app/views/projects/pipelines/_graph.html.haml | 4 ++++ app/views/projects/pipelines/_with_tabs.html.haml | 15 +++------------ app/views/projects/stage/_graph.html.haml | 22 ++++++++++++++++++++++ app/views/projects/stage/_in_stage_group.html.haml | 13 +++++++++++++ app/views/projects/stage/_stage.html.haml | 2 +- 9 files changed, 48 insertions(+), 55 deletions(-) delete mode 100644 app/views/projects/commit/_pipeline_stage.html.haml delete mode 100644 app/views/projects/commit/_pipeline_status_group.html.haml create mode 100644 app/views/projects/pipelines/_graph.html.haml create mode 100644 app/views/projects/stage/_graph.html.haml create mode 100644 app/views/projects/stage/_in_stage_group.html.haml diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index a45612fb28b..ce8b66b1945 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -111,7 +111,7 @@ %span.label.label-primary = tag - - if @build.pipeline.stages_count.many? + - if @build.pipeline.stages_count > 1 .dropdown.build-dropdown .title Stage %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'} @@ -120,7 +120,7 @@ %ul.dropdown-menu - @build.pipeline.stages.each do |stage| %li - %a.stage-item= stage + %a.stage-item= stage.name .builds-container - HasStatus::ORDERED_STATUSES.each do |build_status| diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index 2cd40bb1106..4fc5e15592a 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -25,18 +25,7 @@ = time_interval_in_words pipeline.duration .row-content-block.build-content.middle-block.pipeline-graph.hidden - .pipeline-visualization - %ul.stage-column-list - - pipeline.stages.each do |stage| - %li.stage-column - .stage-name - %a{name: stage.name} - - if stage.name - = stage.name.titleize - .builds-container - %ul - = render "projects/commit/pipeline_stage", statuses: stage.statuses - + = render "projects/pipelines/graph", subject: pipeline - if pipeline.yaml_errors.present? .bs-callout.bs-callout-danger @@ -61,4 +50,5 @@ - if pipeline.project.build_coverage_enabled? %th Coverage %th - = render pipeline.stages + - pipeline.stages.each do |stage| + = render "projects/stage/stage", subject: stage diff --git a/app/views/projects/commit/_pipeline_stage.html.haml b/app/views/projects/commit/_pipeline_stage.html.haml deleted file mode 100644 index f9a9c8707f5..00000000000 --- a/app/views/projects/commit/_pipeline_stage.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -- status_groups = statuses.sort_by(&:name).group_by(&:group_name) -- status_groups.each do |group_name, grouped_statuses| - - if grouped_statuses.one? - - status = grouped_statuses.first - - is_playable = status.playable? && can?(current_user, :update_build, @project) - %li.build{ class: ("playable" if is_playable) } - .curve - .build-content - = render "projects/#{status.to_partial_path}_pipeline", subject: status - - else - %li.build - .curve - .dropdown.inline.build-content - = render "projects/commit/pipeline_status_group", name: group_name, subject: grouped_statuses diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml deleted file mode 100644 index 2b26ad9d6fa..00000000000 --- a/app/views/projects/commit/_pipeline_status_group.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- group_status = CommitStatus.where(id: subject).status -%button.dropdown-menu-toggle.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } } - %span{class: "ci-status-icon ci-status-icon-#{group_status}"} - = ci_icon_for_status(group_status) - %span.ci-status-text - = name - %span.badge= subject.size -.dropdown-menu.grouped-pipeline-dropdown - .arrow - %ul - - subject.each do |status| - %li - = render "projects/#{status.to_partial_path}_pipeline", subject: status diff --git a/app/views/projects/pipelines/_graph.html.haml b/app/views/projects/pipelines/_graph.html.haml new file mode 100644 index 00000000000..3bb6426c156 --- /dev/null +++ b/app/views/projects/pipelines/_graph.html.haml @@ -0,0 +1,4 @@ +.pipeline-visualization + %ul.stage-column-list + - subject.stages.each do |stage| + = render "projects/stage/graph", subject: stage diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 57e793d2e59..2ace9339af3 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -13,17 +13,7 @@ .tab-content #js-tab-pipeline.tab-pane .build-content.middle-block.pipeline-graph - .pipeline-visualization - %ul.stage-column-list - - pipeline.stages.each do |stage| - %li.stage-column - .stage-name - %a{name: stage.name} - - if stage.name - = stage.name.titleize - .builds-container - %ul - = render "projects/commit/pipeline_stage", statuses: statuses + = render "projects/pipelines/graph", subject: pipeline #js-tab-builds.tab-pane - if pipeline.yaml_errors.present? @@ -49,4 +39,5 @@ - if pipeline.project.build_coverage_enabled? %th Coverage %th - = render pipeline.stages + - pipeline.stages.each do |stage| + = render "projects/stage/stage", subject: stage diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml new file mode 100644 index 00000000000..f1d11db58ab --- /dev/null +++ b/app/views/projects/stage/_graph.html.haml @@ -0,0 +1,22 @@ +%li.stage-column + .stage-name + %a{ name: subject.name } + - if subject.name + = subject.name.titleize + .builds-container + %ul + - statuses = subject.statuses.latest + - status_groups = statuses.sort_by(&:name).group_by(&:group_name) + - status_groups.each do |group_name, grouped_statuses| + - if grouped_statuses.one? + - status = grouped_statuses.first + - is_playable = status.playable? && can?(current_user, :update_build, @project) + %li.build{ class: ("playable" if is_playable) } + .curve + .build-content + = render "projects/#{status.to_partial_path}_pipeline", subject: status + - else + %li.build + .curve + .dropdown.inline.build-content + = render "projects/stage/in_stage_group", name: group_name, subject: grouped_statuses diff --git a/app/views/projects/stage/_in_stage_group.html.haml b/app/views/projects/stage/_in_stage_group.html.haml new file mode 100644 index 00000000000..2b26ad9d6fa --- /dev/null +++ b/app/views/projects/stage/_in_stage_group.html.haml @@ -0,0 +1,13 @@ +- group_status = CommitStatus.where(id: subject).status +%button.dropdown-menu-toggle.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } } + %span{class: "ci-status-icon ci-status-icon-#{group_status}"} + = ci_icon_for_status(group_status) + %span.ci-status-text + = name + %span.badge= subject.size +.dropdown-menu.grouped-pipeline-dropdown + .arrow + %ul + - subject.each do |status| + %li + = render "projects/#{status.to_partial_path}_pipeline", subject: status diff --git a/app/views/projects/stage/_stage.html.haml b/app/views/projects/stage/_stage.html.haml index 717075620d9..055d8fca38b 100644 --- a/app/views/projects/stage/_stage.html.haml +++ b/app/views/projects/stage/_stage.html.haml @@ -1,7 +1,7 @@ %tr %th{colspan: 10} %strong - %a{name: subject.name} + %a{ name: subject.name } %span{class: "ci-status-link ci-status-icon-#{subject.status}"} = ci_icon_for_status(subject.status) - if subject.name -- cgit v1.2.1 From 9d85320c67395399ad52467fde2bacf62427ed4b Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Tue, 6 Dec 2016 15:07:02 +0100 Subject: Run builds with runners with tags gitlab-org and 2gb --- .gitlab-ci.yml | 108 ++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 69 insertions(+), 39 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8de7ca897ad..e522d47d19d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -30,7 +30,12 @@ stages: - post-test - pages -# Prepare and merge knapsack tests +# Predefined scopes +.dedicated-runner: &dedicated-runner + tags: + - gitlab-org + - 2gb + .knapsack-state: &knapsack-state services: [] variables: @@ -45,47 +50,14 @@ stages: paths: - knapsack/ -knapsack: - <<: *knapsack-state - stage: prepare - script: - - mkdir -p knapsack/ - - '[[ -f knapsack/rspec_report.json ]] || echo "{}" > knapsack/rspec_report.json' - - '[[ -f knapsack/spinach_report.json ]] || echo "{}" > knapsack/spinach_report.json' - -update-knapsack: - <<: *knapsack-state - stage: post-test - script: - - scripts/merge-reports knapsack/rspec_report.json knapsack/rspec_node_*.json - - scripts/merge-reports knapsack/spinach_report.json knapsack/spinach_node_*.json - - rm -f knapsack/*_node_*.json - only: - - master@gitlab-org/gitlab-ce - - master@gitlab-org/gitlab-ee - - master@gitlab/gitlabhq - - master@gitlab/gitlab-ee - .use-db: &use-db services: - mysql:latest - redis:alpine -setup-test-env: - <<: *use-db - stage: prepare - script: - - bundle exec rake assets:precompile 2>/dev/null - - bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init' - artifacts: - expire_in: 7d - paths: - - public/assets - - tmp/tests - - .rspec-knapsack: &rspec-knapsack stage: test + <<: *dedicated-runner <<: *use-db script: - JOB_NAME=( $CI_BUILD_NAME ) @@ -103,6 +75,7 @@ setup-test-env: .spinach-knapsack: &spinach-knapsack stage: test + <<: *dedicated-runner <<: *use-db script: - JOB_NAME=( $CI_BUILD_NAME ) @@ -118,6 +91,44 @@ setup-test-env: - knapsack/ - coverage/ +# Prepare and merge knapsack tests + +knapsack: + <<: *knapsack-state + <<: *dedicated-runner + stage: prepare + script: + - mkdir -p knapsack/ + - '[[ -f knapsack/rspec_report.json ]] || echo "{}" > knapsack/rspec_report.json' + - '[[ -f knapsack/spinach_report.json ]] || echo "{}" > knapsack/spinach_report.json' + +setup-test-env: + <<: *use-db + <<: *dedicated-runner + stage: prepare + script: + - bundle exec rake assets:precompile 2>/dev/null + - bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init' + artifacts: + expire_in: 7d + paths: + - public/assets + - tmp/tests + +update-knapsack: + <<: *knapsack-state + <<: *dedicated-runner + stage: post-test + script: + - scripts/merge-reports knapsack/rspec_report.json knapsack/rspec_node_*.json + - scripts/merge-reports knapsack/spinach_report.json knapsack/spinach_node_*.json + - rm -f knapsack/*_node_*.json + only: + - master@gitlab-org/gitlab-ce + - master@gitlab-org/gitlab-ee + - master@gitlab/gitlabhq + - master@gitlab/gitlab-ee + rspec 0 20: *rspec-knapsack rspec 1 20: *rspec-knapsack rspec 2 20: *rspec-knapsack @@ -166,10 +177,12 @@ spinach 9 10: *spinach-knapsack .rspec-knapsack-ruby21: &rspec-knapsack-ruby21 <<: *rspec-knapsack + <<: *dedicated-runner <<: *ruby-21 .spinach-knapsack-ruby21: &spinach-knapsack-ruby21 <<: *spinach-knapsack + <<: *dedicated-runner <<: *ruby-21 rspec 0 20 ruby21: *rspec-knapsack-ruby21 @@ -214,6 +227,7 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21 .exec: &exec <<: *ruby-static-analysis + <<: *dedicated-runner stage: test script: - bundle exec $CI_BUILD_NAME @@ -249,12 +263,14 @@ rake ee_compat_check: rake db:migrate:reset: stage: test <<: *use-db + <<: *dedicated-runner script: - rake db:migrate:reset rake db:seed_fu: stage: test <<: *use-db + <<: *dedicated-runner variables: SIZE: "1" SETUP_DB: "false" @@ -276,6 +292,7 @@ teaspoon: - node_modules/ stage: test <<: *use-db + <<: *dedicated-runner script: - npm install - npm link istanbul @@ -288,20 +305,23 @@ teaspoon: lint-doc: stage: test + <<: *dedicated-runner image: "phusion/baseimage:latest" before_script: [] script: - scripts/lint-doc.sh bundler:check: - stage: test - <<: *ruby-static-analysis - script: + stage: test + <<: *dedicated-runner + <<: *ruby-static-analysis + script: - bundle check bundler:audit: stage: test <<: *ruby-static-analysis + <<: *dedicated-runner only: - master@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ee @@ -313,6 +333,7 @@ bundler:audit: migration paths: stage: test <<: *use-db + <<: *dedicated-runner variables: SETUP_DB: "false" only: @@ -334,6 +355,7 @@ migration paths: coverage: stage: post-test services: [] + <<: *dedicated-runner variables: SETUP_DB: "false" USE_BUNDLE_INSTALL: "true" @@ -347,6 +369,7 @@ coverage: - coverage/assets/ lint:javascript: + <<: *dedicated-runner cache: paths: - node_modules/ @@ -358,6 +381,7 @@ lint:javascript: - npm --silent run eslint lint:javascript:report: + <<: *dedicated-runner cache: paths: - node_modules/ @@ -379,6 +403,7 @@ lint:javascript:report: trigger_docs: stage: post-test image: "alpine" + <<: *dedicated-runner before_script: - apk update && apk add curl variables: @@ -394,6 +419,7 @@ trigger_docs: notify:slack: stage: post-test + <<: *dedicated-runner variables: SETUP_DB: "false" USE_BUNDLE_INSTALL: "false" @@ -409,6 +435,7 @@ notify:slack: pages: before_script: [] stage: pages + <<: *dedicated-runner dependencies: - coverage - teaspoon @@ -423,11 +450,12 @@ pages: paths: - public only: - - master + - master@gitlab-org/gitlab-ce # Insurance in case a gem needed by one of our releases gets yanked from # rubygems.org in the future. cache gems: + <<: *dedicated-runner only: - tags variables: @@ -437,3 +465,5 @@ cache gems: artifacts: paths: - vendor/cache + only: + - master@gitlab-org/gitlab-ce -- cgit v1.2.1 From 9c149094f61888e23f610ec2364adefd98dbef9a Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Mon, 5 Dec 2016 12:55:57 -0600 Subject: Remove tags.scss --- app/assets/stylesheets/framework/nav.scss | 10 ++++ app/assets/stylesheets/pages/tags.scss | 7 --- app/views/projects/buttons/_download.html.haml | 79 +++++++++++++------------- app/views/projects/tags/show.html.haml | 21 +++---- 4 files changed, 60 insertions(+), 57 deletions(-) delete mode 100644 app/assets/stylesheets/pages/tags.scss diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index c84a71a624d..69da520f21f 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -268,6 +268,16 @@ width: auto; } } + + &.multi-line { + .nav-text { + line-height: 20px; + } + + .nav-controls { + padding: 17px 0; + } + } } .layout-nav { diff --git a/app/assets/stylesheets/pages/tags.scss b/app/assets/stylesheets/pages/tags.scss deleted file mode 100644 index 24ebd3e7cfa..00000000000 --- a/app/assets/stylesheets/pages/tags.scss +++ /dev/null @@ -1,7 +0,0 @@ -.tag-buttons { - line-height: 40px; - - .btn:not(.dropdown-toggle) { - margin-left: 10px; - } -} diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 7b995bd8735..40bfa01a45a 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -1,42 +1,41 @@ - if !project.empty_repo? && can?(current_user, :download_code, project) - %span{class: 'download-button'} - .dropdown.inline - %button.btn{ 'data-toggle' => 'dropdown' } - = icon('download') - = icon("caret-down") - %span.sr-only - Select Archive Format - %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } - %li.dropdown-header Source code - %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do - %i.fa.fa-download - %span Download zip - %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar.gz - %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar.bz2 - %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar + .dropdown.inline.download-button + %button.btn{ 'data-toggle' => 'dropdown' } + = icon('download') + = icon("caret-down") + %span.sr-only + Select Archive Format + %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } + %li.dropdown-header Source code + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do + %i.fa.fa-download + %span Download zip + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar.gz + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar.bz2 + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar - - pipeline = project.pipelines.latest_successful_for(ref) - - if pipeline - - artifacts = pipeline.builds.latest.with_artifacts - - if artifacts.any? - %li.dropdown-header Artifacts - - unless pipeline.latest? - - latest_pipeline = project.pipeline_for(ref) - %li - .unclickable= ci_status_for_statuseable(latest_pipeline) - %li.dropdown-header Previous Artifacts - - artifacts.each do |job| - %li - = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow' do - %i.fa.fa-download - %span Download '#{job.name}' + - pipeline = project.pipelines.latest_successful_for(ref) + - if pipeline + - artifacts = pipeline.builds.latest.with_artifacts + - if artifacts.any? + %li.dropdown-header Artifacts + - unless pipeline.latest? + - latest_pipeline = project.pipeline_for(ref) + %li + .unclickable= ci_status_for_statuseable(latest_pipeline) + %li.dropdown-header Previous Artifacts + - artifacts.each do |job| + %li + = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow' do + %i.fa.fa-download + %span Download '#{job.name}' diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index 155af755759..12facb6eb73 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -3,8 +3,16 @@ = render "projects/commits/head" %div{ class: container_class } - .sub-header-block - .pull-right.tag-buttons + .top-area.multi-line + .nav-text + .title + %span.item-title= @tag.name + - if @commit + = render 'projects/branches/commit', commit: @commit, project: @project + - else + Cant find HEAD commit for this tag + + .nav-controls - if can?(current_user, :push_code, @project) = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Edit release notes' do = icon("pencil") @@ -15,15 +23,8 @@ = render 'projects/buttons/download', project: @project, ref: @tag.name - if can?(current_user, :admin_project, @project) .pull-right - = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do + = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do %i.fa.fa-trash-o - .tag-info.append-bottom-10 - .title - %span.item-title= @tag.name - - if @commit - = render 'projects/branches/commit', commit: @commit, project: @project - - else - Cant find HEAD commit for this tag - if @tag.message.present? %pre.body = strip_gpg_signature(@tag.message) -- cgit v1.2.1 From 4447652e8191cd0f1543568813c854bfdc2c8a61 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Tue, 6 Dec 2016 08:27:42 -0600 Subject: Move image styles to framework --- app/assets/stylesheets/framework/images.scss | 11 +++++++++++ app/assets/stylesheets/pages/appearances.scss | 11 ----------- 2 files changed, 11 insertions(+), 11 deletions(-) create mode 100644 app/assets/stylesheets/framework/images.scss delete mode 100644 app/assets/stylesheets/pages/appearances.scss diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss new file mode 100644 index 00000000000..878f44116ba --- /dev/null +++ b/app/assets/stylesheets/framework/images.scss @@ -0,0 +1,11 @@ +.appearance-logo-preview { + max-width: 400px; + margin-bottom: 20px; +} + +.appearance-light-logo-preview { + background-color: $background-color; + max-width: 72px; + padding: 10px; + margin-bottom: 10px; +} diff --git a/app/assets/stylesheets/pages/appearances.scss b/app/assets/stylesheets/pages/appearances.scss deleted file mode 100644 index 878f44116ba..00000000000 --- a/app/assets/stylesheets/pages/appearances.scss +++ /dev/null @@ -1,11 +0,0 @@ -.appearance-logo-preview { - max-width: 400px; - margin-bottom: 20px; -} - -.appearance-light-logo-preview { - background-color: $background-color; - max-width: 72px; - padding: 10px; - margin-bottom: 10px; -} -- cgit v1.2.1 From 4aaf6af3cc7970cbe5414d323f290b4a94e479d0 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Tue, 6 Dec 2016 08:29:01 -0600 Subject: Move award emojis to framwork --- app/assets/stylesheets/framework.scss | 2 + app/assets/stylesheets/framework/awards.scss | 140 +++++++++++++++++++++++++++ app/assets/stylesheets/pages/awards.scss | 140 --------------------------- 3 files changed, 142 insertions(+), 140 deletions(-) create mode 100644 app/assets/stylesheets/framework/awards.scss delete mode 100644 app/assets/stylesheets/pages/awards.scss diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 4aaff7d04f1..c0ddaacd558 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -40,3 +40,5 @@ @import "framework/blank"; @import "framework/wells.scss"; @import "framework/page-header.scss"; +@import "framework/awards.scss"; +@import "framework/images.scss"; diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss new file mode 100644 index 00000000000..c13cb4a02b2 --- /dev/null +++ b/app/assets/stylesheets/framework/awards.scss @@ -0,0 +1,140 @@ +.awards { + .emoji-icon { + width: 19px; + height: 19px; + } +} + +.emoji-menu { + position: absolute; + margin-top: 3px; + padding: $gl-padding; + z-index: 9; + width: 300px; + font-size: 14px; + background-color: $award-emoji-menu-bg; + border: 1px solid $award-emoji-menu-border; + border-radius: $border-radius-base; + box-shadow: 0 6px 12px $award-emoji-menu-shadow; + pointer-events: none; + opacity: 0; + transform: scale(.2); + transform-origin: 0 -45px; + transition: .3s cubic-bezier(.87,-.41,.19,1.44); + transition-property: transform, opacity; + + &.is-aligned-right { + transform-origin: 100% -45px; + } + + &.is-visible { + pointer-events: all; + opacity: 1; + transform: scale(1); + } + + .emoji-menu-content { + height: 300px; + overflow-y: scroll; + } +} + +.emoji-search { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC"); + background-repeat: no-repeat; + background-position: right 5px center; + background-size: 16px; +} + +.emoji-menu-list { + list-style: none; + padding-left: 0; + margin-bottom: 0; +} + +.emoji-menu-list-item { + padding: 3px; + margin-left: 1px; + margin-right: 1px; +} + +.emoji-menu-btn { + display: block; + cursor: pointer; + width: 30px; + height: 30px; + padding: 0; + background: none; + border: 0; + border-radius: $border-radius-base; + transition: transform .15s cubic-bezier(.3, 0, .2, 2); + + &:hover { + background-color: transparent; + outline: 0; + transform: scale(1.3); + } + + &:focus, + &:active { + outline: 0; + } + + .emoji-icon { + display: inline-block; + position: relative; + top: 3px; + } +} + +.award-menu-holder { + display: inline-block; + position: relative; +} + +.award-control { + margin: 3px 5px 3px 0; + padding: 5px 6px; + outline: 0; + + &:hover, + &.active, + &:active { + background-color: $row-hover; + border-color: $row-hover-border; + box-shadow: none; + outline: 0; + } + + &.btn { + &:focus { + outline: 0; + } + } + + &.is-loading { + .award-control-icon-normal, + .emoji-icon { + display: none; + } + + .award-control-icon-loading { + display: block; + } + } + + .icon, + .award-control-icon { + float: left; + margin-right: 5px; + font-size: 18px; + } + + .award-control-icon-loading { + display: none; + } + + .award-control-icon { + color: $award-emoji-new-btn-icon-color; + } +} diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss deleted file mode 100644 index c13cb4a02b2..00000000000 --- a/app/assets/stylesheets/pages/awards.scss +++ /dev/null @@ -1,140 +0,0 @@ -.awards { - .emoji-icon { - width: 19px; - height: 19px; - } -} - -.emoji-menu { - position: absolute; - margin-top: 3px; - padding: $gl-padding; - z-index: 9; - width: 300px; - font-size: 14px; - background-color: $award-emoji-menu-bg; - border: 1px solid $award-emoji-menu-border; - border-radius: $border-radius-base; - box-shadow: 0 6px 12px $award-emoji-menu-shadow; - pointer-events: none; - opacity: 0; - transform: scale(.2); - transform-origin: 0 -45px; - transition: .3s cubic-bezier(.87,-.41,.19,1.44); - transition-property: transform, opacity; - - &.is-aligned-right { - transform-origin: 100% -45px; - } - - &.is-visible { - pointer-events: all; - opacity: 1; - transform: scale(1); - } - - .emoji-menu-content { - height: 300px; - overflow-y: scroll; - } -} - -.emoji-search { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC"); - background-repeat: no-repeat; - background-position: right 5px center; - background-size: 16px; -} - -.emoji-menu-list { - list-style: none; - padding-left: 0; - margin-bottom: 0; -} - -.emoji-menu-list-item { - padding: 3px; - margin-left: 1px; - margin-right: 1px; -} - -.emoji-menu-btn { - display: block; - cursor: pointer; - width: 30px; - height: 30px; - padding: 0; - background: none; - border: 0; - border-radius: $border-radius-base; - transition: transform .15s cubic-bezier(.3, 0, .2, 2); - - &:hover { - background-color: transparent; - outline: 0; - transform: scale(1.3); - } - - &:focus, - &:active { - outline: 0; - } - - .emoji-icon { - display: inline-block; - position: relative; - top: 3px; - } -} - -.award-menu-holder { - display: inline-block; - position: relative; -} - -.award-control { - margin: 3px 5px 3px 0; - padding: 5px 6px; - outline: 0; - - &:hover, - &.active, - &:active { - background-color: $row-hover; - border-color: $row-hover-border; - box-shadow: none; - outline: 0; - } - - &.btn { - &:focus { - outline: 0; - } - } - - &.is-loading { - .award-control-icon-normal, - .emoji-icon { - display: none; - } - - .award-control-icon-loading { - display: block; - } - } - - .icon, - .award-control-icon { - float: left; - margin-right: 5px; - font-size: 18px; - } - - .award-control-icon-loading { - display: none; - } - - .award-control-icon { - color: $award-emoji-new-btn-icon-color; - } -} -- cgit v1.2.1 From 2b5a24a8a96bba788db202805cc4f4ec2d278a3f Mon Sep 17 00:00:00 2001 From: Adam Niedzielski <adamsunday@gmail.com> Date: Tue, 6 Dec 2016 15:42:11 +0100 Subject: Explain "js: true" in "deleted_source_branch_spec.rb" [ci skip] --- spec/features/merge_requests/deleted_source_branch_spec.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/features/merge_requests/deleted_source_branch_spec.rb b/spec/features/merge_requests/deleted_source_branch_spec.rb index 778b3a90cf3..d5c9ed8a3b7 100644 --- a/spec/features/merge_requests/deleted_source_branch_spec.rb +++ b/spec/features/merge_requests/deleted_source_branch_spec.rb @@ -1,5 +1,8 @@ require 'spec_helper' +# This test serves as a regression test for a bug that caused an error +# message to be shown by JavaScript when the source branch was deleted. +# Please do not remove "js: true". describe 'Deleted source branch', feature: true, js: true do let(:user) { create(:user) } let(:merge_request) { create(:merge_request) } -- cgit v1.2.1 From 953a10947bea8ff75290cd50fae4ae1f7636a71d Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Tue, 6 Dec 2016 14:49:37 +0100 Subject: Added Ci::Stage specs --- app/models/ci/pipeline.rb | 4 + lib/gitlab/data_builder/pipeline.rb | 2 +- spec/factories/ci/stages.rb | 13 ++ spec/lib/gitlab/ci/status/pipeline/factory_spec.rb | 4 +- spec/lib/gitlab/ci/status/stage/common_spec.rb | 4 +- spec/lib/gitlab/ci/status/stage/factory_spec.rb | 10 +- spec/models/ci/pipeline_spec.rb | 4 + spec/models/ci/stage_spec.rb | 133 +++++++++++++++++++++ 8 files changed, 165 insertions(+), 9 deletions(-) create mode 100644 spec/factories/ci/stages.rb create mode 100644 spec/models/ci/stage_spec.rb diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index aa23b5cf97c..d14825c082a 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -104,6 +104,10 @@ module Ci statuses.select(:stage).distinct.count end + def stages_name + statuses.order(:stage_idx).distinct.pluck(:stage) + end + def stages status_sql = statuses.latest.where('stage=sg.stage').status_sql diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb index e0284d55da8..e50e54b6e99 100644 --- a/lib/gitlab/data_builder/pipeline.rb +++ b/lib/gitlab/data_builder/pipeline.rb @@ -22,7 +22,7 @@ module Gitlab sha: pipeline.sha, before_sha: pipeline.before_sha, status: pipeline.status, - stages: pipeline.stages.map(&:name), + stages: pipeline.stages_name, created_at: pipeline.created_at, finished_at: pipeline.finished_at, duration: pipeline.duration diff --git a/spec/factories/ci/stages.rb b/spec/factories/ci/stages.rb new file mode 100644 index 00000000000..ee3b17b8bf1 --- /dev/null +++ b/spec/factories/ci/stages.rb @@ -0,0 +1,13 @@ +FactoryGirl.define do + factory :ci_stage, class: Ci::Stage do + transient do + name 'test' + status nil + pipeline factory: :ci_empty_pipeline + end + + initialize_with do + Ci::Stage.new(pipeline, name: name, status: status) + end + end +end diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb index d6243940f2e..939ad2edf04 100644 --- a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb @@ -21,7 +21,7 @@ describe Gitlab::Ci::Status::Pipeline::Factory do Gitlab::Ci::Status.const_get(core_status.capitalize)) end - it 'extends core status with common pipeline methods' do + it 'extends core status with common stage methods' do expect(status).to have_details expect(status).not_to have_action expect(status.details_path) @@ -45,7 +45,7 @@ describe Gitlab::Ci::Status::Pipeline::Factory do .to be_a Gitlab::Ci::Status::Pipeline::SuccessWithWarnings end - it 'extends core status with common pipeline methods' do + it 'extends core status with common stage methods' do expect(status).to have_details end end diff --git a/spec/lib/gitlab/ci/status/stage/common_spec.rb b/spec/lib/gitlab/ci/status/stage/common_spec.rb index f15d6047878..f3259c6f23e 100644 --- a/spec/lib/gitlab/ci/status/stage/common_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/common_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' describe Gitlab::Ci::Status::Stage::Common do - let(:pipeline) { create(:ci_pipeline) } - let(:stage) { Ci::Stage.new(pipeline, name: 'test') } + let(:pipeline) { create(:ci_empty_pipeline) } + let(:stage) { build(:ci_stage, pipeline: pipeline, name: 'test') } subject do Class.new(Gitlab::Ci::Status::Core) diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb index 2d22bd1e2a0..17929665c83 100644 --- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' describe Gitlab::Ci::Status::Stage::Factory do - let(:pipeline) { create(:ci_pipeline) } - let(:stage) { Ci::Stage.new(pipeline, name: 'test') } + let(:pipeline) { create(:ci_empty_pipeline) } + let(:stage) { build(:ci_stage, pipeline: pipeline, name: 'test') } subject do described_class.new(stage) @@ -15,8 +15,10 @@ describe Gitlab::Ci::Status::Stage::Factory do context 'when stage has a core status' do HasStatus::AVAILABLE_STATUSES.each do |core_status| context "when core status is #{core_status}" do - let!(:build) do + before do create(:ci_build, pipeline: pipeline, stage: 'test', status: core_status) + create(:commit_status, pipeline: pipeline, stage: 'test', status: core_status) + create(:ci_build, pipeline: pipeline, stage: 'build', status: :failed) end it "fabricates a core status #{core_status}" do @@ -24,7 +26,7 @@ describe Gitlab::Ci::Status::Stage::Factory do Gitlab::Ci::Status.const_get(core_status.capitalize)) end - it 'extends core status with common pipeline methods' do + it 'extends core status with common stage methods' do expect(status).to have_details expect(status.details_path).to include "pipelines/#{pipeline.id}" expect(status.details_path).to include "##{stage.name}" diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index cdc858c13b4..8158e71dd55 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -142,6 +142,10 @@ describe Ci::Pipeline, models: true do expect(pipeline.stages_count).to eq(3) end + it 'returns a valid names of stages' do + expect(pipeline.stages_name).to eq(['build', 'test', 'deploy']) + end + context 'stages with statuses' do let(:statuses) do subject.map do |stage| diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb new file mode 100644 index 00000000000..f232761dba2 --- /dev/null +++ b/spec/models/ci/stage_spec.rb @@ -0,0 +1,133 @@ +require 'spec_helper' + +describe Ci::Stage, models: true do + let(:stage) { build(:ci_stage) } + let(:pipeline) { stage.pipeline } + let(:stage_name) { stage.name } + + describe '#expectations' do + subject { stage } + + it { is_expected.to include_module(StaticModel) } + + it { is_expected.to respond_to(:pipeline) } + it { is_expected.to respond_to(:name) } + + it { is_expected.to delegate_method(:project).to(:pipeline) } + end + + describe '#statuses' do + let!(:stage_build) { create_job(:ci_build) } + let!(:commit_status) { create_job(:commit_status) } + let!(:other_build) { create_job(:ci_build, stage: 'other stage') } + + subject { stage.statuses } + + it "returns only matching statuses" do + is_expected.to contain_exactly(stage_build, commit_status) + end + end + + describe '#builds' do + let!(:stage_build) { create_job(:ci_build) } + let!(:commit_status) { create_job(:commit_status) } + + subject { stage.builds } + + it "returns only builds" do + is_expected.to contain_exactly(stage_build) + end + end + + describe '#status' do + subject { stage.status } + + context 'if status is already defined' do + let(:stage) { build(:ci_stage, status: 'success') } + + it "returns defined status" do + is_expected.to eq('success') + end + end + + context 'if status has to be calculated' do + let!(:stage_build) { create_job(:ci_build, status: :failed) } + + it "returns status of a build" do + is_expected.to eq('failed') + end + + context 'and builds are retried' do + let!(:new_build) { create_job(:ci_build, status: :success) } + + it "returns status of latest build" do + is_expected.to eq('success') + end + end + end + end + + describe '#detailed_status' do + subject { stage.detailed_status } + + context 'when build is created' do + let!(:stage_build) { create_job(:ci_build, status: :created) } + + it 'returns detailed status for created stage' do + expect(subject.text).to eq 'created' + end + end + + context 'when build is pending' do + let!(:stage_build) { create_job(:ci_build, status: :pending) } + + it 'returns detailed status for pending stage' do + expect(subject.text).to eq 'pending' + end + end + + context 'when build is running' do + let!(:stage_build) { create_job(:ci_build, status: :running) } + + it 'returns detailed status for running stage' do + expect(subject.text).to eq 'running' + end + end + + context 'when build is successful' do + let!(:stage_build) { create_job(:ci_build, status: :success) } + + it 'returns detailed status for successful stage' do + expect(subject.text).to eq 'passed' + end + end + + context 'when build is failed' do + let!(:stage_build) { create_job(:ci_build, status: :failed) } + + it 'returns detailed status for failed stage' do + expect(subject.text).to eq 'failed' + end + end + + context 'when build is canceled' do + let!(:stage_build) { create_job(:ci_build, status: :canceled) } + + it 'returns detailed status for canceled stage' do + expect(subject.text).to eq 'canceled' + end + end + + context 'when build is skipped' do + let!(:stage_build) { create_job(:ci_build, status: :skipped) } + + it 'returns detailed status for skipped stage' do + expect(subject.text).to eq 'skipped' + end + end + end + + def create_job(type, status: 'success', stage: stage_name) + create(type, pipeline: pipeline, stage: stage, status: status) + end +end -- cgit v1.2.1 From ee07628721eec27587d3eebedc9193cd73dd781b Mon Sep 17 00:00:00 2001 From: Pedro Moreira da Silva <pedro@gitlab.com> Date: Tue, 6 Dec 2016 15:06:14 +0000 Subject: Fix active nav badges not inheriting the link color. --- app/assets/stylesheets/framework/nav.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index c84a71a624d..981cb0adba1 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -71,6 +71,10 @@ border-bottom: 2px solid $link-underline-blue; color: $black; font-weight: 600; + + .badge { + color: $black; + } } .badge { -- cgit v1.2.1 From ee8433466ee77c1da026842e965b32ebefab6f13 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Tue, 6 Dec 2016 17:12:11 +0200 Subject: Fix importing PRs with not existing branches --- lib/bitbucket/connection.rb | 2 +- lib/gitlab/bitbucket_import/importer.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/bitbucket/connection.rb b/lib/bitbucket/connection.rb index c375fe16aee..e28285f119c 100644 --- a/lib/bitbucket/connection.rb +++ b/lib/bitbucket/connection.rb @@ -45,7 +45,7 @@ module Bitbucket @expires_at = response.expires_at @expires_in = response.expires_in @refresh_token = response.refresh_token - @connection = nil + @connection = nil end private diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 6438c8a52e4..34d93542955 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -81,10 +81,10 @@ module Gitlab description: description, source_project: project, source_branch: pull_request.source_branch_name, - source_branch_sha: project.repository.rugged.lookup(pull_request.source_branch_sha).oid, + source_branch_sha: pull_request.source_branch_sha, target_project: project, target_branch: pull_request.target_branch_name, - target_branch_sha: project.repository.rugged.lookup(pull_request.target_branch_sha).oid, + target_branch_sha: pull_request.target_branch_sha, state: pull_request.state, author_id: gitlab_user_id(project, pull_request.author), assignee_id: nil, -- cgit v1.2.1 From bdd1193f57a2e2e9c38f5f267179bd62a876f030 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Tue, 6 Dec 2016 09:18:17 -0600 Subject: Remove confirmation.scss --- app/assets/stylesheets/pages/confirmation.scss | 32 ------------------------ app/views/devise/confirmations/almost_there.haml | 7 +++--- 2 files changed, 4 insertions(+), 35 deletions(-) delete mode 100644 app/assets/stylesheets/pages/confirmation.scss diff --git a/app/assets/stylesheets/pages/confirmation.scss b/app/assets/stylesheets/pages/confirmation.scss deleted file mode 100644 index 8aab5e8231d..00000000000 --- a/app/assets/stylesheets/pages/confirmation.scss +++ /dev/null @@ -1,32 +0,0 @@ -.well-confirmation { - margin-bottom: 20px; - border-bottom: 1px solid $gray-darker; - - > h1, - h2, - h3, - h4, - h5, - h6 { - font-weight: 400; - } - - .lead { - margin-bottom: 20px; - } - - ul, - ol { - padding-left: 0; - } - - li { - list-style-type: none; - } -} - -.confirmation-content { - a { - color: $md-link-color; - } -} diff --git a/app/views/devise/confirmations/almost_there.haml b/app/views/devise/confirmations/almost_there.haml index 20cd7b0179d..fb70d158096 100644 --- a/app/views/devise/confirmations/almost_there.haml +++ b/app/views/devise/confirmations/almost_there.haml @@ -1,12 +1,13 @@ -.well-confirmation.text-center +.well-confirmation.text-center.append-bottom-20 %h1.prepend-top-0 Almost there... - %p.lead + %p.lead.append-bottom-20 Please check your email to confirm your account + %hr - if current_application_settings.after_sign_up_text.present? .well-confirmation.text-center = markdown_field(current_application_settings, :after_sign_up_text) -%p.confirmation-content.text-center +%p.text-center No confirmation email received? Please check your spam folder or .append-bottom-20.prepend-top-20.text-center %a.btn.btn-lg.btn-success{ href: new_user_confirmation_path } -- cgit v1.2.1 From c7d7f11389838acd658d1427c0d2ebb80fd8df3b Mon Sep 17 00:00:00 2001 From: Ruben Davila <rdavila84@gmail.com> Date: Tue, 6 Dec 2016 10:31:56 -0500 Subject: Bump gitlab-shell version to 4.0.3 --- GITLAB_SHELL_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index fcdb2e109f6..c4e41f94594 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -4.0.0 +4.0.3 -- cgit v1.2.1 From a526e156a117e16d5d135aa69c0011d464697d58 Mon Sep 17 00:00:00 2001 From: Adam Niedzielski <adamsunday@gmail.com> Date: Tue, 6 Dec 2016 16:08:53 +0000 Subject: Encourage bug reporters to mention if they use GitLab.com [ci skip] --- .gitlab/issue_templates/Bug.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md index ac38f0c9521..6d7d88c6791 100644 --- a/.gitlab/issue_templates/Bug.md +++ b/.gitlab/issue_templates/Bug.md @@ -21,6 +21,8 @@ logs, and code as it's very hard to read otherwise.) ### Output of checks +(If you are reporting a bug on GitLab.com, write: This bug happens on GitLab.com) + #### Results of GitLab application Check (For installations with omnibus-gitlab package run and paste the output of: -- cgit v1.2.1 From 43b7b0ce23d4de7055dc1cdd660b92ff03f4eb1e Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Tue, 6 Dec 2016 18:26:24 +0200 Subject: Fix authorization with BitBucket --- lib/omniauth/strategies/bitbucket.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/omniauth/strategies/bitbucket.rb b/lib/omniauth/strategies/bitbucket.rb index c5484c59c47..475aad5970f 100644 --- a/lib/omniauth/strategies/bitbucket.rb +++ b/lib/omniauth/strategies/bitbucket.rb @@ -16,7 +16,7 @@ module OmniAuth end uid do - raw['username'] + raw_info['username'] end info do -- cgit v1.2.1 From 51a921baf90be2a6654990b9b7d062f4c613a64b Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt <bob@vanlanduyt.co> Date: Tue, 6 Dec 2016 17:13:14 +0100 Subject: A simpler implementation of finding a merge request Following a discussion in !7180 --- lib/api/issues.rb | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/api/issues.rb b/lib/api/issues.rb index cfb7c45de8e..26c8f2fecd0 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -28,14 +28,6 @@ module API new_params end - - def merge_request_for_resolving_discussions - return unless merge_request_iid = params[:merge_request_for_resolving_discussions] - - @merge_request_for_resolving_discussions ||= MergeRequestsFinder.new(current_user, project_id: user_project.id). - execute. - find_by(iid: merge_request_iid) - end end resource :issues do @@ -179,7 +171,12 @@ module API attrs = attributes_for_keys(keys) attrs[:labels] = params[:labels] if params[:labels] - attrs[:merge_request_for_resolving_discussions] = merge_request_for_resolving_discussions if params[:merge_request_for_resolving_discussions] + + if merge_request_iid = params[:merge_request_for_resolving_discussions] + attrs[:merge_request_for_resolving_discussions] = MergeRequestsFinder.new(current_user, project_id: user_project.id). + execute. + find_by(iid: merge_request_iid) + end # Convert and filter out invalid confidential flags attrs['confidential'] = to_boolean(attrs['confidential']) -- cgit v1.2.1 From 7e42c22991896e52a28537ff61a48334bf84f7c9 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Tue, 6 Dec 2016 18:50:48 +0200 Subject: Fix 404 error when visit group label edit page Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- config/routes/group.rb | 22 +++++++++++----------- spec/features/groups/labels/edit_spec.rb | 21 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 11 deletions(-) create mode 100644 spec/features/groups/labels/edit_spec.rb diff --git a/config/routes/group.rb b/config/routes/group.rb index 9fe72990994..776c31c9dac 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -1,15 +1,5 @@ resources :groups, only: [:index, :new, :create] -scope(path: 'groups/*id', - controller: :groups, - constraints: { id: Gitlab::Regex.namespace_route_regex }) do - get :edit, as: :edit_group - get :issues, as: :issues_group - get :merge_requests, as: :merge_requests_group - get :projects, as: :projects_group - get :activity, as: :activity_group -end - scope(path: 'groups/*group_id', module: :groups, as: :group, @@ -22,10 +12,20 @@ scope(path: 'groups/*group_id', resource :avatar, only: [:destroy] resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create] - resources :labels, except: [:show], constraints: { id: /\d+/ } do + resources :labels, except: [:show] do post :toggle_subscription, on: :member end end +scope(path: 'groups/*id', + controller: :groups, + constraints: { id: Gitlab::Regex.namespace_route_regex }) do + get :edit, as: :edit_group + get :issues, as: :issues_group + get :merge_requests, as: :merge_requests_group + get :projects, as: :projects_group + get :activity, as: :activity_group +end + # Must be last route in this file get 'groups/*id' => 'groups#show', as: :group_canonical, constraints: { id: Gitlab::Regex.namespace_route_regex } diff --git a/spec/features/groups/labels/edit_spec.rb b/spec/features/groups/labels/edit_spec.rb new file mode 100644 index 00000000000..69281cecb7b --- /dev/null +++ b/spec/features/groups/labels/edit_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +feature 'Edit group label', feature: true do + given(:user) { create(:user) } + given(:group) { create(:group) } + given(:label) { create(:group_label, group: group) } + + background do + group.add_owner(user) + login_as(user) + visit edit_group_label_path(group, label) + end + + scenario 'update label with new title' do + fill_in 'label_title', with: 'new label name' + click_button 'Save changes' + + expect(current_path).to eq(root_path) + expect(label.reload.title).to eq('new label name') + end +end -- cgit v1.2.1 From e16bc50d363fe65bbceb8aed39a0e19ec91b3329 Mon Sep 17 00:00:00 2001 From: Semyon Pupkov <mail@semyonpupkov.com> Date: Tue, 6 Dec 2016 13:54:35 +0500 Subject: Move admin logs spinach test to rspec https://gitlab.com/gitlab-org/gitlab-ce/issues/23036 --- .../unreleased/move-admin-logs-spinach-test-to-rspec.yml | 4 ++++ features/admin/logs.feature | 8 -------- features/steps/admin/logs.rb | 11 ----------- spec/features/admin/admin_browses_logs_spec.rb | 15 +++++++++++++++ 4 files changed, 19 insertions(+), 19 deletions(-) create mode 100644 changelogs/unreleased/move-admin-logs-spinach-test-to-rspec.yml delete mode 100644 features/admin/logs.feature delete mode 100644 features/steps/admin/logs.rb create mode 100644 spec/features/admin/admin_browses_logs_spec.rb diff --git a/changelogs/unreleased/move-admin-logs-spinach-test-to-rspec.yml b/changelogs/unreleased/move-admin-logs-spinach-test-to-rspec.yml new file mode 100644 index 00000000000..696aa8510a0 --- /dev/null +++ b/changelogs/unreleased/move-admin-logs-spinach-test-to-rspec.yml @@ -0,0 +1,4 @@ +--- +title: Move admin logs spinach test to rspec +merge_request: 7945 +author: Semyon Pupkov diff --git a/features/admin/logs.feature b/features/admin/logs.feature deleted file mode 100644 index ceb3bc34927..00000000000 --- a/features/admin/logs.feature +++ /dev/null @@ -1,8 +0,0 @@ -@admin -Feature: Admin Logs - Background: - Given I sign in as an admin - - Scenario: On Admin Logs - Given I visit admin logs page - Then I should see tabs with available logs diff --git a/features/steps/admin/logs.rb b/features/steps/admin/logs.rb deleted file mode 100644 index 63881d69146..00000000000 --- a/features/steps/admin/logs.rb +++ /dev/null @@ -1,11 +0,0 @@ -class Spinach::Features::AdminLogs < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedAdmin - - step 'I should see tabs with available logs' do - expect(page).to have_content 'test.log' - expect(page).to have_content 'githost.log' - expect(page).to have_content 'application.log' - end -end diff --git a/spec/features/admin/admin_browses_logs_spec.rb b/spec/features/admin/admin_browses_logs_spec.rb new file mode 100644 index 00000000000..d880f3f07db --- /dev/null +++ b/spec/features/admin/admin_browses_logs_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe 'Admin browses logs' do + before do + login_as :admin + end + + it 'shows available log files' do + visit admin_logs_path + + expect(page).to have_content 'test.log' + expect(page).to have_content 'githost.log' + expect(page).to have_content 'application.log' + end +end -- cgit v1.2.1 From 9d9fe3c82efa8c0ec75863b5b533af6fdbe48f89 Mon Sep 17 00:00:00 2001 From: Ryan Harris <harrisryan1@gmail.com> Date: Tue, 6 Dec 2016 12:55:24 -0500 Subject: Use default btn styling for Housekeeping button on projects settings page --- app/views/projects/edit.html.haml | 2 +- changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 3a5af2723c6..f8d856fe152 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -145,7 +145,7 @@ such as compressing file revisions and removing unreachable objects. .col-lg-9 = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project), - method: :post, class: "btn btn-save" + method: :post, class: "btn btn-default" %hr .row.prepend-top-default .col-lg-3 diff --git a/changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml b/changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml new file mode 100644 index 00000000000..0770f9752a0 --- /dev/null +++ b/changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml @@ -0,0 +1,4 @@ +--- +title: Changed Housekeeping button on project settings page to default styling +merge_request: +author: Ryan Harris -- cgit v1.2.1 From d33b22f23890f5d67f1e88a41efc4d8adec6611c Mon Sep 17 00:00:00 2001 From: Semyon Pupkov <mail@semyonpupkov.com> Date: Mon, 5 Dec 2016 23:50:13 +0500 Subject: Move admin hooks spinach to rspec https://gitlab.com/gitlab-org/gitlab-ce/issues/23036 --- .../unreleased/move-admin-hooks-spinach-test-to-rspec.yml | 4 ++++ features/admin/hooks.feature | 9 --------- features/steps/admin/hooks.rb | 15 --------------- spec/features/admin/admin_hooks_spec.rb | 15 ++++++++------- 4 files changed, 12 insertions(+), 31 deletions(-) create mode 100644 changelogs/unreleased/move-admin-hooks-spinach-test-to-rspec.yml delete mode 100644 features/admin/hooks.feature delete mode 100644 features/steps/admin/hooks.rb diff --git a/changelogs/unreleased/move-admin-hooks-spinach-test-to-rspec.yml b/changelogs/unreleased/move-admin-hooks-spinach-test-to-rspec.yml new file mode 100644 index 00000000000..7dfd741985a --- /dev/null +++ b/changelogs/unreleased/move-admin-hooks-spinach-test-to-rspec.yml @@ -0,0 +1,4 @@ +--- +title: Move admin hooks spinach to rspec +merge_request: 7942 +author: Semyon Pupkov diff --git a/features/admin/hooks.feature b/features/admin/hooks.feature deleted file mode 100644 index 5ca332d9f1c..00000000000 --- a/features/admin/hooks.feature +++ /dev/null @@ -1,9 +0,0 @@ -@admin -Feature: Admin Hooks - Background: - Given I sign in as an admin - - Scenario: On Admin Hooks - Given I visit admin hooks page - Then I submit the form with enabled SSL verification - And I see new hook with enabled SSL verification \ No newline at end of file diff --git a/features/steps/admin/hooks.rb b/features/steps/admin/hooks.rb deleted file mode 100644 index 541e25fcb70..00000000000 --- a/features/steps/admin/hooks.rb +++ /dev/null @@ -1,15 +0,0 @@ -class Spinach::Features::AdminHooks < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedAdmin - - step "I submit the form with enabled SSL verification" do - fill_in 'hook_url', with: 'http://google.com' - check "Enable SSL verification" - click_on "Add System Hook" - end - - step "I see new hook with enabled SSL verification" do - expect(page).to have_content "SSL Verification: enabled" - end -end diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb index b3ce72b1452..f246997d5a2 100644 --- a/spec/features/admin/admin_hooks_spec.rb +++ b/spec/features/admin/admin_hooks_spec.rb @@ -26,16 +26,17 @@ describe "Admin::Hooks", feature: true do end describe "New Hook" do - before do - @url = FFaker::Internet.uri("http") + let(:url) { FFaker::Internet.uri('http') } + + it 'adds new hook' do visit admin_hooks_path - fill_in "hook_url", with: @url - expect { click_button "Add System Hook" }.to change(SystemHook, :count).by(1) - end + fill_in 'hook_url', with: url + check 'Enable SSL verification' - it "opens new hook popup" do + expect { click_button 'Add System Hook' }.to change(SystemHook, :count).by(1) + expect(page).to have_content 'SSL Verification: enabled' expect(current_path).to eq(admin_hooks_path) - expect(page).to have_content(@url) + expect(page).to have_content(url) end end -- cgit v1.2.1 From bc380e48486903637ec9da977ad2d5bab3dc9dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= <glen@delfi.ee> Date: Thu, 6 Oct 2016 17:50:47 +0000 Subject: Update custom_hooks.md for global custom hooks and chained hook info https://gitlab.com/gitlab-org/gitlab-shell/merge_requests/93 --- doc/administration/custom_hooks.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md index 0387d730489..06291705702 100644 --- a/doc/administration/custom_hooks.md +++ b/doc/administration/custom_hooks.md @@ -42,6 +42,25 @@ Follow the steps below to set up a custom hook: That's it! Assuming the hook code is properly implemented the hook will fire as appropriate. +## Chained hooks support + +> [Introduced][93] in GitLab Shell 4.1.0. + +The hooks could be also placed in `hooks/<hook_name>.d` (global) or `custom_hooks/<hook_name>.d` (per project) +directories supporting chained execution of the hooks. + +The hooks are searched and executed in this order: +1. `<project>.git/hooks/` - symlink to `gitlab-shell/hooks` global dir +1. `<project>.git/hooks/<hook_name>` - executed by `git` itself, this is `gitlab-shell/hooks/<hook_name>` +1. `<project>.git/custom_hooks/<hook_name>` - per project hook (this is already existing behavior) +1. `<project>.git/custom_hooks/<hook_name>.d/*` - per project hooks +1. `<project>.git/hooks/<hook_name>.d/*` - global hooks: all executable files (minus editor backup files) + +Files in `.d` directories need to be executable and not match the backup file pattern (`*~`). + +The hooks of the same type are executed in order and execution stops on the first +script exiting with non-zero value. + ## Custom error messages > [Introduced][5073] in GitLab 8.10. @@ -54,3 +73,4 @@ STDERR takes precedence over STDOUT. [hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks [5073]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5073 +[93]: https://gitlab.com/gitlab-org/gitlab-shell/merge_requests/93 -- cgit v1.2.1 From 0fe3b3608a64e18178eb308cbd1cb3bb4a2c16ad Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Tue, 6 Dec 2016 13:17:36 -0600 Subject: Remove dashboard.scss --- app/assets/stylesheets/framework/variables.scss | 6 ---- app/assets/stylesheets/pages/dashboard.scss | 43 ------------------------- 2 files changed, 49 deletions(-) delete mode 100644 app/assets/stylesheets/pages/dashboard.scss diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 647dcfc5187..18716813c48 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -427,12 +427,6 @@ $common-gray-dark: #444; $common-red: $gl-text-red; $common-green: $gl-text-green; - -/* -* Dashboard -*/ -$dashboard-project-access-icon-color: #888; - /* * Editor */ diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss deleted file mode 100644 index 4421ed6a0b9..00000000000 --- a/app/assets/stylesheets/pages/dashboard.scss +++ /dev/null @@ -1,43 +0,0 @@ -.dashboard { - .side { - .panel { - .panel-heading { - background: $background-color; - border-top-left-radius: 0; - } - - border-top-left-radius: 0; - } - } -} - -.dashboard-search-filter { - padding: 5px; - - .search-text-input { - float: left; - @extend .col-md-2; - } - - .btn { - margin-left: 5px; - float: left; - } -} - -.project-access-icon { - margin-left: 10px; - float: left; - margin-right: 15px; - margin-bottom: 15px; - - i { - color: $dashboard-project-access-icon-color; - } -} - -.dash-project-access-icon { - float: left; - margin-right: 5px; - width: 16px; -} -- cgit v1.2.1 From b738b2a7dcb58f39e1ef2f1722a300a0a825a2a0 Mon Sep 17 00:00:00 2001 From: ja-me-sk <me@jamesk.io> Date: Tue, 6 Dec 2016 13:47:14 -0700 Subject: new DevOps report, 404s, typos correct heading level for topics Update puppet dev ops report for 2016 --- doc/university/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/university/README.md b/doc/university/README.md index 8917636c59b..12727e9d56f 100644 --- a/doc/university/README.md +++ b/doc/university/README.md @@ -5,7 +5,7 @@ GitLab University is the best place to learn about **Version Control with Git an It doesn't replace, but accompanies our great [Documentation](https://docs.gitlab.com) and [Blog Articles](https://about.gitlab.com/blog/). -Would you like to contribute to GitLab University? Then please take a look at our contribution [process](/process) for more information. +Would you like to contribute to GitLab University? Then please take a look at our contribution [process](process) for more information. ## Gitlab University Curriculum @@ -97,7 +97,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project 1. [Markdown in GitLab](../user/markdown.md) 1. [Issues and Merge Requests - Video](https://www.youtube.com/watch?v=raXvuwet78M) -1. [Due Dates and Milestones fro GitLab Issues](https://about.gitlab.com/2016/08/05/feature-highlight-set-dates-for-issues/) +1. [Due Dates and Milestones for GitLab Issues](https://about.gitlab.com/2016/08/05/feature-highlight-set-dates-for-issues/) 1. [How to Use GitLab Labels](https://about.gitlab.com/2016/08/17/using-gitlab-labels/) 1. [Applying GitLab Labels Automatically](https://about.gitlab.com/2016/08/19/applying-gitlab-labels-automatically/) 1. [GitLab Issue Board - Product Page](https://about.gitlab.com/solutions/issueboard/) @@ -147,7 +147,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project 1. [Xebia Labs: Dev Ops Terminology](https://xebialabs.com/glossary/) 1. [Xebia Labs: Periodic Table of DevOps Tools](https://xebialabs.com/periodic-table-of-devops-tools/) -1. [Puppet Labs: State of Dev Ops 2015 - Book](https://puppetlabs.com/sites/default/files/2015-state-of-devops-report.pdf) +1. [Puppet Labs: State of Dev Ops 2016 - Book](https://puppet.com/resources/white-paper/2016-state-of-devops-report) #### 3.2. Installing GitLab with Omnibus @@ -173,7 +173,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project #### 3.6 Custom Languages -1. [How to add Syntax Highlighting Support for Custom Langauges to GitLab - Video](how to add support for your favorite language to GitLab) +1. [How to add Syntax Highlighting Support for Custom Languages to GitLab - Video](https://youtu.be/6WxTMqatrrA) #### 3.7. Scalability and High Availability @@ -198,7 +198,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project --- -## 4. External Articles +### 4. External Articles 1. [2011 WSJ article by Marc Andreessen - Software is Eating the World](http://www.wsj.com/articles/SB10001424053111903480904576512250915629460) 1. [2014 Blog post by Chris Dixon - Software eats software development](http://cdixon.org/2014/04/13/software-eats-software-development/) @@ -206,7 +206,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project --- -## 5. Resources for GitLab Team Members +### 5. Resources for GitLab Team Members *Some content can only be accessed by GitLab team members* -- cgit v1.2.1 From 96f162125dabb3d3ff21cb95abf97e5af6ee5589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Wed, 7 Dec 2016 09:59:52 +0100 Subject: Handle an edge-case whith invitees MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the project has invitees, no group members were returned due to a `user_id NOT IN (42, NULL)` query which always returned [] since a `user_id` would be NULL, thus the condition could never match. Signed-off-by: Rémy Coutable <remy@rymai.me> --- .../projects/project_members_controller.rb | 14 ++-- .../projects/members/group_members_spec.rb | 83 ++++++++++++++++++++-- 2 files changed, 86 insertions(+), 11 deletions(-) diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 10bc75b4c5e..3fb8bba3cd0 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -13,7 +13,13 @@ class Projects::ProjectMembersController < Projects::ApplicationController group = @project.group if group - group_members = group.group_members.where.not(user_id: @project_members.select(:user_id)) + # We need `.where.not(user_id: nil)` here otherwise when a group has an + # invitee, it would make the following query return 0 rows since a NULL + # user_id would be present in the subquery + # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values + # FIXME: This whole logic should be moved to a finder! + non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id) + group_members = group.group_members.where.not(user_id: non_null_user_ids) group_members = group_members.non_invite unless can?(current_user, :admin_group, @group) end @@ -29,13 +35,13 @@ class Projects::ProjectMembersController < Projects::ApplicationController @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) end - members_id = @project_members.pluck(:id) + member_ids = @project_members.pluck(:id) if group_members - members_id << group_members.pluck(:id) + member_ids += group_members.pluck(:id) end - @project_members = Member.where(id: members_id.flatten).order(access_level: :desc).page(params[:page]) + @project_members = Member.where(id: member_ids).order(access_level: :desc).page(params[:page]) @requesters = AccessRequestsFinder.new(@project).execute(current_user) diff --git a/spec/features/projects/members/group_members_spec.rb b/spec/features/projects/members/group_members_spec.rb index 39235a1cd4f..7d0065ee2c4 100644 --- a/spec/features/projects/members/group_members_spec.rb +++ b/spec/features/projects/members/group_members_spec.rb @@ -2,20 +2,89 @@ require 'spec_helper' feature 'Projects members', feature: true do let(:user) { create(:user) } - let(:group) { create(:group) } - let(:project) { create(:project, group: group) } + let(:developer) { create(:user) } + let(:group) { create(:group, :public, :access_requestable) } + let(:project) { create(:empty_project, :public, :access_requestable, creator: user, group: group) } + let(:project_invitee) { create(:project_member, project: project, invite_token: '123', invite_email: 'test1@abc.com', user: nil) } + let(:group_invitee) { create(:group_member, group: group, invite_token: '123', invite_email: 'test2@abc.com', user: nil) } + let(:project_requester) { create(:user) } + let(:group_requester) { create(:user) } background do + project.team << [developer, :developer] group.add_owner(user) login_as(user) - visit namespace_project_project_members_path(project.namespace, project) end - it 'shows group members in list' do - expect(page).to have_selector('.group_member') + context 'with a group invitee' do + before do + group_invitee + visit namespace_project_project_members_path(project.namespace, project) + end + + scenario 'does not appear in the project members page' do + page.within first('.content-list') do + expect(page).not_to have_content('test2@abc.com') + end + end + end + + context 'with a group and a project invitee' do + before do + group_invitee + project_invitee + visit namespace_project_project_members_path(project.namespace, project) + end + + scenario 'shows the project invitee, the project developer, and the group owner' do + page.within first('.content-list') do + expect(page).to have_content('test1@abc.com') + expect(page).not_to have_content('test2@abc.com') + + # Project developer + expect(page).to have_content(developer.name) + + # Group owner + expect(page).to have_content(user.name) + expect(page).to have_content(group.name) + end + end + end + + context 'with a group requester' do + before do + group.request_access(group_requester) + visit namespace_project_project_members_path(project.namespace, project) + end + + scenario 'does not appear in the project members page' do + page.within first('.content-list') do + expect(page).not_to have_content(group_requester.name) + end + end + end + + context 'with a group and a project requesters' do + before do + group.request_access(group_requester) + project.request_access(project_requester) + visit namespace_project_project_members_path(project.namespace, project) + end + + scenario 'shows the project requester, the project developer, and the group owner' do + page.within first('.content-list') do + expect(page).to have_content(project_requester.name) + expect(page).not_to have_content(group_requester.name) + end + + page.within all('.content-list').last do + # Project developer + expect(page).to have_content(developer.name) - page.within first('.content-list .member') do - expect(page).to have_content(group.name) + # Group owner + expect(page).to have_content(user.name) + expect(page).to have_content(group.name) + end end end end -- cgit v1.2.1 From 3e7818e93a69d2f628c864e40b00ab0871bff3dc Mon Sep 17 00:00:00 2001 From: Robert Schilling <rschilling@student.tugraz.at> Date: Mon, 7 Nov 2016 15:15:14 +0100 Subject: Grapify the issues API --- lib/api/helpers.rb | 16 --- lib/api/issues.rb | 263 +++++++++++++++++---------------------- spec/requests/api/issues_spec.rb | 26 ++-- 3 files changed, 126 insertions(+), 179 deletions(-) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 7f94ede7940..898ca470a30 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -217,22 +217,6 @@ module API end end - def issuable_order_by - if params["order_by"] == 'updated_at' - 'updated_at' - else - 'created_at' - end - end - - def issuable_sort - if params["sort"] == 'asc' - :asc - else - :desc - end - end - def filter_by_iid(items, iid) items.where(iid: iid) end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 26c8f2fecd0..c9124649cbb 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -1,6 +1,7 @@ module API - # Issues API class Issues < Grape::API + include PaginationParams + before { authenticate! } helpers do @@ -20,77 +21,68 @@ module API issues.includes(:milestone).where('milestones.title' => milestone) end - def issue_params - new_params = declared(params, include_parent_namespace: false, include_missing: false).to_h - new_params = new_params.with_indifferent_access - new_params.delete(:id) - new_params.delete(:issue_id) + params :issues_params do + optional :labels, type: String, desc: 'Comma-separated list of label names' + optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at', + desc: 'Return issues ordered by `created_at` or `updated_at` fields.' + optional :sort, type: String, values: %w[asc desc], default: 'desc', + desc: 'Return issues sorted in `asc` or `desc` order.' + use :pagination + end - new_params + params :issue_params do + optional :description, type: String, desc: 'The description of an issue' + optional :assignee_id, type: Integer, desc: 'The ID of a user to assign issue' + optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue' + optional :labels, type: String, desc: 'Comma-separated list of label names' + optional :due_date, type: String, desc: 'Date time string in the format YEAR-MONTH-DAY' + optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential' + optional :state_event, type: String, values: %w[open close], + desc: 'State of the issue' end end resource :issues do - # Get currently authenticated user's issues - # - # Parameters: - # state (optional) - Return "opened" or "closed" issues - # labels (optional) - Comma-separated list of label names - # order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` - # sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` - # - # Example Requests: - # GET /issues - # GET /issues?state=opened - # GET /issues?state=closed - # GET /issues?labels=foo - # GET /issues?labels=foo,bar - # GET /issues?labels=foo,bar&state=opened + desc "Get currently authenticated user's issues" do + success Entities::Issue + end + params do + optional :state, type: String, values: %w[opened closed all], default: 'all', + desc: 'Return opened, closed, or all issues' + use :issues_params + end get do issues = current_user.issues.inc_notes_with_associations - issues = filter_issues_state(issues, params[:state]) unless params[:state].nil? + issues = filter_issues_state(issues, params[:state]) issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? - issues = issues.reorder(issuable_order_by => issuable_sort) + issues = issues.reorder(params[:order_by] => params[:sort]) present paginate(issues), with: Entities::Issue, current_user: current_user end end + params do + requires :id, type: String, desc: 'The ID of a group' + end resource :groups do - # Get a list of group issues - # - # Parameters: - # id (required) - The ID of a group - # state (optional) - Return "opened" or "closed" issues - # labels (optional) - Comma-separated list of label names - # milestone (optional) - Milestone title - # order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` - # sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` - # - # Example Requests: - # GET /groups/:id/issues - # GET /groups/:id/issues?state=opened - # GET /groups/:id/issues?state=closed - # GET /groups/:id/issues?labels=foo - # GET /groups/:id/issues?labels=foo,bar - # GET /groups/:id/issues?labels=foo,bar&state=opened - # GET /groups/:id/issues?milestone=1.0.0 - # GET /groups/:id/issues?milestone=1.0.0&state=closed + desc 'Get a list of group issues' do + success Entities::Issue + end + params do + optional :state, type: String, values: %w[opened closed all], default: 'opened', + desc: 'Return opened, closed, or all issues' + use :issues_params + end get ":id/issues" do - group = find_group!(params[:id]) + group = find_group!(params.delete(:id)) - params[:state] ||= 'opened' params[:group_id] = group.id params[:milestone_title] = params.delete(:milestone) params[:label_name] = params.delete(:labels) - if params[:order_by] || params[:sort] - # The Sortable concern takes 'created_desc', not 'created_at_desc' (for example) - params[:sort] = "#{issuable_order_by.sub('_at', '')}_#{issuable_sort}" - end - issues = IssuesFinder.new(current_user, params).execute + issues = issues.reorder(params[:order_by] => params[:sort]) present paginate(issues), with: Entities::Issue, current_user: current_user end end @@ -98,32 +90,19 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do - # Get a list of project issues - # - # Parameters: - # id (required) - The ID of a project - # iid (optional) - Return the project issue having the given `iid` - # state (optional) - Return "opened" or "closed" issues - # labels (optional) - Comma-separated list of label names - # milestone (optional) - Milestone title - # order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` - # sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` - # - # Example Requests: - # GET /projects/:id/issues - # GET /projects/:id/issues?state=opened - # GET /projects/:id/issues?state=closed - # GET /projects/:id/issues?labels=foo - # GET /projects/:id/issues?labels=foo,bar - # GET /projects/:id/issues?labels=foo,bar&state=opened - # GET /projects/:id/issues?milestone=1.0.0 - # GET /projects/:id/issues?milestone=1.0.0&state=closed - # GET /issues?iid=42 + desc 'Get a list of project issues' do + success Entities::Issue + end + params do + optional :state, type: String, values: %w[opened closed all], default: 'all', + desc: 'Return opened, closed, or all issues' + optional :iid, type: Integer, desc: 'The IID of the issue' + use :issues_params + end get ":id/issues" do issues = IssuesFinder.new(current_user, project_id: user_project.id).execute.inc_notes_with_associations - issues = filter_issues_state(issues, params[:state]) unless params[:state].nil? + issues = filter_issues_state(issues, params[:state]) issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil? @@ -131,59 +110,49 @@ module API issues = filter_issues_milestone(issues, params[:milestone]) end - issues = issues.reorder(issuable_order_by => issuable_sort) - + issues = issues.reorder(params[:order_by] => params[:sort]) present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project end - # Get a single project issue - # - # Parameters: - # id (required) - The ID of a project - # issue_id (required) - The ID of a project issue - # Example Request: - # GET /projects/:id/issues/:issue_id + desc 'Get a single project issue' do + success Entities::Issue + end + params do + requires :issue_id, type: Integer, desc: 'The ID of a project issue' + end get ":id/issues/:issue_id" do - @issue = find_project_issue(params[:issue_id]) - present @issue, with: Entities::Issue, current_user: current_user, project: user_project + issue = find_project_issue(params[:issue_id]) + present issue, with: Entities::Issue, current_user: current_user, project: user_project end - # Create a new project issue - # - # Parameters: - # id (required) - The ID of a project - # title (required) - The title of an issue - # description (optional) - The description of an issue - # assignee_id (optional) - The ID of a user to assign issue - # milestone_id (optional) - The ID of a milestone to assign issue - # labels (optional) - The labels of an issue - # created_at (optional) - Date time string, ISO 8601 formatted - # due_date (optional) - Date time string in the format YEAR-MONTH-DAY - # confidential (optional) - Boolean parameter if the issue should be confidential - # merge_request_for_resolving_discussions (optional) - The IID of a merge request for which to resolve discussions - # Example Request: - # POST /projects/:id/issues + desc 'Create a new project issue' do + success Entities::Issue + end + params do + requires :title, type: String, desc: 'The title of an issue' + optional :created_at, type: DateTime, + desc: 'Date time when the issue was created. Available only for admins and project owners.' + optional :merge_request_for_resolving_discussions, type: Integer, + desc: 'The IID of a merge request for which to resolve discussions' + use :issue_params + end post ':id/issues' do - required_attributes! [:title] - - keys = [:title, :description, :assignee_id, :milestone_id, :due_date, :confidential, :labels, :merge_request_for_resolving_discussions] - keys << :created_at if current_user.admin? || user_project.owner == current_user - attrs = attributes_for_keys(keys) + # Setting created_at time only allowed for admins and project owners + unless current_user.admin? || user_project.owner == current_user + params.delete(:created_at) + end - attrs[:labels] = params[:labels] if params[:labels] + issue_params = declared_params(include_missing: false) if merge_request_iid = params[:merge_request_for_resolving_discussions] - attrs[:merge_request_for_resolving_discussions] = MergeRequestsFinder.new(current_user, project_id: user_project.id). + issue_params[:merge_request_for_resolving_discussions] = MergeRequestsFinder.new(current_user, project_id: user_project.id). execute. find_by(iid: merge_request_iid) end - # Convert and filter out invalid confidential flags - attrs['confidential'] = to_boolean(attrs['confidential']) - attrs.delete('confidential') if attrs['confidential'].nil? - - issue = ::Issues::CreateService.new(user_project, current_user, attrs.merge(request: request, api: true)).execute - + issue = ::Issues::CreateService.new(user_project, + current_user, + issue_params.merge(request: request, api: true)).execute if issue.spam? render_api_error!({ error: 'Spam detected' }, 400) end @@ -199,31 +168,26 @@ module API success Entities::Issue end params do - requires :id, type: String, desc: 'The ID of a project' - requires :issue_id, type: Integer, desc: "The ID of a project issue" - optional :title, type: String, desc: 'The new title of the issue' - optional :description, type: String, desc: 'The description of an issue' - optional :assignee_id, type: Integer, desc: 'The ID of a user to assign issue' - optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue' - optional :labels, type: String, desc: 'The labels of an issue' - optional :state_event, type: String, values: ['close', 'reopen'], desc: 'The state event of an issue' - # TODO 9.0, use the Grape DateTime type here - optional :updated_at, type: String, desc: 'Date time string, ISO 8601 formatted' - optional :due_date, type: String, desc: 'Date time string in the format YEAR-MONTH-DAY' - # TODO 9.0, use the Grape boolean type here - optional :confidential, type: String, desc: 'Boolean parameter if the issue should be confidential' + requires :issue_id, type: Integer, desc: 'The ID of a project issue' + optional :title, type: String, desc: 'The title of an issue' + optional :updated_at, type: DateTime, + desc: 'Date time when the issue was updated. Available only for admins and project owners.' + use :issue_params + at_least_one_of :title, :description, :assignee_id, :milestone_id, + :labels, :created_at, :due_date, :confidential, :state_event end put ':id/issues/:issue_id' do - issue = user_project.issues.find(params[:issue_id]) + issue = user_project.issues.find(params.delete(:issue_id)) authorize! :update_issue, issue - # Convert and filter out invalid confidential flags - params[:confidential] = to_boolean(params[:confidential]) - params.delete(:confidential) if params[:confidential].nil? - - params.delete(:updated_at) unless current_user.admin? || user_project.owner == current_user + # Setting created_at time only allowed for admins and project owners + unless current_user.admin? || user_project.owner == current_user + params.delete(:updated_at) + end - issue = ::Issues::UpdateService.new(user_project, current_user, issue_params).execute(issue) + issue = ::Issues::UpdateService.new(user_project, + current_user, + declared_params(include_missing: false)).execute(issue) if issue.valid? present issue, with: Entities::Issue, current_user: current_user, project: user_project @@ -232,19 +196,19 @@ module API end end - # Move an existing issue - # - # Parameters: - # id (required) - The ID of a project - # issue_id (required) - The ID of a project issue - # to_project_id (required) - The ID of the new project - # Example Request: - # POST /projects/:id/issues/:issue_id/move + desc 'Move an existing issue' do + success Entities::Issue + end + params do + requires :issue_id, type: Integer, desc: 'The ID of a project issue' + requires :to_project_id, type: Integer, desc: 'The ID of the new project' + end post ':id/issues/:issue_id/move' do - required_attributes! [:to_project_id] + issue = user_project.issues.find_by(id: params[:issue_id]) + not_found!('Issue') unless issue - issue = user_project.issues.find(params[:issue_id]) - new_project = Project.find(params[:to_project_id]) + new_project = Project.find_by(id: params[:to_project_id]) + not_found!('Project') unless new_project begin issue = ::Issues::MoveService.new(user_project, current_user).execute(issue, new_project) @@ -254,16 +218,13 @@ module API end end - # - # Delete a project issue - # - # Parameters: - # id (required) - The ID of a project - # issue_id (required) - The ID of a project issue - # Example Request: - # DELETE /projects/:id/issues/:issue_id + desc 'Delete a project issue' + params do + requires :issue_id, type: Integer, desc: 'The ID of a project issue' + end delete ":id/issues/:issue_id" do issue = user_project.issues.find_by(id: params[:issue_id]) + not_found!('Issue') unless issue authorize!(:destroy_issue, issue) issue.destroy diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 553983575c4..5c80dd98dc7 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -72,13 +72,6 @@ describe API::Issues, api: true do expect(json_response.last).to have_key('web_url') end - it "adds pagination headers and keep query params" do - get api("/issues?state=closed&per_page=3", user) - expect(response.headers['Link']).to eq( - '<http://www.example.com/api/v3/issues?page=1&per_page=3&private_token=%s&state=closed>; rel="first", <http://www.example.com/api/v3/issues?page=1&per_page=3&private_token=%s&state=closed>; rel="last"' % [user.private_token, user.private_token] - ) - end - it 'returns an array of closed issues' do get api('/issues?state=closed', user) expect(response).to have_http_status(200) @@ -649,9 +642,8 @@ describe API::Issues, api: true do post api("/projects/#{project.id}/issues", user), title: 'new issue', confidential: 'foo' - expect(response).to have_http_status(201) - expect(json_response['title']).to eq('new issue') - expect(json_response['confidential']).to be_falsy + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('confidential is invalid') end it "sends notifications for subscribers of newly added labels" do @@ -862,8 +854,8 @@ describe API::Issues, api: true do put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), confidential: 'foo' - expect(response).to have_http_status(200) - expect(json_response['confidential']).to be_truthy + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('confidential is invalid') end end end @@ -985,6 +977,14 @@ describe API::Issues, api: true do expect(json_response['state']).to eq 'opened' end end + + context 'when issue does not exist' do + it 'returns 404 when trying to move an issue' do + delete api("/projects/#{project.id}/issues/123", user) + + expect(response).to have_http_status(404) + end + end end describe '/projects/:id/issues/:issue_id/move' do @@ -1033,6 +1033,7 @@ describe API::Issues, api: true do to_project_id: target_project.id expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Issue Not Found') end end @@ -1042,6 +1043,7 @@ describe API::Issues, api: true do to_project_id: target_project.id expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Project Not Found') end end -- cgit v1.2.1 From 67b7637e5d7d3cf3e3f5cde6e7f984ece368c48c Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Wed, 7 Dec 2016 11:33:32 +0200 Subject: Apply review comments. Iteration 1 --- lib/bitbucket/client.rb | 29 +++++++++++----------- lib/bitbucket/connection.rb | 8 +++--- lib/bitbucket/paginator.rb | 2 +- lib/bitbucket/representation/issue.rb | 6 +---- .../representation/pull_request_comment.rb | 2 +- lib/bitbucket/representation/repo.rb | 6 +++-- lib/gitlab/bitbucket_import/importer.rb | 2 +- 7 files changed, 26 insertions(+), 29 deletions(-) diff --git a/lib/bitbucket/client.rb b/lib/bitbucket/client.rb index 33e977d655d..9fa44506374 100644 --- a/lib/bitbucket/client.rb +++ b/lib/bitbucket/client.rb @@ -5,40 +5,39 @@ module Bitbucket end def issues(repo) - relative_path = "/repositories/#{repo}/issues" - paginator = Paginator.new(connection, relative_path, :issue) + path = "/repositories/#{repo}/issues" + paginator = Paginator.new(connection, path, :issue) Collection.new(paginator) end - def issue_comments(repo, number) - relative_path = "/repositories/#{repo}/issues/#{number}/comments" - paginator = Paginator.new(connection, relative_path, :url) + def issue_comments(repo, issue_id) + path = "/repositories/#{repo}/issues/#{issue_id}/comments" + paginator = Paginator.new(connection, path, :url) Collection.new(paginator).map do |comment_url| - parsed_response = connection.get(comment_url.to_s) - Representation::Comment.new(parsed_response) + Representation::Comment.new(connection.get(comment_url.to_s)) end end def pull_requests(repo) - relative_path = "/repositories/#{repo}/pullrequests?state=ALL" - paginator = Paginator.new(connection, relative_path, :pull_request) + path = "/repositories/#{repo}/pullrequests?state=ALL" + paginator = Paginator.new(connection, path, :pull_request) Collection.new(paginator) end def pull_request_comments(repo, pull_request) - relative_path = "/repositories/#{repo}/pullrequests/#{pull_request}/comments" - paginator = Paginator.new(connection, relative_path, :pull_request_comment) + path = "/repositories/#{repo}/pullrequests/#{pull_request}/comments" + paginator = Paginator.new(connection, path, :pull_request_comment) Collection.new(paginator) end def pull_request_diff(repo, pull_request) - relative_path = "/repositories/#{repo}/pullrequests/#{pull_request}/diff" + path = "/repositories/#{repo}/pullrequests/#{pull_request}/diff" - connection.get(relative_path) + connection.get(path) end def repo(name) @@ -47,8 +46,8 @@ module Bitbucket end def repos - relative_path = "/repositories/#{user.username}" - paginator = Paginator.new(connection, relative_path, :repo) + path = "/repositories/#{user.username}" + paginator = Paginator.new(connection, path, :repo) Collection.new(paginator) end diff --git a/lib/bitbucket/connection.rb b/lib/bitbucket/connection.rb index e28285f119c..692a596c057 100644 --- a/lib/bitbucket/connection.rb +++ b/lib/bitbucket/connection.rb @@ -5,8 +5,8 @@ module Bitbucket DEFAULT_QUERY = {} def initialize(options = {}) - @api_version = options.fetch(:api_version, DEFAULT_API_VERSION) - @base_uri = options.fetch(:base_uri, DEFAULT_BASE_URI) + @api_version = options.fetch(:api_version, DEFAULT_API_VERSION) + @base_uri = options.fetch(:base_uri, DEFAULT_BASE_URI) @default_query = options.fetch(:query, DEFAULT_QUERY) @token = options[:token] @@ -23,7 +23,7 @@ module Bitbucket @connection ||= OAuth2::AccessToken.new(client, @token, refresh_token: @refresh_token, expires_at: @expires_at, expires_in: @expires_in) end - def query(params = {}) + def set_default_query_parameters(params = {}) @default_query.merge!(params) end @@ -63,7 +63,7 @@ module Bitbucket end def provider - Gitlab.config.omniauth.providers.find { |provider| provider.name == 'bitbucket' } + Gitlab::OAuth::Provider.config_for('bitbucket') end def options diff --git a/lib/bitbucket/paginator.rb b/lib/bitbucket/paginator.rb index a1672d9eaa1..d0e23007ff8 100644 --- a/lib/bitbucket/paginator.rb +++ b/lib/bitbucket/paginator.rb @@ -8,7 +8,7 @@ module Bitbucket @url = url @page = nil - connection.query(pagelen: PAGE_LENGTH, sort: :created_on) + connection.set_default_query_parameters(pagelen: PAGE_LENGTH, sort: :created_on) end def next diff --git a/lib/bitbucket/representation/issue.rb b/lib/bitbucket/representation/issue.rb index 48647ad51f6..dc034c19750 100644 --- a/lib/bitbucket/representation/issue.rb +++ b/lib/bitbucket/representation/issue.rb @@ -8,7 +8,7 @@ module Bitbucket end def author - reporter.fetch('username', 'Anonymous') + raw.dig('reporter', 'username') || 'Anonymous' end def description @@ -40,10 +40,6 @@ module Bitbucket def closed? CLOSED_STATUS.include?(raw['state']) end - - def reporter - raw.fetch('reporter', {}) - end end end end diff --git a/lib/bitbucket/representation/pull_request_comment.rb b/lib/bitbucket/representation/pull_request_comment.rb index c63d749cba7..ae2b069d6a2 100644 --- a/lib/bitbucket/representation/pull_request_comment.rb +++ b/lib/bitbucket/representation/pull_request_comment.rb @@ -18,7 +18,7 @@ module Bitbucket end def parent_id - raw.fetch('parent', {}).fetch('id', nil) + raw.dig('parent', 'id') end def inline? diff --git a/lib/bitbucket/representation/repo.rb b/lib/bitbucket/representation/repo.rb index b291dfe0441..8969ecd1c19 100644 --- a/lib/bitbucket/representation/repo.rb +++ b/lib/bitbucket/representation/repo.rb @@ -23,7 +23,9 @@ module Bitbucket url = raw['links']['clone'].find { |link| link['name'] == 'https' }.fetch('href') if token.present? - url.sub(/^[^\@]*/, "https://x-token-auth:#{token}") + clone_url = URI::parse(url) + clone_url.user = "x-token-auth:#{token}" + clone_url.to_s else url end @@ -37,7 +39,7 @@ module Bitbucket raw['full_name'] end - def has_issues? + def issues_enabled? raw['has_issues'] end diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 34d93542955..0f583b07e93 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -32,7 +32,7 @@ module Gitlab end def import_issues - return unless repo.has_issues? + return unless repo.issues_enabled? client.issues(repo).each do |issue| description = @formatter.author_line(issue.author) -- cgit v1.2.1 From dec384cf23787e156eac8d633f70fb40479a5283 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Wed, 7 Dec 2016 11:31:01 +0100 Subject: Update outdated visible content spec descriptions --- spec/models/repository_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index d9b0e63eeb6..b5a42edd192 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1176,7 +1176,7 @@ describe Repository, models: true do end describe '#after_create_branch' do - it 'flushes the visible content cache' do + it 'expires the branch caches' do expect(repository).to receive(:expire_branches_cache) repository.after_create_branch @@ -1184,7 +1184,7 @@ describe Repository, models: true do end describe '#after_remove_branch' do - it 'flushes the visible content cache' do + it 'expires the branch caches' do expect(repository).to receive(:expire_branches_cache) repository.after_remove_branch -- cgit v1.2.1 From 9e307b93b7c57a14de6e425566f88511024859a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Fri, 2 Dec 2016 14:33:29 +0100 Subject: Allow public access to some Tag API endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- changelogs/unreleased/public-tags-api.yml | 4 +++ doc/api/tags.md | 7 ++-- lib/api/tags.rb | 1 - spec/requests/api/tags_spec.rb | 55 +++++++++++++++++++++++++++---- 4 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 changelogs/unreleased/public-tags-api.yml diff --git a/changelogs/unreleased/public-tags-api.yml b/changelogs/unreleased/public-tags-api.yml new file mode 100644 index 00000000000..f5e844470b2 --- /dev/null +++ b/changelogs/unreleased/public-tags-api.yml @@ -0,0 +1,4 @@ +--- +title: Allow public access to some Tag API endpoints +merge_request: +author: diff --git a/doc/api/tags.md b/doc/api/tags.md index 14573d48fe4..7f78ffc2390 100644 --- a/doc/api/tags.md +++ b/doc/api/tags.md @@ -2,7 +2,9 @@ ## List project repository tags -Get a list of repository tags from a project, sorted by name in reverse alphabetical order. +Get a list of repository tags from a project, sorted by name in reverse +alphabetical order. This endpoint can be accessed without authentication if the +repository is publicly accessible. ``` GET /projects/:id/repository/tags @@ -40,7 +42,8 @@ Parameters: ## Get a single repository tag -Get a specific repository tag determined by its name. +Get a specific repository tag determined by its name. This endpoint can be +accessed without authentication if the repository is publicly accessible. ``` GET /projects/:id/repository/tags/:tag_name diff --git a/lib/api/tags.rb b/lib/api/tags.rb index cd33f9a9903..5b345db3a41 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -1,7 +1,6 @@ module API # Git Tags API class Tags < Grape::API - before { authenticate! } before { authorize! :download_code, user_project } params do diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index 06fa94fae87..a1c32ae65ba 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -15,6 +15,31 @@ describe API::Tags, api: true do let(:tag_name) { project.repository.tag_names.sort.reverse.first } let(:description) { 'Awesome release!' } + shared_examples_for 'repository tags' do + it 'returns the repository tags' do + get api("/projects/#{project.id}/repository/tags", current_user) + + expect(response).to have_http_status(200) + + first_tag = json_response.first + + expect(first_tag['name']).to eq(tag_name) + end + end + + context 'when unauthenticated' do + it_behaves_like 'repository tags' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end + end + + context 'when authenticated' do + it_behaves_like 'repository tags' do + let(:current_user) { user } + end + end + context 'without releases' do it "returns an array of project tags" do get api("/projects/#{project.id}/repository/tags", user) @@ -45,17 +70,33 @@ describe API::Tags, api: true do describe 'GET /projects/:id/repository/tags/:tag_name' do let(:tag_name) { project.repository.tag_names.sort.reverse.first } - it 'returns a specific tag' do - get api("/projects/#{project.id}/repository/tags/#{tag_name}", user) + shared_examples_for 'repository tag' do + it 'returns the repository tag' do + get api("/projects/#{project.id}/repository/tags/#{tag_name}", current_user) + + expect(response).to have_http_status(200) + + expect(json_response['name']).to eq(tag_name) + end + + it 'returns 404 for an invalid tag name' do + get api("/projects/#{project.id}/repository/tags/foobar", current_user) - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(tag_name) + expect(response).to have_http_status(404) + end end - it 'returns 404 for an invalid tag name' do - get api("/projects/#{project.id}/repository/tags/foobar", user) + context 'when unauthenticated' do + it_behaves_like 'repository tag' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end + end - expect(response).to have_http_status(404) + context 'when authenticated' do + it_behaves_like 'repository tag' do + let(:current_user) { user } + end end end -- cgit v1.2.1 From 186c4dd7b4f6d4edc68c6fd65703665d14877e3e Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 30 Nov 2016 11:57:38 +0000 Subject: Fix broken link for latest deployment Updates changelog with MR ID --- app/helpers/environment_helper.rb | 6 ++++++ app/views/projects/builds/show.html.haml | 3 +-- .../unreleased/25136-last-deployment-link.yml | 4 ++++ spec/features/projects/builds_spec.rb | 22 ++++++++++++++++++++++ 4 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/25136-last-deployment-link.yml diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb index 27975b7ddb7..5fde912ab2c 100644 --- a/app/helpers/environment_helper.rb +++ b/app/helpers/environment_helper.rb @@ -20,6 +20,12 @@ module EnvironmentHelper link_to "##{deployment.iid}", [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable] end + def last_deployment_link(deployment, link_text) + return unless deployment + + link_to link_text, [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable] + end + def last_deployment_link_for_environment_build(project, build) environment = environment_for_build(project, build) return unless environment diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 108674dbba6..46a1969b348 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -46,8 +46,7 @@ - else This build is creating a deployment to #{environment_link_for_build(@build.project, @build)} - if environment.try(:last_deployment) - and will overwrite the - = link_to 'latest deployment', deployment_link(environment.last_deployment) + and will overwrite the #{last_deployment_link(environment.last_deployment, 'latest deployment')} .prepend-top-default - if @build.erased? diff --git a/changelogs/unreleased/25136-last-deployment-link.yml b/changelogs/unreleased/25136-last-deployment-link.yml new file mode 100644 index 00000000000..eab1534aa66 --- /dev/null +++ b/changelogs/unreleased/25136-last-deployment-link.yml @@ -0,0 +1,4 @@ +--- +title: Fix Latest deployment link is broken +merge_request: 7839 +author: diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb index a0ccc472d11..f25a0abb33d 100644 --- a/spec/features/projects/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -227,6 +227,28 @@ feature 'Builds', :feature do expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1') end end + + context "Build starts environment" do + context "Build is successfull and has deployment" do + it "shows a link for the build" do + -# link to environment.name + expect(page).to have_link() + end + end + + context "Build is complete and not successfull" do + it "shows a link for the build" do + -# link to environment.name + expect(page).to have_link() + end + end + + context "Build creates a new deployment" do + it "shows a link to lastest deployment" do + expect(page).to have_link("latest deployment") + end + end + end end describe "POST /:project/builds/:id/cancel" do -- cgit v1.2.1 From 19e1b3246dfe085ef604c820e3bc8263e5bffd43 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 2 Dec 2016 17:10:37 +0000 Subject: Updates tests Fix tests Fix rubocop error Fix broken test --- spec/features/projects/builds_spec.rb | 42 +++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb index f25a0abb33d..2b6ad5e3420 100644 --- a/spec/features/projects/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -228,24 +228,42 @@ feature 'Builds', :feature do end end - context "Build starts environment" do - context "Build is successfull and has deployment" do - it "shows a link for the build" do - -# link to environment.name - expect(page).to have_link() + context 'When build starts environment' do + context 'Build is successfull and has deployment' do + it 'shows a link for the build' do + environment = create(:environment, project: project) + pipeline = create(:ci_pipeline, project: project) + deployment = create(:deployment) + build1 = create(:ci_build, :success, environment: environment.name, deployments: [deployment], pipeline: pipeline) + + visit namespace_project_build_path(project.namespace, project, build1) + + expect(page).to have_link environment.name end end - context "Build is complete and not successfull" do - it "shows a link for the build" do - -# link to environment.name - expect(page).to have_link() + context 'Build is complete and not successfull' do + it 'shows a link for the build' do + environment = create(:environment, project: project) + pipeline = create(:ci_pipeline, project: project) + build1 = create(:ci_build, :failed, environment: environment.name, pipeline: pipeline) + + visit namespace_project_build_path(project.namespace, project, build1) + + expect(page).to have_link environment.name end end - context "Build creates a new deployment" do - it "shows a link to lastest deployment" do - expect(page).to have_link("latest deployment") + context 'Build creates a new deployment' do + it 'shows a link to lastest deployment' do + environment = create(:environment, project: project) + create(:deployment, environment: environment, sha: project.commit.id) + pipeline = create(:ci_pipeline, project: project) + build1 = create(:ci_build, :success, environment: environment.name, pipeline: pipeline) + + visit namespace_project_build_path(project.namespace, project, build1) + + expect(page).to have_link('latest deployment') end end end -- cgit v1.2.1 From 2dc907bc1761499e27c0a75eeccbbd350b38f80e Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 7 Dec 2016 10:34:00 +0000 Subject: Changes after review Fix error --- app/helpers/environment_helper.rb | 14 ++++++-------- app/views/projects/builds/show.html.haml | 2 +- spec/features/projects/builds_spec.rb | 8 ++++---- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb index 5fde912ab2c..96d6f64eb8e 100644 --- a/app/helpers/environment_helper.rb +++ b/app/helpers/environment_helper.rb @@ -14,16 +14,14 @@ module EnvironmentHelper end end - def deployment_link(deployment) + def deployment_link(deployment, text) return unless deployment - link_to "##{deployment.iid}", [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable] - end - - def last_deployment_link(deployment, link_text) - return unless deployment - - link_to link_text, [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable] + if text + link_to text, [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable] + else + link_to "##{deployment.iid}", [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable] + end end def last_deployment_link_for_environment_build(project, build) diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 46a1969b348..3ef46872199 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -46,7 +46,7 @@ - else This build is creating a deployment to #{environment_link_for_build(@build.project, @build)} - if environment.try(:last_deployment) - and will overwrite the #{last_deployment_link(environment.last_deployment, 'latest deployment')} + and will overwrite the #{deployment_link(environment.last_deployment, 'latest deployment')} .prepend-top-default - if @build.erased? diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb index 2b6ad5e3420..ea99239d5fc 100644 --- a/spec/features/projects/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -228,8 +228,8 @@ feature 'Builds', :feature do end end - context 'When build starts environment' do - context 'Build is successfull and has deployment' do + context 'when build starts environment' do + context 'build is successfull and has deployment' do it 'shows a link for the build' do environment = create(:environment, project: project) pipeline = create(:ci_pipeline, project: project) @@ -242,7 +242,7 @@ feature 'Builds', :feature do end end - context 'Build is complete and not successfull' do + context 'build is complete and not successfull' do it 'shows a link for the build' do environment = create(:environment, project: project) pipeline = create(:ci_pipeline, project: project) @@ -254,7 +254,7 @@ feature 'Builds', :feature do end end - context 'Build creates a new deployment' do + context 'build creates a new deployment' do it 'shows a link to lastest deployment' do environment = create(:environment, project: project) create(:deployment, environment: environment, sha: project.commit.id) -- cgit v1.2.1 From 9cc19d9b2ce6af378dce67260c13f208ddc40bca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Mon, 5 Dec 2016 16:00:26 +0100 Subject: Remove wrong '.builds-feature' class from the MR settings fieldset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- .../_merge_request_merge_settings.html.haml | 15 +++++ .../projects/_merge_request_settings.html.haml | 26 +++----- app/views/projects/edit.html.haml | 3 +- ...es-settings-hidden-when-builds-are-disabled.yml | 4 ++ .../settings/merge_requests_settings_spec.rb | 70 ++++++++++++++++++++++ 5 files changed, 99 insertions(+), 19 deletions(-) create mode 100644 app/views/projects/_merge_request_merge_settings.html.haml create mode 100644 changelogs/unreleased/25171-fix-mr-features-settings-hidden-when-builds-are-disabled.yml create mode 100644 spec/features/projects/settings/merge_requests_settings_spec.rb diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml new file mode 100644 index 00000000000..afe2fd7fd7b --- /dev/null +++ b/app/views/projects/_merge_request_merge_settings.html.haml @@ -0,0 +1,15 @@ +- form = local_assigns.fetch(:form) + +.form-group + .checkbox.builds-feature + = form.label :only_allow_merge_if_build_succeeds do + = form.check_box :only_allow_merge_if_build_succeeds + %strong Only allow merge requests to be merged if the build succeeds + %br + %span.descr + Builds need to be configured to enable this feature. + = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_build_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds') + .checkbox + = form.label :only_allow_merge_if_all_discussions_are_resolved do + = form.check_box :only_allow_merge_if_all_discussions_are_resolved + %strong Only allow merge requests to be merged if all discussions are resolved diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml index 6e143c4b570..818010bc7d3 100644 --- a/app/views/projects/_merge_request_settings.html.haml +++ b/app/views/projects/_merge_request_settings.html.haml @@ -1,18 +1,8 @@ -.merge-requests-feature - %fieldset.builds-feature - %hr - %h5.prepend-top-0 - Merge Requests - .form-group - .checkbox - = f.label :only_allow_merge_if_build_succeeds do - = f.check_box :only_allow_merge_if_build_succeeds - %strong Only allow merge requests to be merged if the build succeeds - %br - %span.descr - Builds need to be configured to enable this feature. - = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_build_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds') - .checkbox - = f.label :only_allow_merge_if_all_discussions_are_resolved do - = f.check_box :only_allow_merge_if_all_discussions_are_resolved - %strong Only allow merge requests to be merged if all discussions are resolved +- form = local_assigns.fetch(:form) + +%fieldset.features.merge-requests-feature.append-bottom-default + %hr + %h5.prepend-top-0 + Merge Requests + + = render 'projects/merge_request_merge_settings', form: form diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 3a5af2723c6..01cd8fa0938 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -112,7 +112,8 @@ %span.descr Enable Container Registry for this project = link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank' - = render 'merge_request_settings', f: f + = render 'merge_request_settings', form: f + %hr %fieldset.features.append-bottom-default %h5.prepend-top-0 diff --git a/changelogs/unreleased/25171-fix-mr-features-settings-hidden-when-builds-are-disabled.yml b/changelogs/unreleased/25171-fix-mr-features-settings-hidden-when-builds-are-disabled.yml new file mode 100644 index 00000000000..a7576e2cbdb --- /dev/null +++ b/changelogs/unreleased/25171-fix-mr-features-settings-hidden-when-builds-are-disabled.yml @@ -0,0 +1,4 @@ +--- +title: Remove wrong '.builds-feature' class from the MR settings fieldset +merge_request: 7930 +author: diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb new file mode 100644 index 00000000000..4bfaa499272 --- /dev/null +++ b/spec/features/projects/settings/merge_requests_settings_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +feature 'Project settings > Merge Requests', feature: true, js: true do + include GitlabRoutingHelper + + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + + background do + project.team << [user, :master] + login_as(user) + end + + context 'when Merge Request and Builds are initially enabled' do + before do + project.project_feature.update_attribute('merge_requests_access_level', ProjectFeature::ENABLED) + end + + context 'when Builds are initially enabled' do + before do + project.project_feature.update_attribute('builds_access_level', ProjectFeature::ENABLED) + visit edit_project_path(project) + end + + scenario 'shows the Merge Requests settings' do + expect(page).to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') + + select 'Disabled', from: "project_project_feature_attributes_merge_requests_access_level" + + expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved') + end + end + + context 'when Builds are initially disabled' do + before do + project.project_feature.update_attribute('builds_access_level', ProjectFeature::DISABLED) + visit edit_project_path(project) + end + + scenario 'shows the Merge Requests settings that do not depend on Builds feature' do + expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') + + select 'Everyone with access', from: "project_project_feature_attributes_builds_access_level" + + expect(page).to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') + end + end + end + + context 'when Merge Request are initially disabled' do + before do + project.project_feature.update_attribute('merge_requests_access_level', ProjectFeature::DISABLED) + visit edit_project_path(project) + end + + scenario 'does not show the Merge Requests settings' do + expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved') + + select 'Everyone with access', from: "project_project_feature_attributes_merge_requests_access_level" + + expect(page).to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') + end + end +end -- cgit v1.2.1 From c4e46c5740426f656c019b51b9731ea8a16f42d0 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Wed, 7 Dec 2016 12:47:06 +0100 Subject: Fix success status --- app/models/ci/pipeline.rb | 3 ++- app/models/concerns/has_status.rb | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index d14825c082a..fda8228a1e9 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -105,7 +105,8 @@ module Ci end def stages_name - statuses.order(:stage_idx).distinct.pluck(:stage) + statuses.order(:stage_idx).distinct. + pluck(:stage, :stage_idx).map(&:first) end def stages diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 43f312d319e..90432fc4050 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -26,7 +26,7 @@ module HasStatus WHEN (#{builds})=(#{skipped}) THEN 'skipped' WHEN (#{builds})=(#{success}) THEN 'success' WHEN (#{builds})=(#{created}) THEN 'created' - WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'skipped' + WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'success' WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled' WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending' WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running' -- cgit v1.2.1 From b12d6541835024eb74384551b84bf0e74747d0c3 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Wed, 7 Dec 2016 14:00:06 +0200 Subject: BitBuckpet importer. Refactoring. Iteration 2 --- app/controllers/import/bitbucket_controller.rb | 2 +- lib/bitbucket/client.rb | 6 ++---- lib/bitbucket/page.rb | 6 ++---- lib/bitbucket/paginator.rb | 4 ++-- lib/bitbucket/representation/base.rb | 4 ++++ lib/bitbucket/representation/url.rb | 9 --------- lib/gitlab/bitbucket_import/importer.rb | 7 +++++++ 7 files changed, 18 insertions(+), 20 deletions(-) delete mode 100644 lib/bitbucket/representation/url.rb diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 9c97a97a5dd..12716d60e7d 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 provider - Gitlab.config.omniauth.providers.find { |provider| provider.name == 'bitbucket' } + Gitlab::OAuth::Provider.config_for('bitbucket') end def options diff --git a/lib/bitbucket/client.rb b/lib/bitbucket/client.rb index 9fa44506374..3457c2c6454 100644 --- a/lib/bitbucket/client.rb +++ b/lib/bitbucket/client.rb @@ -13,11 +13,9 @@ module Bitbucket def issue_comments(repo, issue_id) path = "/repositories/#{repo}/issues/#{issue_id}/comments" - paginator = Paginator.new(connection, path, :url) + paginator = Paginator.new(connection, path, :comment) - Collection.new(paginator).map do |comment_url| - Representation::Comment.new(connection.get(comment_url.to_s)) - end + Collection.new(paginator) end def pull_requests(repo) diff --git a/lib/bitbucket/page.rb b/lib/bitbucket/page.rb index 49d083cc66f..8f50f67f84d 100644 --- a/lib/bitbucket/page.rb +++ b/lib/bitbucket/page.rb @@ -18,14 +18,12 @@ module Bitbucket private def parse_attrs(raw) - attrs = %w(size page pagelen next previous) - attrs.map { |attr| { attr.to_sym => raw[attr] } }.reduce(&:merge) + raw.slice(*%w(size page pagelen next previous)).symbolize_keys end def parse_values(raw, bitbucket_rep_class) return [] unless raw['values'] && raw['values'].is_a?(Array) - - raw['values'].map { |hash| bitbucket_rep_class.new(hash) } + bitbucket_rep_class.decorate(raw['values']) end def representation_class(type) diff --git a/lib/bitbucket/paginator.rb b/lib/bitbucket/paginator.rb index d0e23007ff8..37f12328447 100644 --- a/lib/bitbucket/paginator.rb +++ b/lib/bitbucket/paginator.rb @@ -26,12 +26,12 @@ module Bitbucket page.nil? || page.next? end - def page_url + def next_url page.nil? ? url : page.next end def fetch_next_page - parsed_response = connection.get(page_url) + parsed_response = connection.get(next_url) Page.new(parsed_response, type) end end diff --git a/lib/bitbucket/representation/base.rb b/lib/bitbucket/representation/base.rb index 7b639492d38..94adaacc9b5 100644 --- a/lib/bitbucket/representation/base.rb +++ b/lib/bitbucket/representation/base.rb @@ -5,6 +5,10 @@ module Bitbucket @raw = raw end + def self.decorate(entries) + entries.map { |entry| new(entry)} + end + private attr_reader :raw diff --git a/lib/bitbucket/representation/url.rb b/lib/bitbucket/representation/url.rb deleted file mode 100644 index 24ae1048013..00000000000 --- a/lib/bitbucket/representation/url.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Bitbucket - module Representation - class Url < Representation::Base - def to_s - raw.dig('links', 'self', 'href') - end - end - end -end diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 0f583b07e93..825d43e6589 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -50,6 +50,13 @@ module Gitlab if issue.persisted? client.issue_comments(repo, issue.iid).each do |comment| + # The note can be blank for issue service messages like "Chenged title: ..." + # We would like to import those comments as well but there is no any + # specific parameter that would allow to process them, it's just an empty comment. + # To prevent our importer from just crashing or from creating useless empty comments + # we do this check. + next unless comment.note.present? + note = @formatter.author_line(comment.author) note += comment.note -- cgit v1.2.1 From 93c72e0f71919968972e874519e01370edcce022 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Wed, 7 Dec 2016 13:14:45 +0100 Subject: Add Ci::Status::Factory --- lib/gitlab/ci/status/factory.rb | 43 +++++++++++++++++++++++++++++++ lib/gitlab/ci/status/pipeline/factory.rb | 30 ++++----------------- lib/gitlab/ci/status/stage/factory.rb | 28 ++------------------ spec/lib/gitlab/ci/status/factory_spec.rb | 26 +++++++++++++++++++ 4 files changed, 76 insertions(+), 51 deletions(-) create mode 100644 lib/gitlab/ci/status/factory.rb create mode 100644 spec/lib/gitlab/ci/status/factory_spec.rb diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb new file mode 100644 index 00000000000..b2f896f2211 --- /dev/null +++ b/lib/gitlab/ci/status/factory.rb @@ -0,0 +1,43 @@ +module Gitlab + module Ci + module Status + class Factory + attr_reader :subject + + def initialize(subject) + @subject = subject + end + + def fabricate! + if extended_status + extended_status.new(core_status) + else + core_status + end + end + + private + + def subject_status + @subject_status ||= subject.status + end + + def core_status + Gitlab::Ci::Status + .const_get(subject_status.capitalize) + .new(subject) + end + + def extended_status + @extended ||= extended_statuses.find do |status| + status.matches?(subject) + end + end + + def extended_statuses + [] + end + end + end + end +end diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb index 71d27bf7cf5..4ac4ec671d0 100644 --- a/lib/gitlab/ci/status/pipeline/factory.rb +++ b/lib/gitlab/ci/status/pipeline/factory.rb @@ -2,35 +2,15 @@ module Gitlab module Ci module Status module Pipeline - class Factory - EXTENDED_STATUSES = [Pipeline::SuccessWithWarnings] - - def initialize(pipeline) - @pipeline = pipeline - @status = pipeline.status || :created - end - - def fabricate! - if extended_status - extended_status.new(core_status) - else - core_status - end - end - + class Factory < Status::Factory private - def core_status - Gitlab::Ci::Status - .const_get(@status.capitalize) - .new(@pipeline) - .extend(Status::Pipeline::Common) + def extended_statuses + [Pipeline::SuccessWithWarnings] end - def extended_status - @extended ||= EXTENDED_STATUSES.find do |status| - status.matches?(@pipeline) - end + def core_status + super.extend(Status::Pipeline::Common) end end end diff --git a/lib/gitlab/ci/status/stage/factory.rb b/lib/gitlab/ci/status/stage/factory.rb index a2f7ad81d39..c6522d5ada1 100644 --- a/lib/gitlab/ci/status/stage/factory.rb +++ b/lib/gitlab/ci/status/stage/factory.rb @@ -2,35 +2,11 @@ module Gitlab module Ci module Status module Stage - class Factory - EXTENDED_STATUSES = [] - - def initialize(stage) - @stage = stage - @status = stage.status || :created - end - - def fabricate! - if extended_status - extended_status.new(core_status) - else - core_status - end - end - + class Factory < Status::Factory private def core_status - Gitlab::Ci::Status - .const_get(@status.capitalize) - .new(@stage) - .extend(Status::Stage::Common) - end - - def extended_status - @extended ||= EXTENDED_STATUSES.find do |status| - status.matches?(@stage) - end + super.extend(Status::Stage::Common) end end end diff --git a/spec/lib/gitlab/ci/status/factory_spec.rb b/spec/lib/gitlab/ci/status/factory_spec.rb new file mode 100644 index 00000000000..8ebdbddb001 --- /dev/null +++ b/spec/lib/gitlab/ci/status/factory_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Factory do + let(:object) { double(status: :created) } + + subject do + described_class.new(object) + end + + let(:status) do + subject.fabricate! + end + + context 'when object has a core status' do + HasStatus::AVAILABLE_STATUSES.each do |core_status| + context "when core status is #{core_status}" do + let(:object) { double(status: core_status) } + + it "fabricates a core status #{core_status}" do + expect(status).to be_a( + Gitlab::Ci::Status.const_get(core_status.capitalize)) + end + end + end + end +end -- cgit v1.2.1 From 26cd1dd21edaf37f3c74e001ec49555734674731 Mon Sep 17 00:00:00 2001 From: Pedro Moreira da Silva <pedro@gitlab.com> Date: Wed, 7 Dec 2016 12:15:41 +0000 Subject: Add changelog for !7850. https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7850 --- ...most-slash-command-for-issue-create-needs-better-documentation.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml diff --git a/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml b/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml new file mode 100644 index 00000000000..531b0f83099 --- /dev/null +++ b/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml @@ -0,0 +1,4 @@ +--- +title: Improve help message for issue create slash command +merge_request: 7850 +author: -- cgit v1.2.1 From eb8a713caac13de61321970c4dc2715e1c0929f2 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 7 Dec 2016 09:19:08 +0000 Subject: Fixed timeago re-rendering every element Fixes a performance regression that was causing timeago to get re-rendered on every element & therefore adding a lot of new setTimeouts --- app/assets/javascripts/lib/utils/datetime_utility.js | 4 +++- changelogs/unreleased/timeago-perf-fix.yml | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/timeago-perf-fix.yml diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 963d2851e5f..e8e502694d6 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -29,7 +29,7 @@ setTimeago = true; } - $timeagoEls.each(function() { + $timeagoEls.filter(':not([data-timeago-rendered])').each(function() { var $el = $(this); $el.attr('title', gl.utils.formatDate($el.attr('datetime'))); @@ -39,6 +39,8 @@ template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' }); } + + $el.attr('data-timeago-rendered', true); gl.utils.renderTimeago($el); }); }; diff --git a/changelogs/unreleased/timeago-perf-fix.yml b/changelogs/unreleased/timeago-perf-fix.yml new file mode 100644 index 00000000000..265e7db29a9 --- /dev/null +++ b/changelogs/unreleased/timeago-perf-fix.yml @@ -0,0 +1,4 @@ +--- +title: Fixed timeago re-rendering every timeago +merge_request: +author: -- cgit v1.2.1 From 2fc1e643a7d8fb2fb4f9df49a2347d4f82776e99 Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Wed, 7 Dec 2016 12:39:19 +0000 Subject: Fix Backup::Manager#remove_old --- lib/backup/manager.rb | 19 ++--- spec/lib/gitlab/backup/manager_spec.rb | 127 +++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 9 deletions(-) create mode 100644 spec/lib/gitlab/backup/manager_spec.rb diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 96c20100541..7e6537e3d9e 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -82,16 +82,17 @@ module Backup removed = 0 Dir.chdir(Gitlab.config.backup.path) do - file_list = Dir.glob('*_gitlab_backup.tar') - file_list.map! do |path_string| - if path_string =~ /(\d+)(?:_\d{4}_\d{2}_\d{2})?_gitlab_backup\.tar/ - { timestamp: $1.to_i, path: path_string } - end - end - file_list.sort.each do |file| - if Time.at(file[:timestamp]) < (Time.now - keep_time) - if Kernel.system(*%W(rm #{file[:path]})) + Dir.glob('*_gitlab_backup.tar').each do |file| + next unless file =~ /(\d+)(?:_\d{4}_\d{2}_\d{2})?_gitlab_backup\.tar/ + + timestamp = $1.to_i + + if Time.at(timestamp) < (Time.now - keep_time) + begin + FileUtils.rm(file) removed += 1 + rescue => e + $progress.puts "Deleting #{file} failed: #{e.message}".color(:red) end end end diff --git a/spec/lib/gitlab/backup/manager_spec.rb b/spec/lib/gitlab/backup/manager_spec.rb new file mode 100644 index 00000000000..1b749d1bd39 --- /dev/null +++ b/spec/lib/gitlab/backup/manager_spec.rb @@ -0,0 +1,127 @@ +require 'spec_helper' + +describe Backup::Manager, lib: true do + describe '#remove_old' do + let(:progress) { StringIO.new } + + let(:files) do + [ + '1451606400_2016_01_01_gitlab_backup.tar', + '1451520000_2015_12_31_gitlab_backup.tar', + '1450742400_2015_12_22_gitlab_backup.tar', + '1449878400_gitlab_backup.tar', + '1449014400_gitlab_backup.tar', + 'manual_gitlab_backup.tar' + ] + end + + before do + allow(Dir).to receive(:chdir).and_yield + allow(Dir).to receive(:glob).and_return(files) + allow(FileUtils).to receive(:rm) + allow(Time).to receive(:now).and_return(Time.utc(2016)) + + allow(progress).to receive(:puts) + allow(progress).to receive(:print) + + allow_any_instance_of(String).to receive(:color) do |string, _color| + string + end + + @old_progress = $progress # rubocop:disable Style/GlobalVars + $progress = progress # rubocop:disable Style/GlobalVars + end + + after do + $progress = @old_progress # rubocop:disable Style/GlobalVars + end + + context 'when keep_time is zero' do + before do + allow(Gitlab.config.backup).to receive(:keep_time).and_return(0) + + subject.remove_old + end + + it 'removes no files' do + expect(FileUtils).not_to have_received(:rm) + end + + it 'prints a skipped message' do + expect(progress).to have_received(:puts).with('skipping') + end + end + + context 'when there are no files older than keep_time' do + before do + allow(Gitlab.config.backup).to receive(:keep_time).and_return(2592000) + + subject.remove_old + end + + it 'removes no files' do + expect(FileUtils).not_to have_received(:rm) + end + + it 'prints a done message' do + expect(progress).to have_received(:puts).with('done. (0 removed)') + end + end + + context 'when keep_time is set to remove files' do + before do + allow(Gitlab.config.backup).to receive(:keep_time).and_return(1) + + subject.remove_old + end + + it 'removes matching files with a human-readable timestamp' do + expect(FileUtils).to have_received(:rm).with(files[1]) + expect(FileUtils).to have_received(:rm).with(files[2]) + end + + it 'removes matching files without a human-readable timestamp' do + expect(FileUtils).to have_received(:rm).with(files[3]) + expect(FileUtils).to have_received(:rm).with(files[4]) + end + + it 'does not remove files that are not old enough' do + expect(FileUtils).not_to have_received(:rm).with(files[0]) + end + + it 'does not remove non-matching files' do + expect(FileUtils).not_to have_received(:rm).with(files[5]) + end + + it 'prints a done message' do + expect(progress).to have_received(:puts).with('done. (4 removed)') + end + end + + context 'when removing a file fails' do + let(:file) { files[1] } + let(:message) { "Permission denied @ unlink_internal - #{file}" } + + before do + allow(Gitlab.config.backup).to receive(:keep_time).and_return(1) + allow(FileUtils).to receive(:rm).with(file).and_raise(Errno::EACCES, message) + + subject.remove_old + end + + it 'removes the remaining expected files' do + expect(FileUtils).to have_received(:rm).with(files[2]) + expect(FileUtils).to have_received(:rm).with(files[3]) + expect(FileUtils).to have_received(:rm).with(files[4]) + end + + it 'sets the correct removed count' do + expect(progress).to have_received(:puts).with('done. (3 removed)') + end + + it 'prints the error from file that could not be removed' do + expect(progress).to have_received(:puts).with(a_string_matching(message)) + end + end + end +end -- cgit v1.2.1 From 98c0eb0f75692b1281adda9bfb75e1fcc12cec6d Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Wed, 7 Dec 2016 15:54:32 +0200 Subject: BitBucket refactoring. Iteration 3 --- lib/bitbucket/client.rb | 26 ++++++++++---------------- lib/bitbucket/collection.rb | 2 +- lib/bitbucket/paginator.rb | 2 +- lib/gitlab/bitbucket_import/importer.rb | 6 +++--- 4 files changed, 15 insertions(+), 21 deletions(-) diff --git a/lib/bitbucket/client.rb b/lib/bitbucket/client.rb index 3457c2c6454..e23da4556aa 100644 --- a/lib/bitbucket/client.rb +++ b/lib/bitbucket/client.rb @@ -6,35 +6,26 @@ module Bitbucket def issues(repo) path = "/repositories/#{repo}/issues" - paginator = Paginator.new(connection, path, :issue) - - Collection.new(paginator) + get_collection(path, :issue) end def issue_comments(repo, issue_id) path = "/repositories/#{repo}/issues/#{issue_id}/comments" - paginator = Paginator.new(connection, path, :comment) - - Collection.new(paginator) + get_collection(path, :comment) end def pull_requests(repo) path = "/repositories/#{repo}/pullrequests?state=ALL" - paginator = Paginator.new(connection, path, :pull_request) - - Collection.new(paginator) + get_collection(path, :pull_request) end def pull_request_comments(repo, pull_request) path = "/repositories/#{repo}/pullrequests/#{pull_request}/comments" - paginator = Paginator.new(connection, path, :pull_request_comment) - - Collection.new(paginator) + get_collection(path, :pull_request_comment) end def pull_request_diff(repo, pull_request) path = "/repositories/#{repo}/pullrequests/#{pull_request}/diff" - connection.get(path) end @@ -45,9 +36,7 @@ module Bitbucket def repos path = "/repositories/#{user.username}" - paginator = Paginator.new(connection, path, :repo) - - Collection.new(paginator) + get_collection(path, :repo) end def user @@ -60,5 +49,10 @@ module Bitbucket private attr_reader :connection + + def get_collection(path, type) + paginator = Paginator.new(connection, path, type) + Collection.new(paginator) + end end end diff --git a/lib/bitbucket/collection.rb b/lib/bitbucket/collection.rb index 9cc8947417c..3a9379ff680 100644 --- a/lib/bitbucket/collection.rb +++ b/lib/bitbucket/collection.rb @@ -3,7 +3,7 @@ module Bitbucket def initialize(paginator) super() do |yielder| loop do - paginator.next.each { |item| yielder << item } + paginator.items.each { |item| yielder << item } end end diff --git a/lib/bitbucket/paginator.rb b/lib/bitbucket/paginator.rb index 37f12328447..641a6ed79d6 100644 --- a/lib/bitbucket/paginator.rb +++ b/lib/bitbucket/paginator.rb @@ -11,7 +11,7 @@ module Bitbucket connection.set_default_query_parameters(pagelen: PAGE_LENGTH, sort: :created_on) end - def next + def items raise StopIteration unless has_next_page? @page = fetch_next_page diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 825d43e6589..fba382e6fea 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -50,7 +50,7 @@ module Gitlab if issue.persisted? client.issue_comments(repo, issue.iid).each do |comment| - # The note can be blank for issue service messages like "Chenged title: ..." + # The note can be blank for issue service messages like "Changed title: ..." # We would like to import those comments as well but there is no any # specific parameter that would allow to process them, it's just an empty comment. # To prevent our importer from just crashing or from creating useless empty comments @@ -70,8 +70,8 @@ module Gitlab end end end - rescue ActiveRecord::RecordInvalid - nil + rescue ActiveRecord::RecordInvalid => e + Rails.logger.error("Bitbucket importer ERROR in #{project.path_with_namespace}: Couldn't import record properly #{e.message}") end def import_pull_requests -- cgit v1.2.1 From bd3bd9bcea11244c56a0f7b63a6afa6fe439bf01 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Wed, 7 Dec 2016 16:16:18 +0200 Subject: Remove outdated bitbucket_import.rb --- lib/gitlab/bitbucket_import.rb | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 lib/gitlab/bitbucket_import.rb diff --git a/lib/gitlab/bitbucket_import.rb b/lib/gitlab/bitbucket_import.rb deleted file mode 100644 index 7298152e7e9..00000000000 --- a/lib/gitlab/bitbucket_import.rb +++ /dev/null @@ -1,6 +0,0 @@ -module Gitlab - module BitbucketImport - mattr_accessor :public_key - @public_key = nil - end -end -- cgit v1.2.1 From 8b379465a5be48c8062379a3dea8e58110c52d87 Mon Sep 17 00:00:00 2001 From: tiagonbotelho <tiagonbotelho@hotmail.com> Date: Mon, 21 Nov 2016 11:42:10 +0000 Subject: Reenables /user API request to return private-token if user is admin and requested with sudo --- lib/api/entities.rb | 1 + lib/api/users.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 899d68bc6c7..1dd191161ef 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -32,6 +32,7 @@ module API expose :can_create_project?, as: :can_create_project expose :two_factor_enabled?, as: :two_factor_enabled expose :external + expose :private_token, if: lambda { |user, options| user.is_admin? && options[:sudo_identifier] } end class UserLogin < UserFull diff --git a/lib/api/users.rb b/lib/api/users.rb index bc2362aa72e..b3870e0c7c8 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -353,7 +353,7 @@ module API success Entities::UserFull end get do - present current_user, with: Entities::UserFull + present current_user, with: Entities::UserFull, sudo_identifier: sudo_identifier end desc "Get the currently authenticated user's SSH keys" do -- cgit v1.2.1 From 3ed96afc47c481db4f8c0a6581602abaee920808 Mon Sep 17 00:00:00 2001 From: tiagonbotelho <tiagonbotelho@hotmail.com> Date: Mon, 21 Nov 2016 12:59:37 +0000 Subject: adds impersonator variable and makes sudo usage overall more clear --- .../24537-reenable-private-token-with-sudo.yml | 5 + doc/api/users.md | 51 +++++- lib/api/entities.rb | 7 +- lib/api/helpers.rb | 13 +- lib/api/session.rb | 4 +- lib/api/users.rb | 16 +- spec/fixtures/api/schemas/user/login.json | 37 ++++ spec/fixtures/api/schemas/user/public.json | 79 ++++++++ spec/requests/api/api_helpers_spec.rb | 199 +++++++++++++-------- spec/requests/api/users_spec.rb | 79 ++++++-- 10 files changed, 390 insertions(+), 100 deletions(-) create mode 100644 changelogs/unreleased/24537-reenable-private-token-with-sudo.yml create mode 100644 spec/fixtures/api/schemas/user/login.json create mode 100644 spec/fixtures/api/schemas/user/public.json diff --git a/changelogs/unreleased/24537-reenable-private-token-with-sudo.yml b/changelogs/unreleased/24537-reenable-private-token-with-sudo.yml new file mode 100644 index 00000000000..9fbbaeb914d --- /dev/null +++ b/changelogs/unreleased/24537-reenable-private-token-with-sudo.yml @@ -0,0 +1,5 @@ +--- +title: Reenables /user API request to return private-token if user is admin and request + is made with sudo +merge_request: 7615 +author: diff --git a/doc/api/users.md b/doc/api/users.md index 52a6b691610..28b6c7bd491 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -291,7 +291,9 @@ Parameters: - `id` (required) - The ID of the user -## Current user +## User + +### For normal users Gets currently authenticated user. @@ -335,6 +337,53 @@ GET /user } ``` +### For admins + +Parameters: + +- `sudo` (required) - the ID of a user + +``` +GET /user +``` + +```json +{ + "id": 1, + "username": "john_smith", + "email": "john@example.com", + "name": "John Smith", + "state": "active", + "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg", + "web_url": "http://localhost:3000/john_smith", + "created_at": "2012-05-23T08:00:58Z", + "is_admin": false, + "bio": null, + "location": null, + "skype": "", + "linkedin": "", + "twitter": "", + "website_url": "", + "organization": "", + "last_sign_in_at": "2012-06-01T11:41:01Z", + "confirmed_at": "2012-05-23T09:05:22Z", + "theme_id": 1, + "color_scheme_id": 2, + "projects_limit": 100, + "current_sign_in_at": "2012-06-02T06:36:55Z", + "identities": [ + {"provider": "github", "extern_uid": "2435223452345"}, + {"provider": "bitbucket", "extern_uid": "john_smith"}, + {"provider": "google_oauth2", "extern_uid": "8776128412476123468721346"} + ], + "can_create_group": true, + "can_create_project": true, + "two_factor_enabled": true, + "external": false, + "private_token": "dd34asd13as" +} +``` + ## List SSH keys Get a list of currently authenticated user's SSH keys. diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 1dd191161ef..006d5f9f44e 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -22,7 +22,7 @@ module API expose :provider, :extern_uid end - class UserFull < User + class UserPublic < User expose :last_sign_in_at expose :confirmed_at expose :email @@ -32,10 +32,9 @@ module API expose :can_create_project?, as: :can_create_project expose :two_factor_enabled?, as: :two_factor_enabled expose :external - expose :private_token, if: lambda { |user, options| user.is_admin? && options[:sudo_identifier] } end - class UserLogin < UserFull + class UserWithPrivateToken < UserPublic expose :private_token end @@ -290,7 +289,7 @@ module API end class SSHKeyWithUser < SSHKey - expose :user, using: Entities::UserFull + expose :user, using: Entities::UserPublic end class Note < Grape::Entity diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 7f94ede7940..6a47efc74f0 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -44,11 +44,14 @@ module API return nil end - identifier = sudo_identifier() + identifier = sudo_identifier - # If the sudo is the current user do nothing - if identifier && !(@current_user.id == identifier || @current_user.username == identifier) + if identifier + # We check for private_token because we cannot allow PAT to be used forbidden!('Must be admin to use sudo') unless @current_user.is_admin? + forbidden!('Private token must be specified in order to use sudo') unless private_token_used? + + @impersonator = @current_user @current_user = User.by_username_or_id(identifier) not_found!("No user id or username for: #{identifier}") if @current_user.nil? end @@ -399,6 +402,10 @@ module API links.join(', ') end + def private_token_used? + private_token == @current_user.private_token + end + def secret_token Gitlab::Shell.secret_token end diff --git a/lib/api/session.rb b/lib/api/session.rb index d09400b81f5..002ffd1d154 100644 --- a/lib/api/session.rb +++ b/lib/api/session.rb @@ -1,7 +1,7 @@ module API class Session < Grape::API desc 'Login to get token' do - success Entities::UserLogin + success Entities::UserWithPrivateToken end params do optional :login, type: String, desc: 'The username' @@ -14,7 +14,7 @@ module API return unauthorized! unless user return render_api_error!('401 Unauthorized. You have 2FA enabled. Please use a personal access token to access the API', 401) if user.two_factor_enabled? - present user, with: Entities::UserLogin + present user, with: Entities::UserWithPrivateToken end end end diff --git a/lib/api/users.rb b/lib/api/users.rb index b3870e0c7c8..1dab799dd61 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -51,7 +51,7 @@ module API users = users.external if params[:external] && current_user.is_admin? end - entity = current_user.is_admin? ? Entities::UserFull : Entities::UserBasic + entity = current_user.is_admin? ? Entities::UserPublic : Entities::UserBasic present paginate(users), with: entity end @@ -66,7 +66,7 @@ module API not_found!('User') unless user if current_user && current_user.is_admin? - present user, with: Entities::UserFull + present user, with: Entities::UserPublic elsif can?(current_user, :read_user, user) present user, with: Entities::User else @@ -75,7 +75,7 @@ module API end desc 'Create a user. Available only for admins.' do - success Entities::UserFull + success Entities::UserPublic end params do requires :email, type: String, desc: 'The email of the user' @@ -99,7 +99,7 @@ module API end if user.save - present user, with: Entities::UserFull + present user, with: Entities::UserPublic else conflict!('Email has already been taken') if User. where(email: user.email). @@ -114,7 +114,7 @@ module API end desc 'Update a user. Available only for admins.' do - success Entities::UserFull + success Entities::UserPublic end params do requires :id, type: Integer, desc: 'The ID of the user' @@ -161,7 +161,7 @@ module API user_params.delete(:provider) if user.update_attributes(user_params) - present user, with: Entities::UserFull + present user, with: Entities::UserPublic else render_validation_error!(user) end @@ -350,10 +350,10 @@ module API resource :user do desc 'Get the currently authenticated user' do - success Entities::UserFull + success Entities::UserPublic end get do - present current_user, with: Entities::UserFull, sudo_identifier: sudo_identifier + present current_user, with: @impersonator ? Entities::UserWithPrivateToken : Entities::UserPublic end desc "Get the currently authenticated user's SSH keys" do diff --git a/spec/fixtures/api/schemas/user/login.json b/spec/fixtures/api/schemas/user/login.json new file mode 100644 index 00000000000..e6c1d9c9d84 --- /dev/null +++ b/spec/fixtures/api/schemas/user/login.json @@ -0,0 +1,37 @@ +{ + "type": "object", + "required": [ + "id", + "username", + "email", + "name", + "state", + "avatar_url", + "web_url", + "created_at", + "is_admin", + "bio", + "location", + "skype", + "linkedin", + "twitter", + "website_url", + "organization", + "last_sign_in_at", + "confirmed_at", + "theme_id", + "color_scheme_id", + "projects_limit", + "current_sign_in_at", + "identities", + "can_create_group", + "can_create_project", + "two_factor_enabled", + "external", + "private_token" + ], + "properties": { + "$ref": "full.json", + "private_token": { "type": "string" } + } +} diff --git a/spec/fixtures/api/schemas/user/public.json b/spec/fixtures/api/schemas/user/public.json new file mode 100644 index 00000000000..dbd5d32e89c --- /dev/null +++ b/spec/fixtures/api/schemas/user/public.json @@ -0,0 +1,79 @@ +{ + "type": "object", + "required": [ + "id", + "username", + "email", + "name", + "state", + "avatar_url", + "web_url", + "created_at", + "is_admin", + "bio", + "location", + "skype", + "linkedin", + "twitter", + "website_url", + "organization", + "last_sign_in_at", + "confirmed_at", + "theme_id", + "color_scheme_id", + "projects_limit", + "current_sign_in_at", + "identities", + "can_create_group", + "can_create_project", + "two_factor_enabled", + "external" + ], + "properties": { + "id": { "type": "integer" }, + "username": { "type": "string" }, + "email": { + "type": "string", + "pattern": "^[^@]+@[^@]+$" + }, + "name": { "type": "string" }, + "state": { + "type": "string", + "enum": ["active", "blocked"] + }, + "avatar_url": { "type": "string" }, + "web_url": { "type": "string" }, + "created_at": { "type": "date" }, + "is_admin": { "type": "boolean" }, + "bio": { "type": ["string", "null"] }, + "location": { "type": ["string", "null"] }, + "skype": { "type": "string" }, + "linkedin": { "type": "string" }, + "twitter": { "type": "string "}, + "website_url": { "type": "string" }, + "organization": { "type": ["string", "null"] }, + "last_sign_in_at": { "type": "date" }, + "confirmed_at": { "type": ["date", "null"] }, + "theme_id": { "type": "integer" }, + "color_scheme_id": { "type": "integer" }, + "projects_limit": { "type": "integer" }, + "current_sign_in_at": { "type": "date" }, + "identities": { + "type": "array", + "items": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": ["github", "bitbucket", "google_oauth2"] + }, + "extern_uid": { "type": ["number", "string"] } + } + } + }, + "can_create_group": { "type": "boolean" }, + "can_create_project": { "type": "boolean" }, + "two_factor_enabled": { "type": "boolean" }, + "external": { "type": "boolean" } + } +} diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb index 36517ad0f8c..3f34309f419 100644 --- a/spec/requests/api/api_helpers_spec.rb +++ b/spec/requests/api/api_helpers_spec.rb @@ -153,85 +153,144 @@ describe API::Helpers, api: true do end end - it "changes current user to sudo when admin" do - set_env(admin, user.id) - expect(current_user).to eq(user) - set_param(admin, user.id) - expect(current_user).to eq(user) - set_env(admin, user.username) - expect(current_user).to eq(user) - set_param(admin, user.username) - expect(current_user).to eq(user) - end + context 'sudo usage' do + context 'with admin' do + context 'with header' do + context 'with id' do + it 'changes current_user to sudo' do + set_env(admin, user.id) - it "throws an error when the current user is not an admin and attempting to sudo" do - set_env(user, admin.id) - expect { current_user }.to raise_error(Exception) - set_param(user, admin.id) - expect { current_user }.to raise_error(Exception) - set_env(user, admin.username) - expect { current_user }.to raise_error(Exception) - set_param(user, admin.username) - expect { current_user }.to raise_error(Exception) - end + expect(current_user).to eq(user) + end - it "throws an error when the user cannot be found for a given id" do - id = user.id + admin.id - expect(user.id).not_to eq(id) - expect(admin.id).not_to eq(id) - set_env(admin, id) - expect { current_user }.to raise_error(Exception) + it 'handles sudo to oneself' do + set_env(admin, admin.id) - set_param(admin, id) - expect { current_user }.to raise_error(Exception) - end + expect(current_user).to eq(admin) + end - it "throws an error when the user cannot be found for a given username" do - username = "#{user.username}#{admin.username}" - expect(user.username).not_to eq(username) - expect(admin.username).not_to eq(username) - set_env(admin, username) - expect { current_user }.to raise_error(Exception) + it 'throws an error when user cannot be found' do + id = user.id + admin.id + expect(user.id).not_to eq(id) + expect(admin.id).not_to eq(id) - set_param(admin, username) - expect { current_user }.to raise_error(Exception) - end + set_env(admin, id) - it "handles sudo's to oneself" do - set_env(admin, admin.id) - expect(current_user).to eq(admin) - set_param(admin, admin.id) - expect(current_user).to eq(admin) - set_env(admin, admin.username) - expect(current_user).to eq(admin) - set_param(admin, admin.username) - expect(current_user).to eq(admin) - end + expect { current_user }.to raise_error(Exception) + end + end - it "handles multiple sudo's to oneself" do - set_env(admin, user.id) - expect(current_user).to eq(user) - expect(current_user).to eq(user) - set_env(admin, user.username) - expect(current_user).to eq(user) - expect(current_user).to eq(user) - - set_param(admin, user.id) - expect(current_user).to eq(user) - expect(current_user).to eq(user) - set_param(admin, user.username) - expect(current_user).to eq(user) - expect(current_user).to eq(user) - end + context 'with username' do + it 'changes current_user to sudo' do + set_env(admin, user.username) + + expect(current_user).to eq(user) + end + + it 'handles sudo to oneself' do + set_env(admin, admin.username) + + expect(current_user).to eq(admin) + end + + it "throws an error when the user cannot be found for a given username" do + username = "#{user.username}#{admin.username}" + expect(user.username).not_to eq(username) + expect(admin.username).not_to eq(username) + + set_env(admin, username) + + expect { current_user }.to raise_error(Exception) + end + end + end + + context 'with param' do + context 'with id' do + it 'changes current_user to sudo' do + set_param(admin, user.id) + + expect(current_user).to eq(user) + end + + it 'handles sudo to oneself' do + set_param(admin, admin.id) + + expect(current_user).to eq(admin) + end + + it 'handles sudo to oneself using string' do + set_env(admin, user.id.to_s) + + expect(current_user).to eq(user) + end + + it 'throws an error when user cannot be found' do + id = user.id + admin.id + expect(user.id).not_to eq(id) + expect(admin.id).not_to eq(id) - it "handles multiple sudo's to oneself using string ids" do - set_env(admin, user.id.to_s) - expect(current_user).to eq(user) - expect(current_user).to eq(user) + set_param(admin, id) - set_param(admin, user.id.to_s) - expect(current_user).to eq(user) - expect(current_user).to eq(user) + expect { current_user }.to raise_error(Exception) + end + end + + context 'with username' do + it 'changes current_user to sudo' do + set_param(admin, user.username) + + expect(current_user).to eq(user) + end + + it 'handles sudo to oneself' do + set_param(admin, admin.username) + + expect(current_user).to eq(admin) + end + + it "throws an error when the user cannot be found for a given username" do + username = "#{user.username}#{admin.username}" + expect(user.username).not_to eq(username) + expect(admin.username).not_to eq(username) + + set_param(admin, username) + + expect { current_user }.to raise_error(Exception) + end + end + end + end + + context 'with regular user' do + context 'with env' do + it 'changes current_user to sudo when admin and user id' do + set_env(user, admin.id) + + expect { current_user }.to raise_error(Exception) + end + + it 'changes current_user to sudo when admin and user username' do + set_env(user, admin.username) + + expect { current_user }.to raise_error(Exception) + end + end + + context 'with params' do + it 'changes current_user to sudo when admin and user id' do + set_param(user, admin.id) + + expect { current_user }.to raise_error(Exception) + end + + it 'changes current_user to sudo when admin and user username' do + set_param(user, admin.username) + + expect { current_user }.to raise_error(Exception) + end + end + end end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index f82f52e7399..c37dbfa0a33 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -651,20 +651,75 @@ describe API::Users, api: true do end describe "GET /user" do - it "returns current user" do - get api("/user", user) - expect(response).to have_http_status(200) - expect(json_response['email']).to eq(user.email) - expect(json_response['is_admin']).to eq(user.is_admin?) - expect(json_response['can_create_project']).to eq(user.can_create_project?) - expect(json_response['can_create_group']).to eq(user.can_create_group?) - expect(json_response['projects_limit']).to eq(user.projects_limit) - expect(json_response['private_token']).to be_blank + let(:personal_access_token) { create(:personal_access_token, user: user) } + let(:private_token) { user.private_token } + + context 'with regular user' do + context 'with personal access token' do + it 'returns 403 without private token when sudo is defined' do + get api("/user?private_token=#{personal_access_token.token}&sudo=#{user.id}") + + expect(response).to have_http_status(403) + end + end + + context 'with private token' do + it 'returns 403 without private token when sudo defined' do + get api("/user?private_token=#{private_token}&sudo=#{user.id}") + + expect(response).to have_http_status(403) + end + end + + it 'returns current user without private token when sudo not defined' do + get api("/user", user) + + expect(response).to have_http_status(200) + expect(response).to match_response_schema('user/public') + end end - it "returns 401 error if user is unauthenticated" do - get api("/user") - expect(response).to have_http_status(401) + context 'with admin' do + let(:user) { create(:admin) } + + context 'with personal access token' do + it 'returns 403 without private token when sudo defined' do + get api("/user?private_token=#{personal_access_token.token}&sudo=#{user.id}") + + expect(response).to have_http_status(403) + end + + it 'returns current user without private token when sudo not defined' do + get api("/user?private_token=#{personal_access_token.token}") + + expect(response).to have_http_status(200) + expect(response).to match_response_schema('user/public') + end + end + + context 'with private token' do + it 'returns current user with private token when sudo defined' do + get api("/user?private_token=#{private_token}&sudo=#{user.id}") + + expect(response).to have_http_status(200) + expect(response).to match_response_schema('user/login') + end + + it 'returns current user without private token when sudo not defined' do + get api("/user?private_token=#{private_token}") + + expect(response).to have_http_status(200) + expect(response).to match_response_schema('user/public') + end + end + end + + context 'with unauthenticated user' do + it "returns 401 error if user is unauthenticated" do + get api("/user") + + expect(response).to have_http_status(401) + end end end -- cgit v1.2.1 From 6ab74b1cb3f6982c52c7124a6e9e451c83d33645 Mon Sep 17 00:00:00 2001 From: jnoortheen <jnoortheen@gmail.com> Date: Tue, 29 Nov 2016 22:01:01 +0530 Subject: fix: 24982- Remove'Signed in successfully' message After this change the sign-in-success flash message will not be shown refactor: set flash message to be nil while signing in test: changed tests to reflect removal of sign-in message refactor: adding signed_in message back See Merge Request !7837 issue#24982 --- app/controllers/sessions_controller.rb | 2 ++ .../unreleased/24982-ux-improvement-sign-in-success-message.yml | 5 +++++ spec/controllers/sessions_controller_spec.rb | 1 - spec/features/u2f_spec.rb | 9 ++++----- 4 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 5d7ecfeacf4..38e7c6f4a48 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -31,6 +31,8 @@ class SessionsController < Devise::SessionsController resource.update_attributes(reset_password_token: nil, reset_password_sent_at: nil) end + # hide the signed-in notification + flash[:notice] = nil log_audit_event(current_user, with: authentication_method) end end diff --git a/changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml b/changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml new file mode 100644 index 00000000000..12ea08e3815 --- /dev/null +++ b/changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml @@ -0,0 +1,5 @@ +--- +title: 'fix: 24982- Remove''Signed in successfully'' message After this change the + sign-in-success flash message will not be shown' +merge_request: 7837 +author: jnoortheen diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 48d69377461..b56c7880b64 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -22,7 +22,6 @@ describe SessionsController do it 'authenticates user correctly' do post(:create, user: { login: user.username, password: user.password }) - expect(response).to set_flash.to /Signed in successfully/ expect(subject.current_user). to eq user end diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb index b750f27ea72..be21b403084 100644 --- a/spec/features/u2f_spec.rb +++ b/spec/features/u2f_spec.rb @@ -163,8 +163,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: click_on "Sign in via U2F device" expect(page.body).to match('We heard back from your U2F device') click_on "Authenticate via U2F Device" - - expect(page.body).to match('Signed in successfully') + expect(page.body).to match('href="/users/sign_out"') end end @@ -178,7 +177,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: expect(page.body).to match('We heard back from your U2F device') click_on "Authenticate via U2F Device" - expect(page.body).to match('Signed in successfully') + expect(page.body).to match('href="/users/sign_out"') end end @@ -234,7 +233,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: expect(page.body).to match('We heard back from your U2F device') click_on "Authenticate via U2F Device" - expect(page.body).to match('Signed in successfully') + expect(page.body).to match('href="/users/sign_out"') end end end @@ -275,7 +274,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: expect(page.body).to match('We heard back from your U2F device') click_on "Authenticate via U2F Device" - expect(page.body).to match('Signed in successfully') + expect(page.body).to match('href="/users/sign_out"') logout end -- cgit v1.2.1 From 19fb84e3a897f5b251fb3f437fe365c6ec342c34 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 7 Dec 2016 15:27:14 +0000 Subject: Updated members dropdowns This ports some code over from EE to reduce conflicts --- app/assets/javascripts/gl_dropdown.js | 5 ++ app/assets/javascripts/members.js.es6 | 71 +++++++++++++++++++---- app/assets/stylesheets/framework/dropdowns.scss | 5 ++ app/assets/stylesheets/pages/members.scss | 4 ++ app/views/groups/group_members/update.js.haml | 1 + app/views/projects/project_members/update.js.haml | 1 + app/views/shared/members/_member.html.haml | 20 ++++++- changelogs/unreleased/members-dropdowns.yml | 4 ++ features/steps/group/members.rb | 7 ++- features/steps/project/team_management.rb | 6 +- 10 files changed, 109 insertions(+), 15 deletions(-) create mode 100644 changelogs/unreleased/members-dropdowns.yml diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 969778dded7..9a91018a8e4 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -650,6 +650,11 @@ } else if(value) { field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']"); } + + if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) { + return; + } + if (el.hasClass(ACTIVE_CLASS)) { el.removeClass(ACTIVE_CLASS); if (field && field.length) { diff --git a/app/assets/javascripts/members.js.es6 b/app/assets/javascripts/members.js.es6 index 895bc10784f..64826894965 100644 --- a/app/assets/javascripts/members.js.es6 +++ b/app/assets/javascripts/members.js.es6 @@ -1,38 +1,87 @@ -/* eslint-disable */ -((w) => { - w.gl = w.gl || {}; +/* eslint-disable class-methods-use-this */ +(() => { + window.gl = window.gl || {}; class Members { constructor() { this.addListeners(); + this.initGLDropdown(); } addListeners() { $('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow); - $('.js-member-update-control').off('change').on('change', this.formSubmit); - $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess); + $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this)); + $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this)); gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change'); } + initGLDropdown() { + $('.js-member-permissions-dropdown').each((i, btn) => { + const $btn = $(btn); + + $btn.glDropdown({ + selectable: true, + isSelectable(selected, $el) { + return !$el.hasClass('is-active'); + }, + fieldName: $btn.data('field-name'), + id(selected, $el) { + return $el.data('id'); + }, + toggleLabel(selected, $el) { + return $el.text(); + }, + clicked: (selected, $el) => { + const $link = $($el); + const { $toggle, $dateInput } = this.getMemberListItems($link); + + $toggle.attr('disabled', true); + $dateInput.attr('disabled', true); + + $btn.closest('form').trigger('submit.rails'); + }, + }); + }); + } + removeRow(e) { const $target = $(e.target); if ($target.hasClass('btn-remove')) { $target.closest('.member') - .fadeOut(function () { + .fadeOut(function fadeOutMemberRow() { $(this).remove(); }); } } - formSubmit() { - $(this).closest('form').trigger("submit.rails").end().disable(); + formSubmit(e) { + const $this = $(e.currentTarget); + const { $toggle, $dateInput } = this.getMemberListItems($this); + + $this.closest('form').trigger('submit.rails'); + + $toggle.attr('disabled', true); + $dateInput.attr('disabled', true); } - formSuccess() { - $(this).find('.js-member-update-control').enable(); + formSuccess(e) { + const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member')); + + $toggle.removeAttr('disabled'); + $dateInput.removeAttr('disabled'); + } + + getMemberListItems($el) { + const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`); + + return { + $memberListItem, + $toggle: $memberListItem.find('.dropdown-menu-toggle'), + $dateInput: $memberListItem.find('.js-access-expiration-date'), + }; } } gl.Members = Members; -})(window); +})(); diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 33de652c06f..d5914b900e2 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -42,6 +42,11 @@ border-radius: $border-radius-base; white-space: nowrap; + &[disabled] { + background-color: $input-bg-disabled; + cursor: not-allowed; + } + &.no-outline { outline: 0; } diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index 756efa9c7fa..5f3bbb40ba0 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -54,6 +54,10 @@ @media (min-width: $screen-sm-min) { width: 50%; } + + .dropdown-menu-toggle { + width: 100%; + } } .member-access-text { diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml index de8f53b6b52..9d05bff6c4e 100644 --- a/app/views/groups/group_members/update.js.haml +++ b/app/views/groups/group_members/update.js.haml @@ -1,3 +1,4 @@ :plain var $listItem = $('#{escape_javascript(render('shared/members/member', member: @group_member))}'); $("##{dom_id(@group_member)} .list-item-name").replaceWith($listItem.find('.list-item-name')); + gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@group_member)}")); diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml index 91927181efb..d15f4310ff5 100644 --- a/app/views/projects/project_members/update.js.haml +++ b/app/views/projects/project_members/update.js.haml @@ -1,3 +1,4 @@ :plain var $listItem = $('#{escape_javascript(render('shared/members/member', member: @project_member))}'); $("##{dom_id(@project_member)} .list-item-name").replaceWith($listItem.find('.list-item-name')); + gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@project_member)}")); diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 432047a1c4e..ac3e4d9bac6 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -48,9 +48,25 @@ - if show_controls - if user != current_user = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| - = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{member.id}", disabled: !can_admin_member + = f.hidden_field :access_level + .member-form-control.dropdown.append-right-5 + %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button", + disabled: !can_admin_member, + data: { toggle: "dropdown", field_name: "#{f.object_name}[access_level]" } } + %span.dropdown-toggle-text + = member.human_access + = icon("chevron-down") + .dropdown-menu.dropdown-select.dropdown-menu-align-right.dropdown-menu-selectable + = dropdown_title("Change permissions") + .dropdown-content + %ul + - Gitlab::Access.options.each do |role, role_id| + %li + = link_to role, "javascript:void(0)", + class: ("is-active" if member.access_level == role_id), + data: { id: role_id, el_id: dom_id(member) } .prepend-left-5.clearable-input.member-form-control - = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member + = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member, data: { el_id: dom_id(member) } %i.clear-icon.js-clear-input - else %span.member-access-text= member.human_access diff --git a/changelogs/unreleased/members-dropdowns.yml b/changelogs/unreleased/members-dropdowns.yml new file mode 100644 index 00000000000..b15403d6d62 --- /dev/null +++ b/changelogs/unreleased/members-dropdowns.yml @@ -0,0 +1,4 @@ +--- +title: Updated members dropdowns +merge_request: +author: diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb index cefc55d07ab..adaf375453c 100644 --- a/features/steps/group/members.rb +++ b/features/steps/group/members.rb @@ -117,7 +117,12 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps member = mary_jane_member page.within "#group_member_#{member.id}" do - select 'Developer', from: "member_access_level_#{member.id}" + click_button member.human_access + + page.within '.dropdown-menu' do + click_link 'Developer' + end + wait_for_ajax end end diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb index b21d0849ad1..99b6397ba74 100644 --- a/features/steps/project/team_management.rb +++ b/features/steps/project/team_management.rb @@ -65,7 +65,11 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps user = User.find_by(name: 'Dmitriy') project_member = project.project_members.find_by(user_id: user.id) page.within "#project_member_#{project_member.id}" do - select "Reporter", from: "member_access_level_#{project_member.id}" + click_button project_member.human_access + + page.within '.dropdown-menu' do + click_link 'Reporter' + end end end -- cgit v1.2.1 From 4eb53036c7469d027d2a75aff9e70950557195d2 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 7 Dec 2016 15:27:22 +0000 Subject: Changes after review --- app/helpers/environment_helper.rb | 10 ++++------ app/views/projects/builds/show.html.haml | 2 +- spec/features/projects/builds_spec.rb | 31 ++++++++++++++----------------- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb index 96d6f64eb8e..ff8550439d0 100644 --- a/app/helpers/environment_helper.rb +++ b/app/helpers/environment_helper.rb @@ -14,14 +14,12 @@ module EnvironmentHelper end end - def deployment_link(deployment, text) + def deployment_link(deployment, text: nil) return unless deployment - if text - link_to text, [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable] - else - link_to "##{deployment.iid}", [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable] - end + link_label = text ? text : "##{deployment.iid}" + + link_to link_label, [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable] end def last_deployment_link_for_environment_build(project, build) diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 3ef46872199..cdeb81372ee 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -46,7 +46,7 @@ - else This build is creating a deployment to #{environment_link_for_build(@build.project, @build)} - if environment.try(:last_deployment) - and will overwrite the #{deployment_link(environment.last_deployment, 'latest deployment')} + and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')} .prepend-top-default - if @build.erased? diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb index ea99239d5fc..8c4d4320dc5 100644 --- a/spec/features/projects/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -229,39 +229,36 @@ feature 'Builds', :feature do end context 'when build starts environment' do + let(:environment) { create(:environment, project: project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + context 'build is successfull and has deployment' do - it 'shows a link for the build' do - environment = create(:environment, project: project) - pipeline = create(:ci_pipeline, project: project) - deployment = create(:deployment) - build1 = create(:ci_build, :success, environment: environment.name, deployments: [deployment], pipeline: pipeline) + let(:deployment) { create(:deployment) } + let(:build) { create(:ci_build, :success, environment: environment.name, deployments: [deployment], pipeline: pipeline) } - visit namespace_project_build_path(project.namespace, project, build1) + it 'shows a link for the build' do + visit namespace_project_build_path(project.namespace, project, build) expect(page).to have_link environment.name end end context 'build is complete and not successfull' do - it 'shows a link for the build' do - environment = create(:environment, project: project) - pipeline = create(:ci_pipeline, project: project) - build1 = create(:ci_build, :failed, environment: environment.name, pipeline: pipeline) + let(:build) { create(:ci_build, :failed, environment: environment.name, pipeline: pipeline) } - visit namespace_project_build_path(project.namespace, project, build1) + it 'shows a link for the build' do + visit namespace_project_build_path(project.namespace, project, build) expect(page).to have_link environment.name end end context 'build creates a new deployment' do - it 'shows a link to lastest deployment' do - environment = create(:environment, project: project) - create(:deployment, environment: environment, sha: project.commit.id) - pipeline = create(:ci_pipeline, project: project) - build1 = create(:ci_build, :success, environment: environment.name, pipeline: pipeline) + let!(:deployment) { create(:deployment, environment: environment, sha: project.commit.id) } + let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) } - visit namespace_project_build_path(project.namespace, project, build1) + it 'shows a link to lastest deployment' do + visit namespace_project_build_path(project.namespace, project, build) expect(page).to have_link('latest deployment') end -- cgit v1.2.1 From 00cd864237d6c7ec57ecb49d304ca8dfa9e41d31 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Wed, 7 Dec 2016 18:04:02 +0200 Subject: BitBucket importer: import issues with labels --- lib/bitbucket/representation/issue.rb | 4 ++++ lib/gitlab/bitbucket_import/importer.rb | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/lib/bitbucket/representation/issue.rb b/lib/bitbucket/representation/issue.rb index dc034c19750..6c8e9a4c244 100644 --- a/lib/bitbucket/representation/issue.rb +++ b/lib/bitbucket/representation/issue.rb @@ -7,6 +7,10 @@ module Bitbucket raw['id'] end + def kind + raw['kind'] + end + def author raw.dig('reporter', 'username') || 'Anonymous' end diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index fba382e6fea..8852f5b0f3f 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -1,12 +1,18 @@ module Gitlab module BitbucketImport class Importer + LABELS = [{ title: 'bug', color: '#FF0000'}, + { title: 'enhancement', color: '#428BCA'}, + { title: 'proposal', color: '#69D100'}, + { title: 'task', color: '#7F8C8D'}].freeze + attr_reader :project, :client def initialize(project) @project = project @client = Bitbucket::Client.new(project.import_data.credentials) @formatter = Gitlab::ImportFormatter.new + @labels = {} end def execute @@ -34,10 +40,14 @@ module Gitlab def import_issues return unless repo.issues_enabled? + create_labels + client.issues(repo).each do |issue| description = @formatter.author_line(issue.author) description += issue.description + label_name = issue.kind + issue = project.issues.create( iid: issue.iid, title: issue.title, @@ -48,6 +58,8 @@ module Gitlab updated_at: issue.updated_at ) + assign_label(issue, label_name) + if issue.persisted? client.issue_comments(repo, issue.iid).each do |comment| # The note can be blank for issue service messages like "Changed title: ..." @@ -74,6 +86,16 @@ module Gitlab Rails.logger.error("Bitbucket importer ERROR in #{project.path_with_namespace}: Couldn't import record properly #{e.message}") end + def create_labels + LABELS.each do |label| + @labels[label[:title]] = project.labels.create!(label) + end + end + + def assign_label(issue, label_name) + issue.labels << @labels[label_name] + end + def import_pull_requests pull_requests = client.pull_requests(repo) -- cgit v1.2.1 From faf8f421103194be9af09dd8d24c582437bb7d2f Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran <alfredo@gitlab.com> Date: Wed, 7 Dec 2016 12:58:23 -0500 Subject: Do nothing if file is undefined --- app/assets/javascripts/blob/template_selector.js.es6 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js.es6 index 5434a19bcec..0ff5c0fab05 100644 --- a/app/assets/javascripts/blob/template_selector.js.es6 +++ b/app/assets/javascripts/blob/template_selector.js.es6 @@ -70,6 +70,8 @@ // e.g. // Api.gitignoreText item.name, @requestFileSuccess.bind(@) requestFileSuccess(file, { skipFocus } = {}) { + if (!file) return; + const oldValue = this.editor.getValue(); let newValue = file.content; -- cgit v1.2.1 From 8842f55201ae4725b307336686f8ab168fa0effb Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Wed, 7 Dec 2016 19:30:14 +0100 Subject: Code review --- app/models/ci/stage.rb | 4 +++- app/views/projects/commit/_pipeline.html.haml | 5 ++--- app/views/projects/pipelines/_graph.html.haml | 4 ++-- app/views/projects/pipelines/_with_tabs.html.haml | 5 ++--- app/views/projects/stage/_graph.html.haml | 11 ++++++----- spec/lib/gitlab/ci/status/factory_spec.rb | 6 +----- spec/lib/gitlab/ci/status/pipeline/factory_spec.rb | 4 ++-- 7 files changed, 18 insertions(+), 21 deletions(-) diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index fe1c5c642e1..d2a37c0a827 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -9,7 +9,9 @@ module Ci delegate :project, to: :pipeline def initialize(pipeline, name:, status: nil) - @pipeline, @name, @status = pipeline, name, status + @pipeline = pipeline + @name = name + @status = status end def to_param diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index 4fc5e15592a..a677e859a15 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -25,7 +25,7 @@ = time_interval_in_words pipeline.duration .row-content-block.build-content.middle-block.pipeline-graph.hidden - = render "projects/pipelines/graph", subject: pipeline + = render "projects/pipelines/graph", subject: pipeline, as: :pipeline - if pipeline.yaml_errors.present? .bs-callout.bs-callout-danger @@ -50,5 +50,4 @@ - if pipeline.project.build_coverage_enabled? %th Coverage %th - - pipeline.stages.each do |stage| - = render "projects/stage/stage", subject: stage + = render partial: "projects/stage/stage", collection: pipeline.stages, as: :stage diff --git a/app/views/projects/pipelines/_graph.html.haml b/app/views/projects/pipelines/_graph.html.haml index 3bb6426c156..16297b93d31 100644 --- a/app/views/projects/pipelines/_graph.html.haml +++ b/app/views/projects/pipelines/_graph.html.haml @@ -1,4 +1,4 @@ +- pipeline = local_assigns.fetch(:pipeline) .pipeline-visualization %ul.stage-column-list - - subject.stages.each do |stage| - = render "projects/stage/graph", subject: stage + = render partial: "projects/stage/graph", collection: pipeline.stages, as: :stage diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 2ace9339af3..a58bcb38b18 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -13,7 +13,7 @@ .tab-content #js-tab-pipeline.tab-pane .build-content.middle-block.pipeline-graph - = render "projects/pipelines/graph", subject: pipeline + = render "projects/pipelines/graph", subject: pipeline, as: :pipeline #js-tab-builds.tab-pane - if pipeline.yaml_errors.present? @@ -39,5 +39,4 @@ - if pipeline.project.build_coverage_enabled? %th Coverage %th - - pipeline.stages.each do |stage| - = render "projects/stage/stage", subject: stage + = render partial: "projects/stage/stage", collection: pipeline.stages, as: :stage diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml index f1d11db58ab..d8c87fae5a1 100644 --- a/app/views/projects/stage/_graph.html.haml +++ b/app/views/projects/stage/_graph.html.haml @@ -1,12 +1,13 @@ +- stage = local_assigns.fetch(:stage) +- statuses = stage.statuses.latest +- status_groups = statuses.sort_by(&:name).group_by(&:group_name) %li.stage-column .stage-name - %a{ name: subject.name } - - if subject.name - = subject.name.titleize + %a{ name: stage.name } + - if stage.name + = stage.name.titleize .builds-container %ul - - statuses = subject.statuses.latest - - status_groups = statuses.sort_by(&:name).group_by(&:group_name) - status_groups.each do |group_name, grouped_statuses| - if grouped_statuses.one? - status = grouped_statuses.first diff --git a/spec/lib/gitlab/ci/status/factory_spec.rb b/spec/lib/gitlab/ci/status/factory_spec.rb index 8ebdbddb001..d5bd7f7102b 100644 --- a/spec/lib/gitlab/ci/status/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/factory_spec.rb @@ -1,15 +1,11 @@ require 'spec_helper' describe Gitlab::Ci::Status::Factory do - let(:object) { double(status: :created) } - subject do described_class.new(object) end - let(:status) do - subject.fabricate! - end + let(:status) { subject.fabricate! } context 'when object has a core status' do HasStatus::AVAILABLE_STATUSES.each do |core_status| diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb index 939ad2edf04..d6243940f2e 100644 --- a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb @@ -21,7 +21,7 @@ describe Gitlab::Ci::Status::Pipeline::Factory do Gitlab::Ci::Status.const_get(core_status.capitalize)) end - it 'extends core status with common stage methods' do + it 'extends core status with common pipeline methods' do expect(status).to have_details expect(status).not_to have_action expect(status.details_path) @@ -45,7 +45,7 @@ describe Gitlab::Ci::Status::Pipeline::Factory do .to be_a Gitlab::Ci::Status::Pipeline::SuccessWithWarnings end - it 'extends core status with common stage methods' do + it 'extends core status with common pipeline methods' do expect(status).to have_details end end -- cgit v1.2.1 From 6bb3293932a0ff249386f8c1f245ebc85f15df31 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Wed, 7 Dec 2016 19:32:13 +0100 Subject: Move .pipeline-graph to pipelines/graph --- app/views/projects/commit/_pipeline.html.haml | 2 +- app/views/projects/pipelines/_graph.html.haml | 2 +- app/views/projects/pipelines/_with_tabs.html.haml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index a677e859a15..6a45853e614 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -24,7 +24,7 @@ in = time_interval_in_words pipeline.duration - .row-content-block.build-content.middle-block.pipeline-graph.hidden + .row-content-block.build-content.middle-block.hidden = render "projects/pipelines/graph", subject: pipeline, as: :pipeline - if pipeline.yaml_errors.present? diff --git a/app/views/projects/pipelines/_graph.html.haml b/app/views/projects/pipelines/_graph.html.haml index 16297b93d31..0202833c0bf 100644 --- a/app/views/projects/pipelines/_graph.html.haml +++ b/app/views/projects/pipelines/_graph.html.haml @@ -1,4 +1,4 @@ - pipeline = local_assigns.fetch(:pipeline) -.pipeline-visualization +.pipeline-visualization.pipeline-graph %ul.stage-column-list = render partial: "projects/stage/graph", collection: pipeline.stages, as: :stage diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index a58bcb38b18..7ef498f890b 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -12,7 +12,7 @@ .tab-content #js-tab-pipeline.tab-pane - .build-content.middle-block.pipeline-graph + .build-content.middle-block = render "projects/pipelines/graph", subject: pipeline, as: :pipeline #js-tab-builds.tab-pane -- cgit v1.2.1 From ae1550bb5234aa5f6ce2611ca606ac49fc38c2d1 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Wed, 7 Dec 2016 12:37:41 -0600 Subject: Fade out should be white instead of gray --- app/assets/stylesheets/pages/notes.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 16b099c09eb..2e89d3f4e41 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -124,7 +124,7 @@ ul.notes { position: absolute; left: 0; bottom: 0; - background: linear-gradient(rgba($gray-light, 0.1) -100px, $white-light 100%); + background: linear-gradient(rgba($white-light, 0.1) -100px, $white-light 100%); } &.hide-shade { -- cgit v1.2.1 From df84444b30cddfc073f69b128f74a50ddbc5d87c Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Wed, 7 Dec 2016 22:17:58 +0100 Subject: Fix specs --- app/views/projects/ci/builds/_build.html.haml | 8 ++++---- app/views/projects/commit/_pipeline.html.haml | 2 +- app/views/projects/pipelines/_with_tabs.html.haml | 4 ++-- app/views/projects/stage/_stage.html.haml | 14 +++++++------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index e75547c815f..18b3b04154f 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -104,9 +104,9 @@ = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do = icon('remove', class: 'cred') - elsif allow_retry - - if build.retryable? - = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do - = icon('repeat') - - elsif build.playable? && !admin + - if build.playable? && !admin = link_to play_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do = custom_icon('icon_play') + - elsif build.retryable? + = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do + = icon('repeat') diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index 6a45853e614..c7b5c1124b3 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -25,7 +25,7 @@ = time_interval_in_words pipeline.duration .row-content-block.build-content.middle-block.hidden - = render "projects/pipelines/graph", subject: pipeline, as: :pipeline + = render "projects/pipelines/graph", pipeline: pipeline - if pipeline.yaml_errors.present? .bs-callout.bs-callout-danger diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 7ef498f890b..739e5930822 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -13,7 +13,7 @@ .tab-content #js-tab-pipeline.tab-pane .build-content.middle-block - = render "projects/pipelines/graph", subject: pipeline, as: :pipeline + = render "projects/pipelines/graph", pipeline: pipeline #js-tab-builds.tab-pane - if pipeline.yaml_errors.present? @@ -39,4 +39,4 @@ - if pipeline.project.build_coverage_enabled? %th Coverage %th - = render partial: "projects/stage/stage", collection: pipeline.stages, as: :stage + = render partial: "projects/stage/stage", collection: pipeline.stages, as: :stage diff --git a/app/views/projects/stage/_stage.html.haml b/app/views/projects/stage/_stage.html.haml index 055d8fca38b..1eca375db3d 100644 --- a/app/views/projects/stage/_stage.html.haml +++ b/app/views/projects/stage/_stage.html.haml @@ -1,14 +1,14 @@ %tr %th{colspan: 10} %strong - %a{ name: subject.name } - %span{class: "ci-status-link ci-status-icon-#{subject.status}"} - = ci_icon_for_status(subject.status) - - if subject.name + %a{ name: stage.name } + %span{class: "ci-status-link ci-status-icon-#{stage.status}"} + = ci_icon_for_status(stage.status) + - if stage.name   - = subject.name.titleize - = render subject.statuses.latest_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, allow_retry: true - = render subject.statuses.retried_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, retried: true + = stage.name.titleize += render stage.statuses.latest_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, allow_retry: true += render stage.statuses.retried_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, retried: true %tr %td{colspan: 10}   -- cgit v1.2.1 From 0a8289cafd4a996452c0e21322178793be8120be Mon Sep 17 00:00:00 2001 From: Rydkin Maxim <maks.rydkin@gmail.com> Date: Sun, 4 Dec 2016 00:45:48 +0300 Subject: add link_to_if helper on target_branch link on Merge Request show page for case of deleted target branch add spec on #24507 bug description add changelog entry fix changelog remove unnecessary js:true from specs change spec title add test for link to target branch before deletions renamed spec about state of target branch link before and after deletion some fixes into spec --- app/views/projects/merge_requests/_show.html.haml | 2 +- ...remove_deleted_branch_link_in_merge_request.yml | 4 +++ spec/features/merge_requests/target_branch_spec.rb | 41 ++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml create mode 100644 spec/features/merge_requests/target_branch_spec.rb diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 0e2975bd551..896f10104fa 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -31,7 +31,7 @@ %span.label-branch= source_branch_with_namespace(@merge_request) %span into %span.label-branch - = link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch) + = link_to_if @merge_request.target_branch_exists?, @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch) - if @merge_request.open? && @merge_request.diverged_from_target_branch? %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind) diff --git a/changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml b/changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml new file mode 100644 index 00000000000..34999480d4a --- /dev/null +++ b/changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml @@ -0,0 +1,4 @@ +--- +title: 'Remove unnecessary target branch link from MR page in case of deleted target branch' +merge_request: 7916 +author: Rydkin Maxim diff --git a/spec/features/merge_requests/target_branch_spec.rb b/spec/features/merge_requests/target_branch_spec.rb new file mode 100644 index 00000000000..b6134540273 --- /dev/null +++ b/spec/features/merge_requests/target_branch_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe 'Target branch', feature: true do + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request) } + let(:project) { merge_request.project } + + def path_to_merge_request + namespace_project_merge_request_path( + project.namespace, + project, merge_request + ) + end + + before do + login_as user + project.team << [user, :master] + end + + it 'shows link to target branch' do + visit path_to_merge_request + expect(page).to have_link('feature', href: namespace_project_commits_path(project.namespace, project, merge_request.target_branch)) + end + + context 'when branch was deleted' do + before do + DeleteBranchService.new(project, user).execute('feature') + visit path_to_merge_request + end + + it 'shows a message about missing target branch' do + expect(page).to have_content( + 'Target branch feature does not exist' + ) + end + + it 'does not show link to target branch' do + expect(page).not_to have_link('feature') + end + end +end -- cgit v1.2.1 From c3c9122d1e6aa532ad213394c4758653d4cf3874 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Mon, 5 Dec 2016 17:04:36 +0000 Subject: Adds new hover state Fixes the line that connects the dots Adds style for the badge Add new style for status text Fix badge style Adjust font weight --- app/assets/stylesheets/framework/variables.scss | 7 +++++ app/assets/stylesheets/pages/pipelines.scss | 34 ++++++++++++++++------ .../commit/_pipeline_status_group.html.haml | 2 +- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 18716813c48..0591801d259 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -571,3 +571,10 @@ $body-text-shadow: rgba(255,255,255,0.01); */ $ui-dev-kit-example-color: #bbb; $ui-dev-kit-example-border: #ddd; + +/* +Pipeline Graph +*/ +$stage-hover-bg: #eaf3fc; +$stage-hover-border: #d1e7fc; +$stage-bagde-text: #d4d4d4; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 08062b85504..cc8d4dd9544 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -363,15 +363,22 @@ .build { border: 1px solid $border-color; + border-radius: 30px; background-color: $white-light; + color: $gl-text-color; position: relative; padding: 7px 10px 8px; - border-radius: 30px; width: 186px; margin-bottom: 10px; &:hover { - background-color: $gray-lighter; + background-color: $stage-hover-bg; + border: 1px solid $stage-hover-border; + + .ci-status-text, + .dropdown-counter-bagde { + color: $gl-text-color; + } } &.playable { @@ -411,14 +418,14 @@ } .ci-status-text { - width: 135px; + max-width: 110px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - vertical-align: middle; + vertical-align: bottom; display: inline-block; position: relative; - top: -1px; + font-weight: 100; } a { @@ -435,7 +442,7 @@ flex-grow: 1; .ci-status-text { - max-width: 112px; + max-width: 110px; width: auto; } } @@ -480,7 +487,7 @@ } .ci-status-text { - width: 112px; + width: 110px; } .arrow { @@ -520,9 +527,18 @@ } } + .dropdown-counter-bagde { + float: right; + color: $stage-bagde-text; + font-weight: 100; + font-size: 13px; + margin-top: 1px; + margin-right: 2px; + } + svg { vertical-align: middle; - margin-right: 5px; + margin-right: 3px; } // Connect first build in each stage with right horizontal line @@ -531,7 +547,7 @@ content: ''; position: absolute; top: 48%; - right: -48px; + right: -49px; border-top: 2px solid $border-color; width: 48px; height: 1px; diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml index 2b26ad9d6fa..1e91e249fe9 100644 --- a/app/views/projects/commit/_pipeline_status_group.html.haml +++ b/app/views/projects/commit/_pipeline_status_group.html.haml @@ -4,7 +4,7 @@ = ci_icon_for_status(group_status) %span.ci-status-text = name - %span.badge= subject.size + %span.dropdown-counter-bagde= subject.size .dropdown-menu.grouped-pipeline-dropdown .arrow %ul -- cgit v1.2.1 From f9c103c2f314a2f9edbdfb93a26ace597d62e7e6 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Tue, 6 Dec 2016 11:56:50 +0000 Subject: Fix tooltip to show the all name CSS - Changes to look like newest mockups - Simplifies nested elements - Divides nodes from lines Remove is playable from left side Remove nested elements in scss Improve dropdown Focus state Fix scss linter Remove not used css Fix typo --- app/assets/stylesheets/pages/pipelines.scss | 498 ++++++++++----------- .../projects/ci/builds/_build_pipeline.html.haml | 6 +- .../commit/_pipeline_status_group.html.haml | 2 +- 3 files changed, 231 insertions(+), 275 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index cc8d4dd9544..cd7df7beda8 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -287,15 +287,40 @@ } // Pipeline visualization +.pipeline-actions { + border-bottom: none; +} -.toggle-pipeline-btn { - background-color: $gray-dark; +.tab-pane { + &.pipelines { + .ci-table { + min-width: 900px; + } - &.graph-collapsed { - background-color: $white-light; + .content-list.pipelines { + overflow: auto; + } + + .stage { + max-width: 100px; + width: 100px; + } + + .pipeline-actions { + min-width: initial; + } + } + + &.builds { + .ci-table { + tr { + height: 71px; + } + } } } +// Pipeline graph .pipeline-graph { width: 100%; background-color: $background-color; @@ -304,52 +329,120 @@ white-space: nowrap; transition: max-height 0.3s, padding 0.3s; - &.graph-collapsed { - max-height: 0; - padding: 0 16px; - } -} - -.pipeline-visualization { - position: relative; - ul { padding: 0; } -} -.stage-column { - display: inline-block; - vertical-align: top; + a { + text-decoration: none; + color: $gl-text-color-light; + } - &:not(:last-child) { - margin-right: 44px; + svg { + vertical-align: middle; + margin-right: 3px; } - &.left-margin { - &:not(:first-child) { - margin-left: 44px; + .stage-column { + display: inline-block; + vertical-align: top; - .left-connector { - &::before { - content: ''; - position: absolute; - top: 48%; - left: -48px; - border-top: 2px solid $border-color; - width: 48px; - height: 1px; + &:not(:last-child) { + margin-right: 44px; + } + + &.left-margin { + &:not(:first-child) { + margin-left: 44px; + + .left-connector { + &::before { + content: ''; + position: absolute; + top: 48%; + left: -48px; + border-top: 2px solid $border-color; + width: 48px; + height: 1px; + } } } } - } - &.no-margin { - margin: 0; - } + &.no-margin { + margin: 0; + } - li { - list-style: none; + li { + list-style: none; + } + + &:last-child { + .build { + // Remove right connecting horizontal line from first build in last stage + &:first-child { + &::after { + border: none; + } + } + // Remove right curved connectors from all builds in last stage + &:not(:first-child) { + &::after { + border: none; + } + } + // Remove opposite curve + .curve { + &::before { + display: none; + } + } + } + } + + &:first-child { + .build { + // Remove left curved connectors from all builds in first stage + &:not(:first-child) { + &::before { + border: none; + } + } + // Remove opposite curve + .curve { + &::after { + display: none; + } + } + } + } + + // Curve first child connecting lines in opposite direction + .curve { + display: none; + + &::before, + &::after { + content: ''; + width: 21px; + height: 25px; + position: absolute; + top: -31px; + border-top: 2px solid $border-color; + } + + &::after { + left: -44px; + border-right: 2px solid $border-color; + border-radius: 0 20px; + } + + &::before { + right: -44px; + border-left: 2px solid $border-color; + border-radius: 20px 0 0; + } + } } .stage-name { @@ -365,24 +458,12 @@ border: 1px solid $border-color; border-radius: 30px; background-color: $white-light; - color: $gl-text-color; position: relative; - padding: 7px 10px 8px; + padding: 8px 10px 9px; width: 186px; margin-bottom: 10px; - &:hover { - background-color: $stage-hover-bg; - border: 1px solid $stage-hover-border; - - .ci-status-text, - .dropdown-counter-bagde { - color: $gl-text-color; - } - } - &.playable { - svg { height: 13px; width: 20px; @@ -395,150 +476,56 @@ } } - .build-content { - display: -ms-flexbox; - display: -webkit-flex; - display: flex; - width: 164px; - - .ci-status-icon { - svg { - height: 20px; - width: 20px; - } - } - - .tooltip { - white-space: nowrap; - - .tooltip-inner { - overflow: hidden; - text-overflow: ellipsis; - } - } - - .ci-status-text { - max-width: 110px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - vertical-align: bottom; - display: inline-block; - position: relative; - font-weight: 100; - } - - a { - color: $gl-text-color-light; - text-decoration: none; - } + &:hover { + background-color: $stage-hover-bg; + border: 1px solid $stage-hover-border; + a, + .dropdown-counter-badge, .dropdown-menu-toggle { - background-color: transparent; - border: none; - width: auto; - padding: 0; - color: $gl-text-color-light; - flex-grow: 1; - - .ci-status-text { - max-width: 110px; - width: auto; - } + color: $gl-text-color; } - .grouped-pipeline-dropdown { - padding: 0; - width: 186px; - left: auto; - right: -197px; - top: -9px; - - ul { - max-height: 245px; - overflow: auto; - - li:first-child { - padding-top: 8px; - } - - li:last-child { - padding-bottom: 8px; - } - } + .grouped-pipeline-dropdown a { + color: $gl-text-color-light; - a { + &:hover { color: $gl-text-color; - padding: 7px 8px 8px; - - &:hover { - background-color: $blue-light-transparent; - border-radius: 3px; - - .ci-status-text { - text-decoration: none; - } - } - } - - svg { - width: 14px; - height: 14px; - } - - .ci-status-text { - width: 110px; } + } + } - .arrow { - &::before, - &::after { - content: ''; - display: inline-block; - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; - top: 18px; - } - - &::before { - left: -5px; - margin-top: -6px; - border-width: 7px 5px 7px 0; - border-right-color: $border-color; - } + .ci-status-icon svg { + height: 20px; + width: 20px; + } - &::after { - left: -4px; - margin-top: -9px; - border-width: 10px 7px 10px 0; - border-right-color: $white-light; - } - } + .arrow { + &::before, + &::after { + content: ''; + display: inline-block; + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + top: 18px; } - .badge { - background-color: $gray-darker; - color: $gl-text-color-light; - font-weight: normal; - margin-left: $btn-xs-side-margin; + &::before { + left: -5px; + margin-top: -6px; + border-width: 7px 5px 7px 0; + border-right-color: $border-color; } - } - - .dropdown-counter-bagde { - float: right; - color: $stage-bagde-text; - font-weight: 100; - font-size: 13px; - margin-top: 1px; - margin-right: 2px; - } - svg { - vertical-align: middle; - margin-right: 3px; + &::after { + left: -4px; + margin-top: -9px; + border-width: 10px 7px 10px 0; + border-right-color: $white-light; + } } // Connect first build in each stage with right horizontal line @@ -595,113 +582,86 @@ } } - &:last-child { - .build { - // Remove right connecting horizontal line from first build in last stage - &:first-child { - &::after { - border: none; - } - } - // Remove right curved connectors from all builds in last stage - &:not(:first-child) { - &::after { - border: none; - } - } - // Remove opposite curve - .curve { - &::before { - display: none; - } - } - } - } - - &:first-child { - .build { - // Remove left curved connectors from all builds in first stage - &:not(:first-child) { - &::before { - border: none; - } - } - // Remove opposite curve - .curve { - &::after { - display: none; - } - } - } + .ci-status-text { + max-width: 110px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: bottom; + display: inline-block; + position: relative; + font-weight: 100; } - // Curve first child connecting lines in opposite direction - .curve { - display: none; + .dropdown-menu-toggle { + background-color: transparent; + border: none; + padding: 0; + color: $gl-text-color-light; + flex-grow: 1; - &::before, - &::after { - content: ''; - width: 21px; - height: 25px; - position: absolute; - top: -32px; - border-top: 2px solid $border-color; + &:focus { + outline: none; } - &::after { - left: -44px; - border-right: 2px solid $border-color; - border-radius: 0 20px; - } + &:hover { + color: $gl-text-color; - &::before { - right: -44px; - border-left: 2px solid $border-color; - border-radius: 20px 0 0; + .dropdown-counter-bagde { + color: $gl-text-color; + } } } -} -.pipeline-actions { - border-bottom: none; -} - -.toggle-pipeline-btn { - - .fa { - color: $dropdown-header-color; + .dropdown-counter-bagde { + float: right; + color: $stage-bagde-text; + font-weight: 100; + font-size: 13px; + margin-top: 1px; + margin-right: 2px; } -} -.tab-pane { + .grouped-pipeline-dropdown { + padding: 0; + width: 191px; + left: auto; + right: -206px; + top: -11px; + box-shadow: 0 1px 5px $black-transparent; + + ul { + max-height: 245px; + overflow: auto; - &.pipelines { + li { + padding-top: 1px; + padding-bottom: 1px; + } - .ci-table { - min-width: 900px; - } + li:first-child { + padding-top: 9px; + } - .content-list.pipelines { - overflow: auto; + li:last-child { + padding-bottom: 9px; + } } - .stage { - max-width: 100px; - width: 100px; - } + a { + color: $gl-text-color-light; + padding: 7px 8px 8px; - .pipeline-actions { - min-width: initial; + &:hover { + background-color: $stage-hover-bg; + border-radius: 3px; + color: $gl-text-color; + } } - } - - &.builds { - .ci-table { - tr { - height: 71px; - } + .ci-status-icon svg { + height: 18px; + width: 18px; } } } diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml index 423a1282eb2..ac90d3278e2 100644 --- a/app/views/projects/ci/builds/_build_pipeline.html.haml +++ b/app/views/projects/ci/builds/_build_pipeline.html.haml @@ -1,9 +1,5 @@ - is_playable = subject.playable? && can?(current_user, :update_build, @project) -- if is_playable - = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.pipeline-graph', placement: 'bottom' } do - = ci_icon_for_status('play') - .ci-status-text= subject.name -- elsif can?(current_user, :read_build, @project) +- if can?(current_user, :read_build, @project) = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } do %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} = ci_icon_for_status(subject.status) diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml index 1e91e249fe9..8b782d38193 100644 --- a/app/views/projects/commit/_pipeline_status_group.html.haml +++ b/app/views/projects/commit/_pipeline_status_group.html.haml @@ -4,7 +4,7 @@ = ci_icon_for_status(group_status) %span.ci-status-text = name - %span.dropdown-counter-bagde= subject.size + %span.dropdown-counter-badge= subject.size .dropdown-menu.grouped-pipeline-dropdown .arrow %ul -- cgit v1.2.1 From bd30f75af67240d4069d4a4559faecf4ad7fab5a Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 7 Dec 2016 18:03:40 +0000 Subject: Adds actions to the nodes Improve CSS for dropdown actions --- app/assets/stylesheets/framework/variables.scss | 2 +- app/assets/stylesheets/pages/pipelines.scss | 84 ++++++++++++++++------ .../projects/ci/builds/_build_pipeline.html.haml | 22 ++++++ .../commit/_pipeline_status_group.html.haml | 2 +- 4 files changed, 86 insertions(+), 24 deletions(-) diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 0591801d259..cfb2ed9321b 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -577,4 +577,4 @@ Pipeline Graph */ $stage-hover-bg: #eaf3fc; $stage-hover-border: #d1e7fc; -$stage-bagde-text: #d4d4d4; +$stage-badge-text: #d4d4d4; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index cd7df7beda8..81cd397ef14 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -463,19 +463,6 @@ width: 186px; margin-bottom: 10px; - &.playable { - svg { - height: 13px; - width: 20px; - position: relative; - top: 1px; - - path { - fill: $layout-link-gray; - } - } - } - &:hover { background-color: $stage-hover-bg; border: 1px solid $stage-hover-border; @@ -607,15 +594,15 @@ &:hover { color: $gl-text-color; - .dropdown-counter-bagde { + .dropdown-counter-badge { color: $gl-text-color; } } } - .dropdown-counter-bagde { + .dropdown-counter-badge { float: right; - color: $stage-bagde-text; + color: $stage-badge-text; font-weight: 100; font-size: 13px; margin-top: 1px; @@ -630,27 +617,49 @@ top: -11px; box-shadow: 0 1px 5px $black-transparent; + a { + display: inline-block; + + &:hover { + background-color: $stage-hover-bg; + } + } + ul { max-height: 245px; overflow: auto; li { - padding-top: 1px; - padding-bottom: 1px; + padding-top: 2px; + margin: 0 5px; } li:first-child { - padding-top: 9px; + padding-top: 6px; } li:last-child { - padding-bottom: 9px; + padding-bottom: 6px; } } - a { + .dropdown-build { color: $gl-text-color-light; - padding: 7px 8px 8px; + + a { + padding: 7px 8px 8px; + } + + a.ci-action-icon-container { + padding: 0; + font-size: 11px; + float: right; + margin-top: 5px; + + i { + font-size: 11px; + } + } &:hover { background-color: $stage-hover-bg; @@ -665,3 +674,34 @@ } } } + +// Action Icons +.ci-action-icon-container { + padding: 0; + + .ci-action-icon-wrapper { + display: inline-block; + float: right; + + i { + color: $stage-badge-text; + border-radius: 100%; + border: 1px solid $stage-badge-text; + text-align: center; + display: table-cell; + vertical-align: middle; + padding: 5px; + font-size: 13px; + + &:hover { + color: $gl-text-color; + background-color: $stage-hover-bg; + border: 1px solid $gl-text-color; + } + } + + .ci-play-icon { + padding: 5px 4px 5px 7px; + } + } +} diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml index ac90d3278e2..41b9265fe5e 100644 --- a/app/views/projects/ci/builds/_build_pipeline.html.haml +++ b/app/views/projects/ci/builds/_build_pipeline.html.haml @@ -1,9 +1,31 @@ - is_playable = subject.playable? && can?(current_user, :update_build, @project) +- can_cancel = subject.active? && can?(current_user, :update_build, @project) +- can_retry = subject.retryable? && can?(current_user, :update_build, @project) +- can_stop = subject.complete? && subject.stops_environment? && can?(current_user, :update_build, @project) + - if can?(current_user, :read_build, @project) = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } do %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} = ci_icon_for_status(subject.status) .ci-status-text= subject.name + + - if is_playable + = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: "#{subject.name} - Play", class: 'ci-action-icon-container' do + %i.ci-action-icon-wrapper + = icon('play', class: 'ci-play-icon') + - elsif can_cancel + = link_to cancel_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: "#{subject.name} - Cancel", class: 'ci-action-icon-container' do + %i.ci-action-icon-wrapper + = icon('ban') + - elsif can_retry + = link_to retry_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: "#{subject.name} - Retry", class: 'ci-action-icon-container' do + %i.ci-action-icon-wrapper + = icon('refresh') + - elsif can_stop + = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: "#{subject.name} - Stop", class: 'ci-action-icon-container' do + %i.ci-action-icon-wrapper + = icon('stop') + - else %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} = ci_icon_for_status(subject.status) diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml index 8b782d38193..2d198d1b389 100644 --- a/app/views/projects/commit/_pipeline_status_group.html.haml +++ b/app/views/projects/commit/_pipeline_status_group.html.haml @@ -9,5 +9,5 @@ .arrow %ul - subject.each do |status| - %li + %li.dropdown-build = render "projects/#{status.to_partial_path}_pipeline", subject: status -- cgit v1.2.1 From 2320a5c7118084ed999b9fa0966c3315b94edd67 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 7 Dec 2016 20:28:29 +0000 Subject: Adds new icons for pipeline graph Fix padding Fix hover state of icon --- app/assets/stylesheets/pages/pipelines.scss | 5 +- app/helpers/ci_status_helper.rb | 66 ++++++++++++++-------- .../shared/icons/_icon_graph_job_cancelled.svg | 1 + app/views/shared/icons/_icon_graph_job_created.svg | 1 + app/views/shared/icons/_icon_graph_job_failed.svg | 1 + app/views/shared/icons/_icon_graph_job_manual.svg | 1 + app/views/shared/icons/_icon_graph_job_pending.svg | 1 + app/views/shared/icons/_icon_graph_job_running.svg | 1 + app/views/shared/icons/_icon_graph_job_skipped.svg | 1 + app/views/shared/icons/_icon_graph_job_success.svg | 1 + app/views/shared/icons/_icon_graph_job_warning.svg | 1 + 11 files changed, 56 insertions(+), 24 deletions(-) create mode 100755 app/views/shared/icons/_icon_graph_job_cancelled.svg create mode 100755 app/views/shared/icons/_icon_graph_job_created.svg create mode 100755 app/views/shared/icons/_icon_graph_job_failed.svg create mode 100755 app/views/shared/icons/_icon_graph_job_manual.svg create mode 100755 app/views/shared/icons/_icon_graph_job_pending.svg create mode 100755 app/views/shared/icons/_icon_graph_job_running.svg create mode 100755 app/views/shared/icons/_icon_graph_job_skipped.svg create mode 100755 app/views/shared/icons/_icon_graph_job_success.svg create mode 100755 app/views/shared/icons/_icon_graph_job_warning.svg diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 81cd397ef14..06b078d5345 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -647,7 +647,7 @@ color: $gl-text-color-light; a { - padding: 7px 8px 8px; + padding: 6px 8px 7px; } a.ci-action-icon-container { @@ -692,11 +692,12 @@ vertical-align: middle; padding: 5px; font-size: 13px; + background: $white-light; &:hover { color: $gl-text-color; background-color: $stage-hover-bg; - border: 1px solid $gl-text-color; + border: 1px solid $stage-hover-bg; } } diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 8e19752a8a1..999d279e5b9 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -43,32 +43,54 @@ module CiStatusHelper status.humanize end - def ci_icon_for_status(status) + def ci_icon_for_status(status, graph: nil) if detailed_status?(status) return custom_icon(status.icon) end - icon_name = - case status - when 'success' - 'icon_status_success' - when 'success_with_warnings' - 'icon_status_warning' - when 'failed' - 'icon_status_failed' - when 'pending' - 'icon_status_pending' - when 'running' - 'icon_status_running' - when 'play' - 'icon_play' - when 'created' - 'icon_status_created' - when 'skipped' - 'icon_status_skipped' - else - 'icon_status_canceled' - end + if graph + icon_name = + case status + when 'success' + 'icon_graph_job_success' + when 'success_with_warnings' + 'icon_graph_job_warning' + when 'failed' + 'icon_graph_job_failed' + when 'pending' + 'icon_graph_job_pending' + when 'running' + 'icon_graph_job_running' + when 'created' + 'icon_graph_job_created' + when 'skipped' + 'icon_graph_job_skipped' + else + 'icon_graph_job_canceled' + end + else + icon_name = + case status + when 'success' + 'icon_status_success' + when 'success_with_warnings' + 'icon_status_warning' + when 'failed' + 'icon_status_failed' + when 'pending' + 'icon_status_pending' + when 'running' + 'icon_status_running' + when 'play' + 'icon_play' + when 'created' + 'icon_status_created' + when 'skipped' + 'icon_status_skipped' + else + 'icon_status_canceled' + end + end custom_icon(icon_name) end diff --git a/app/views/shared/icons/_icon_graph_job_cancelled.svg b/app/views/shared/icons/_icon_graph_job_cancelled.svg new file mode 100755 index 00000000000..bd5d04e1cd7 --- /dev/null +++ b/app/views/shared/icons/_icon_graph_job_cancelled.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></svg> diff --git a/app/views/shared/icons/_icon_graph_job_created.svg b/app/views/shared/icons/_icon_graph_job_created.svg new file mode 100755 index 00000000000..326ad04e017 --- /dev/null +++ b/app/views/shared/icons/_icon_graph_job_created.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></svg> diff --git a/app/views/shared/icons/_icon_graph_job_failed.svg b/app/views/shared/icons/_icon_graph_job_failed.svg new file mode 100755 index 00000000000..64da5aa31fc --- /dev/null +++ b/app/views/shared/icons/_icon_graph_job_failed.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></svg> diff --git a/app/views/shared/icons/_icon_graph_job_manual.svg b/app/views/shared/icons/_icon_graph_job_manual.svg new file mode 100755 index 00000000000..c98839f51a9 --- /dev/null +++ b/app/views/shared/icons/_icon_graph_job_manual.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></svg> diff --git a/app/views/shared/icons/_icon_graph_job_pending.svg b/app/views/shared/icons/_icon_graph_job_pending.svg new file mode 100755 index 00000000000..02d5da407e3 --- /dev/null +++ b/app/views/shared/icons/_icon_graph_job_pending.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></svg> diff --git a/app/views/shared/icons/_icon_graph_job_running.svg b/app/views/shared/icons/_icon_graph_job_running.svg new file mode 100755 index 00000000000..532f4fee33c --- /dev/null +++ b/app/views/shared/icons/_icon_graph_job_running.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></svg> diff --git a/app/views/shared/icons/_icon_graph_job_skipped.svg b/app/views/shared/icons/_icon_graph_job_skipped.svg new file mode 100755 index 00000000000..1998dfef9ea --- /dev/null +++ b/app/views/shared/icons/_icon_graph_job_skipped.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7.69 7.7l-.905.905a.7.7 0 0 0 .99.99l1.85-1.85c.411-.412.411-1.078 0-1.49l-1.85-1.85a.7.7 0 0 0-.99.99l.905.905H4.48a.7.7 0 0 0 0 1.4h3.21z"/></g></svg> diff --git a/app/views/shared/icons/_icon_graph_job_success.svg b/app/views/shared/icons/_icon_graph_job_success.svg new file mode 100755 index 00000000000..eed5006bebe --- /dev/null +++ b/app/views/shared/icons/_icon_graph_job_success.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></svg> diff --git a/app/views/shared/icons/_icon_graph_job_warning.svg b/app/views/shared/icons/_icon_graph_job_warning.svg new file mode 100755 index 00000000000..cb785635b7e --- /dev/null +++ b/app/views/shared/icons/_icon_graph_job_warning.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></svg> -- cgit v1.2.1 From b4f18a30fb776e28fac405922cb5dfcfdc8ac5d7 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 7 Dec 2016 22:56:47 +0000 Subject: Adds tests for the status and actions icons rendered in the pipeline graph Fix padding in dropdown --- app/assets/stylesheets/pages/pipelines.scss | 4 --- spec/features/projects/pipelines/pipeline_spec.rb | 41 +++++++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 06b078d5345..304a7932a06 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -646,10 +646,6 @@ .dropdown-build { color: $gl-text-color-light; - a { - padding: 6px 8px 7px; - } - a.ci-action-icon-container { padding: 0; font-size: 11px; diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 3350a3aeefc..e21de05ac64 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -38,6 +38,47 @@ describe "Pipelines", feature: true, js: true do expect(page).to have_css('#js-tab-pipeline.active') end + context 'pipeline graph' do + it 'shows a running icon and a cancel action for the running build' do + page.within('.stage-column:first-child .build:first-child') do + expect(page).to have_selector('.ci-status-icon-running') + expect(page).to have_content('deploy') + expect(page).to have_selector('.ci-action-icon-container .fa-ban') + end + end + + it 'shows the success icon and a retry action for the successfull build' do + page.within('.stage-column:nth-child(3)') do + expect(page).to have_selector('.ci-status-icon-success') + expect(page).to have_content('build') + expect(page).to have_selector('.ci-action-icon-container .fa-refresh') + end + end + + it 'shows the failed icon and a retry action for the failed build' do + page.within('.stage-column:nth-child(2) .build') do + expect(page).to have_selector('.ci-status-icon-failed') + expect(page).to have_content('test') + expect(page).to have_selector('.ci-action-icon-container .fa-refresh') + end + end + + it 'shows the skipped icon and a play action for the manual build' do + page.within('.stage-column:first-child .build:nth-child(2)') do + expect(page).to have_selector('.ci-status-icon-skipped') + expect(page).to have_content('manual') + expect(page).to have_selector('.ci-action-icon-container .ci-play-icon') + end + end + + it 'shows the success icon for the generic comit status build' do + page.within('.stage-column:nth-child(4) .build') do + expect(page).to have_selector('.ci-status-icon-success') + expect(page).to have_content('jenkins') + end + end + end + context 'page tabs' do it 'shows Pipeline and Builds tabs with link' do expect(page).to have_link('Pipeline') -- cgit v1.2.1 From f7335aa5cb1ed368f0583d7dca8fcfa7574d6ad9 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 7 Dec 2016 23:09:55 +0000 Subject: Adds changelog entry --- changelogs/unreleased/22604-manual-actions.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/22604-manual-actions.yml diff --git a/changelogs/unreleased/22604-manual-actions.yml b/changelogs/unreleased/22604-manual-actions.yml new file mode 100644 index 00000000000..7335e597292 --- /dev/null +++ b/changelogs/unreleased/22604-manual-actions.yml @@ -0,0 +1,4 @@ +--- +title: Resolve "Manual actions on pipeline graph" +merge_request: 7931 +author: -- cgit v1.2.1 From f4f9af06c9e6f468d9f696dbb5438dd8825fe773 Mon Sep 17 00:00:00 2001 From: Horacio Sanson <horacio@allm.net> Date: Tue, 29 Nov 2016 13:18:13 +0900 Subject: Enable display of admonition icons in Asciidoc. When rendering Asciidoc documents this merge request enables the diplay of admonition blocks using font icons. This improves the looks of Asciidoc redered files. This uses the font-awesome fonts already present in Gitlab so no large dependencies are required for this to work. --- app/assets/stylesheets/framework.scss | 1 + app/assets/stylesheets/framework/asciidoctor.scss | 27 ++++++++++++++++++++++ .../enable-asciidoctor-admonition-icons.yml | 4 ++++ lib/gitlab/asciidoc.rb | 2 +- 4 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 app/assets/stylesheets/framework/asciidoctor.scss create mode 100644 changelogs/unreleased/enable-asciidoctor-admonition-icons.yml diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 4d4835568ed..c82a9a2b9e3 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -6,6 +6,7 @@ @import "framework/animations.scss"; @import "framework/avatar.scss"; +@import "framework/asciidoctor.scss"; @import "framework/blocks.scss"; @import "framework/buttons.scss"; @import "framework/calendar.scss"; diff --git a/app/assets/stylesheets/framework/asciidoctor.scss b/app/assets/stylesheets/framework/asciidoctor.scss new file mode 100644 index 00000000000..62493c32833 --- /dev/null +++ b/app/assets/stylesheets/framework/asciidoctor.scss @@ -0,0 +1,27 @@ +.admonitionblock td.icon { + width: 1%; + + [class^="fa icon-"] { + @extend .fa-2x; + } + + .icon-note { + @extend .fa-thumb-tack; + } + + .icon-tip { + @extend .fa-lightbulb-o; + } + + .icon-warning { + @extend .fa-exclamation-triangle; + } + + .icon-caution { + @extend .fa-fire; + } + + .icon-important { + @extend .fa-exclamation-circle; + } +} diff --git a/changelogs/unreleased/enable-asciidoctor-admonition-icons.yml b/changelogs/unreleased/enable-asciidoctor-admonition-icons.yml new file mode 100644 index 00000000000..9c52e53c3b4 --- /dev/null +++ b/changelogs/unreleased/enable-asciidoctor-admonition-icons.yml @@ -0,0 +1,4 @@ +--- +title: Enable AsciiDoctor admonition icons +merge_request: 7812 +author: Horacio Sanson diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb index 1a22ad9acf5..9667df4ffb8 100644 --- a/lib/gitlab/asciidoc.rb +++ b/lib/gitlab/asciidoc.rb @@ -6,7 +6,7 @@ module Gitlab module Asciidoc DEFAULT_ADOC_ATTRS = [ 'showtitle', 'idprefix=user-content-', 'idseparator=-', 'env=gitlab', - 'env-gitlab', 'source-highlighter=html-pipeline' + 'env-gitlab', 'source-highlighter=html-pipeline', 'icons=font' ].freeze # Public: Converts the provided Asciidoc markup into HTML. -- cgit v1.2.1 From e41c3f698d82be51076bf83a3e7d4d90d9fc7efb Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Thu, 8 Dec 2016 11:07:45 +1100 Subject: Correct previous stable branch used in 8.14 to 8.15 update guide [ci skip] --- doc/update/8.14-to-8.15.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/update/8.14-to-8.15.md b/doc/update/8.14-to-8.15.md index 576b943b98c..3f58493fa63 100644 --- a/doc/update/8.14-to-8.15.md +++ b/doc/update/8.14-to-8.15.md @@ -113,7 +113,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS 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-13-stable:config/gitlab.yml.example origin/8-15-stable:config/gitlab.yml.example +git diff origin/8-14-stable:config/gitlab.yml.example origin/8-15-stable:config/gitlab.yml.example ``` #### Git configuration @@ -131,10 +131,10 @@ Ensure you're still up-to-date with the latest NGINX configuration changes: ```sh # For HTTPS configurations -git diff origin/8-13-stable:lib/support/nginx/gitlab-ssl origin/8-15-stable:lib/support/nginx/gitlab-ssl +git diff origin/8-14-stable:lib/support/nginx/gitlab-ssl origin/8-15-stable:lib/support/nginx/gitlab-ssl # For HTTP configurations -git diff origin/8-13-stable:lib/support/nginx/gitlab origin/8-15-stable:lib/support/nginx/gitlab +git diff origin/8-14-stable:lib/support/nginx/gitlab origin/8-15-stable:lib/support/nginx/gitlab ``` If you are using Apache instead of NGINX please see the updated [Apache templates]. -- cgit v1.2.1 From c6acc7ed437e4dfef3fed9346718e67cd5905547 Mon Sep 17 00:00:00 2001 From: Andre Guedes <andrebsguedes@gmail.com> Date: Tue, 29 Nov 2016 14:51:45 -0200 Subject: Render SVG as images in notes --- app/uploaders/uploader_helper.rb | 2 +- changelogs/unreleased/render-svg-in-diffs-and-notes.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/render-svg-in-diffs-and-notes.yml diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb index b10ad71d052..fbaea2744a3 100644 --- a/app/uploaders/uploader_helper.rb +++ b/app/uploaders/uploader_helper.rb @@ -1,6 +1,6 @@ # Extra methods for uploader module UploaderHelper - IMAGE_EXT = %w[png jpg jpeg gif bmp tiff] + IMAGE_EXT = %w[png jpg jpeg gif bmp tiff svg] # We recommend using the .mp4 format over .mov. Videos in .mov format can # still be used but you really need to make sure they are served with the # proper MIME type video/mp4 and not video/quicktime or your videos won't play diff --git a/changelogs/unreleased/render-svg-in-diffs-and-notes.yml b/changelogs/unreleased/render-svg-in-diffs-and-notes.yml new file mode 100644 index 00000000000..827b0dbb1d3 --- /dev/null +++ b/changelogs/unreleased/render-svg-in-diffs-and-notes.yml @@ -0,0 +1,4 @@ +--- +title: Render SVG images in diffs and notes +merge_request: 7747 +author: andrebsguedes -- cgit v1.2.1 From 9e136aa9973323413189498a26d06672ecee295d Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Thu, 8 Dec 2016 17:30:07 +1100 Subject: Update factory_girl_rails to 4.7.0 --- Gemfile | 2 +- Gemfile.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index f49ef0386e7..f27d6363e3d 100644 --- a/Gemfile +++ b/Gemfile @@ -271,7 +271,7 @@ group :development, :test do gem 'fuubar', '~> 2.0.0' gem 'database_cleaner', '~> 1.5.0' - gem 'factory_girl_rails', '~> 4.6.0' + gem 'factory_girl_rails', '~> 4.7.0' gem 'rspec-rails', '~> 3.5.0' gem 'rspec-retry', '~> 0.4.5' gem 'spinach-rails', '~> 0.2.1' diff --git a/Gemfile.lock b/Gemfile.lock index 7a024e81ad2..c464ff70587 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -177,10 +177,10 @@ GEM excon (0.52.0) execjs (2.6.0) expression_parser (0.9.0) - factory_girl (4.5.0) + factory_girl (4.7.0) activesupport (>= 3.0.0) - factory_girl_rails (4.6.0) - factory_girl (~> 4.5.0) + factory_girl_rails (4.7.0) + factory_girl (~> 4.7.0) railties (>= 3.0.0) faraday (0.9.2) multipart-post (>= 1.2, < 3) @@ -819,7 +819,7 @@ DEPENDENCIES dropzonejs-rails (~> 0.7.1) email_reply_parser (~> 0.5.8) email_spec (~> 1.6.0) - factory_girl_rails (~> 4.6.0) + factory_girl_rails (~> 4.7.0) ffaker (~> 2.0.0) flay (~> 2.6.1) fog-aws (~> 0.9) -- cgit v1.2.1 From 007255ed5833bdd7bec8927d9f197002dc519186 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 8 Dec 2016 09:51:36 +0100 Subject: Added Ci::Status::Build --- app/models/ci/build.rb | 4 +++ app/models/commit_status.rb | 4 +++ lib/gitlab/ci/status/build/common.rb | 54 +++++++++++++++++++++++++++++++++++ lib/gitlab/ci/status/build/factory.rb | 15 ++++++++++ lib/gitlab/ci/status/core.rb | 10 +++++-- 5 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 lib/gitlab/ci/status/build/common.rb create mode 100644 lib/gitlab/ci/status/build/factory.rb diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 88c46076df6..0f4c498c266 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -100,6 +100,10 @@ module Ci end end + def detailed_status + Gitlab::Ci::Status::Build::Factory.new(self).fabricate! + end + def manual? self.when == 'manual' end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index cf90475f4d4..fce16174e22 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -131,4 +131,8 @@ class CommitStatus < ActiveRecord::Base def has_trace? false end + + def detailed_status + Gitlab::Ci::Status::Factory.new(self).fabricate! + end end diff --git a/lib/gitlab/ci/status/build/common.rb b/lib/gitlab/ci/status/build/common.rb new file mode 100644 index 00000000000..d3d7e03ee3f --- /dev/null +++ b/lib/gitlab/ci/status/build/common.rb @@ -0,0 +1,54 @@ +module Gitlab + module Ci + module Status + module Build + module Common + def has_details? + true + end + + def details_path + namespace_project_build_path(@subject.project.namespace, + @subject.project, + @subject.pipeline) + end + + def action_type + case + when @subject.playable? then :playable + when @subject.active? then :cancel + when @subject.retryable? then :retry + end + end + + def has_action?(current_user) + action_type && can?(current_user, :update_build, @subject) + end + + def action_icon + case action_type + when :playable then 'remove' + when :cancel then 'icon_play' + when :retry then 'repeat' + end + end + + def action_path + case action_type + when :playable + play_namespace_project_build_path(subject.project.namespace, subject.project, subject) + when :cancel + cancel_namespace_project_build_path(subject.project.namespace, subject.project, subject) + when :retry + retry_namespace_project_build_path(subject.project.namespace, subject.project, subject) + end + end + + def action_method + :post + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb new file mode 100644 index 00000000000..dd38e1418b6 --- /dev/null +++ b/lib/gitlab/ci/status/build/factory.rb @@ -0,0 +1,15 @@ +module Gitlab + module Ci + module Status + module Build + class Factory < Status::Factory + private + + def core_status + super.extend(Status::Build::Common) + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb index ce4108fdcf2..60c559248aa 100644 --- a/lib/gitlab/ci/status/core.rb +++ b/lib/gitlab/ci/status/core.rb @@ -34,15 +34,15 @@ module Gitlab end def has_details? - raise NotImplementedError + false end def details_path raise NotImplementedError end - def has_action? - raise NotImplementedError + def has_action?(_user = nil) + false end def action_icon @@ -52,6 +52,10 @@ module Gitlab def action_path raise NotImplementedError end + + def action_method + raise NotImplementedError + end end end end -- cgit v1.2.1 From cd4a2270c5ccbdbd9e57e0c625eee2d80357d6be Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 8 Dec 2016 10:40:56 +0100 Subject: Improve actions --- app/models/ci/build.rb | 4 +++ lib/gitlab/ci/status/build/common.rb | 26 ++++++------------- lib/gitlab/ci/status/build/factory.rb | 4 +++ lib/gitlab/ci/status/build/play.rb | 47 +++++++++++++++++++++++++++++++++++ lib/gitlab/ci/status/build/stop.rb | 47 +++++++++++++++++++++++++++++++++++ 5 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 lib/gitlab/ci/status/build/play.rb create mode 100644 lib/gitlab/ci/status/build/stop.rb diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 0f4c498c266..73564dd2aa0 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -127,6 +127,10 @@ module Ci end end + def cancelable? + active? + end + def retryable? project.builds_enabled? && commands.present? && complete? end diff --git a/lib/gitlab/ci/status/build/common.rb b/lib/gitlab/ci/status/build/common.rb index d3d7e03ee3f..2bed68d1a11 100644 --- a/lib/gitlab/ci/status/build/common.rb +++ b/lib/gitlab/ci/status/build/common.rb @@ -13,33 +13,23 @@ module Gitlab @subject.pipeline) end - def action_type - case - when @subject.playable? then :playable - when @subject.active? then :cancel - when @subject.retryable? then :retry - end - end - def has_action?(current_user) - action_type && can?(current_user, :update_build, @subject) + (subject.cancelable? || subject.retryable?) && + can?(current_user, :update_build, @subject) end def action_icon - case action_type - when :playable then 'remove' - when :cancel then 'icon_play' - when :retry then 'repeat' + case + when subject.cancelable? then 'icon_play' + when subject.retryable? then 'repeat' end end def action_path - case action_type - when :playable - play_namespace_project_build_path(subject.project.namespace, subject.project, subject) - when :cancel + case + when subject.cancelable? cancel_namespace_project_build_path(subject.project.namespace, subject.project, subject) - when :retry + when subject.retryable? retry_namespace_project_build_path(subject.project.namespace, subject.project, subject) end end diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb index dd38e1418b6..d8a9f53f236 100644 --- a/lib/gitlab/ci/status/build/factory.rb +++ b/lib/gitlab/ci/status/build/factory.rb @@ -5,6 +5,10 @@ module Gitlab class Factory < Status::Factory private + def extended_statuses + [Stop, Play] + end + def core_status super.extend(Status::Build::Common) end diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb new file mode 100644 index 00000000000..581c81d0175 --- /dev/null +++ b/lib/gitlab/ci/status/build/play.rb @@ -0,0 +1,47 @@ +module Gitlab + module Ci + module Status + module Status + class Play < SimpleDelegator + extend Status::Extended + + def text + 'play' + end + + def label + 'play' + end + + def icon + 'icon_status_skipped' + end + + def to_s + 'play' + end + + def has_action?(current_user) + can?(current_user, :update_build, subject) + end + + def action_icon + :play + end + + def action_path + play_namespace_project_build_path(subject.project.namespace, subject.project, subject) + end + + def action_method + :post + end + + def self.matches?(build) + build.playable? && !build.stops_environment? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb new file mode 100644 index 00000000000..261de9695c5 --- /dev/null +++ b/lib/gitlab/ci/status/build/stop.rb @@ -0,0 +1,47 @@ +module Gitlab + module Ci + module Status + module Status + class Play < SimpleDelegator + extend Status::Extended + + def text + 'stop' + end + + def label + 'stop' + end + + def icon + 'icon_status_skipped' + end + + def to_s + 'stop' + end + + def has_action?(current_user) + can?(current_user, :update_build, subject) + end + + def action_icon + :play + end + + def action_path + play_namespace_project_build_path(subject.project.namespace, subject.project, subject) + end + + def action_method + :post + end + + def self.matches?(build) + build.playable? && build.stops_environment? + end + end + end + end + end +end -- cgit v1.2.1 From 83232be0e14cc8b35bf74532203a6e4371c15e70 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Mon, 31 Oct 2016 13:00:53 +0200 Subject: Add nested groups support on data level * add parent_id field to namespaces table to store relation with nested groups * create routes table to keep information about full path of every group and project * project/group lookup by full path from routes table Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- app/controllers/admin/groups_controller.rb | 2 +- app/controllers/groups/application_controller.rb | 2 +- app/helpers/groups_helper.rb | 2 +- app/models/concerns/routable.rb | 70 ++++++++++++++++ app/models/namespace.rb | 18 +++- app/models/project.rb | 97 +++------------------- app/models/route.rb | 22 +++++ app/services/destroy_group_service.rb | 4 + app/views/projects/forks/error.html.haml | 6 +- changelogs/unreleased/dz-nested-groups.yml | 4 + .../20161124111390_add_parent_id_to_namespace.rb | 12 +++ .../20161124111395_add_index_to_parent_id.rb | 14 ++++ db/migrate/20161124111402_add_routes_table.rb | 18 ++++ db/migrate/20161130095245_fill_routes_table.rb | 21 +++++ .../20161130101252_fill_projects_routes_table.rb | 22 +++++ ...20161202152031_remove_duplicates_from_routes.rb | 28 +++++++ db/migrate/20161202152035_add_index_to_routes.rb | 16 ++++ db/schema.rb | 25 ++++-- lib/api/helpers.rb | 2 +- lib/constraints/group_url_constrainer.rb | 2 +- spec/lib/constraints/group_url_constrainer_spec.rb | 7 ++ spec/lib/gitlab/import_export/all_models.yml | 1 + spec/models/concerns/routable_spec.rb | 67 +++++++++++++++ spec/models/namespace_spec.rb | 8 ++ spec/models/project_spec.rb | 62 -------------- spec/models/route_spec.rb | 29 +++++++ 26 files changed, 402 insertions(+), 159 deletions(-) create mode 100644 app/models/concerns/routable.rb create mode 100644 app/models/route.rb create mode 100644 changelogs/unreleased/dz-nested-groups.yml create mode 100644 db/migrate/20161124111390_add_parent_id_to_namespace.rb create mode 100644 db/migrate/20161124111395_add_index_to_parent_id.rb create mode 100644 db/migrate/20161124111402_add_routes_table.rb create mode 100644 db/migrate/20161130095245_fill_routes_table.rb create mode 100644 db/migrate/20161130101252_fill_projects_routes_table.rb create mode 100644 db/migrate/20161202152031_remove_duplicates_from_routes.rb create mode 100644 db/migrate/20161202152035_add_index_to_routes.rb create mode 100644 spec/models/concerns/routable_spec.rb create mode 100644 spec/models/route_spec.rb diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index aa7570cd896..1e3d194e9f9 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -56,7 +56,7 @@ class Admin::GroupsController < Admin::ApplicationController private def group - @group ||= Group.find_by(path: params[:id]) + @group ||= Group.find_by_full_path(params[:id]) end def group_params diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb index 949b4a6c25a..c411c21bb80 100644 --- a/app/controllers/groups/application_controller.rb +++ b/app/controllers/groups/application_controller.rb @@ -9,7 +9,7 @@ class Groups::ApplicationController < ApplicationController def group unless @group id = params[:group_id] || params[:id] - @group = Group.find_by(path: id) + @group = Group.find_by_full_path(id) unless @group && can?(current_user, :read_group, @group) @group = nil diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 19ab059aea6..f6d4ea4659a 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -5,7 +5,7 @@ module GroupsHelper def group_icon(group) if group.is_a?(String) - group = Group.find_by(path: group) + group = Group.find_by_full_path(group) end group.try(:avatar_url) || image_path('no_group_avatar.png') diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb new file mode 100644 index 00000000000..d36bb9da296 --- /dev/null +++ b/app/models/concerns/routable.rb @@ -0,0 +1,70 @@ +# Store object full path in separate table for easy lookup and uniq validation +# Object must have path db field and respond to full_path and full_path_changed? methods. +module Routable + extend ActiveSupport::Concern + + included do + has_one :route, as: :source, autosave: true, dependent: :destroy + + validates_associated :route + + before_validation :update_route_path, if: :full_path_changed? + end + + class_methods do + # Finds a single object by full path match in routes table. + # + # Usage: + # + # Klass.find_by_full_path('gitlab-org/gitlab-ce') + # + # Returns a single object, or nil. + def find_by_full_path(path) + # On MySQL we want to ensure the ORDER BY uses a case-sensitive match so + # any literal matches come first, for this we have to use "BINARY". + # Without this there's still no guarantee in what order MySQL will return + # rows. + binary = Gitlab::Database.mysql? ? 'BINARY' : '' + + order_sql = "(CASE WHEN #{binary} routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)" + + where_paths_in([path]).reorder(order_sql).take + end + + # Builds a relation to find multiple objects by their full paths. + # + # Usage: + # + # Klass.where_paths_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee}) + # + # Returns an ActiveRecord::Relation. + def where_paths_in(paths) + wheres = [] + cast_lower = Gitlab::Database.postgresql? + + paths.each do |path| + path = connection.quote(path) + where = "(routes.path = #{path})" + + if cast_lower + where = "(#{where} OR (LOWER(routes.path) = LOWER(#{path})))" + end + + wheres << where + end + + if wheres.empty? + none + else + joins(:route).where(wheres.join(' OR ')) + end + end + end + + private + + def update_route_path + route || build_route(source: self) + route.path = full_path + end +end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 891dffac648..ff161e4c53f 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -4,12 +4,16 @@ class Namespace < ActiveRecord::Base include CacheMarkdownField include Sortable include Gitlab::ShellAdapter + include Routable cache_markdown_field :description, pipeline: :description has_many :projects, dependent: :destroy belongs_to :owner, class_name: "User" + belongs_to :parent, class_name: "Namespace" + has_many :children, class_name: "Namespace", foreign_key: :parent_id + validates :owner, presence: true, unless: ->(n) { n.type == "Group" } validates :name, length: { within: 0..255 }, @@ -86,7 +90,7 @@ class Namespace < ActiveRecord::Base end def to_param - path + full_path end def human_name @@ -150,6 +154,14 @@ class Namespace < ActiveRecord::Base Gitlab.config.lfs.enabled end + def full_path + if parent + parent.full_path + '/' + path + else + path + end + end + private def repository_storage_paths @@ -185,4 +197,8 @@ class Namespace < ActiveRecord::Base where(projects: { namespace_id: id }). find_each(&:refresh_members_authorized_projects) end + + def full_path_changed? + path_changed? || parent_id_changed? + end end diff --git a/app/models/project.rb b/app/models/project.rb index 9d58aff4033..7659a7bf152 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -14,6 +14,7 @@ class Project < ActiveRecord::Base include TokenAuthenticatable include ProjectFeaturesCompatibility include SelectForProjectAuthorization + include Routable extend Gitlab::ConfigHelper @@ -324,87 +325,6 @@ class Project < ActiveRecord::Base non_archived.where(table[:name].matches(pattern)) end - # Finds a single project for the given path. - # - # path - The full project path (including namespace path). - # - # Returns a Project, or nil if no project could be found. - def find_with_namespace(path) - namespace_path, project_path = path.split('/', 2) - - return unless namespace_path && project_path - - namespace_path = connection.quote(namespace_path) - project_path = connection.quote(project_path) - - # On MySQL we want to ensure the ORDER BY uses a case-sensitive match so - # any literal matches come first, for this we have to use "BINARY". - # Without this there's still no guarantee in what order MySQL will return - # rows. - binary = Gitlab::Database.mysql? ? 'BINARY' : '' - - order_sql = "(CASE WHEN #{binary} namespaces.path = #{namespace_path} " \ - "AND #{binary} projects.path = #{project_path} THEN 0 ELSE 1 END)" - - where_paths_in([path]).reorder(order_sql).take - end - - # Builds a relation to find multiple projects by their full paths. - # - # Each path must be in the following format: - # - # namespace_path/project_path - # - # For example: - # - # gitlab-org/gitlab-ce - # - # Usage: - # - # Project.where_paths_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee}) - # - # This would return the projects with the full paths matching the values - # given. - # - # paths - An Array of full paths (namespace path + project path) for which - # to find the projects. - # - # Returns an ActiveRecord::Relation. - def where_paths_in(paths) - wheres = [] - cast_lower = Gitlab::Database.postgresql? - - paths.each do |path| - namespace_path, project_path = path.split('/', 2) - - next unless namespace_path && project_path - - namespace_path = connection.quote(namespace_path) - project_path = connection.quote(project_path) - - where = "(namespaces.path = #{namespace_path} - AND projects.path = #{project_path})" - - if cast_lower - where = "( - #{where} - OR ( - LOWER(namespaces.path) = LOWER(#{namespace_path}) - AND LOWER(projects.path) = LOWER(#{project_path}) - ) - )" - end - - wheres << where - end - - if wheres.empty? - none - else - joins(:namespace).where(wheres.join(' OR ')) - end - end - def visibility_levels Gitlab::VisibilityLevel.options end @@ -440,6 +360,10 @@ class Project < ActiveRecord::Base def group_ids joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id) end + + # Add alias for Routable method for compatibility with old code. + # In future all calls `find_with_namespace` should be replaced with `find_by_full_path` + alias_method :find_with_namespace, :find_by_full_path end def lfs_enabled? @@ -879,13 +803,14 @@ class Project < ActiveRecord::Base end alias_method :human_name, :name_with_namespace - def path_with_namespace - if namespace - namespace.path + '/' + path + def full_path + if namespace && path + namespace.full_path + '/' + path else path end end + alias_method :path_with_namespace, :full_path def execute_hooks(data, hooks_scope = :push_hooks) hooks.send(hooks_scope).each do |hook| @@ -1373,4 +1298,8 @@ class Project < ActiveRecord::Base def validate_board_limit(board) raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS end + + def full_path_changed? + path_changed? || namespace_id_changed? + end end diff --git a/app/models/route.rb b/app/models/route.rb new file mode 100644 index 00000000000..d40214b9da6 --- /dev/null +++ b/app/models/route.rb @@ -0,0 +1,22 @@ +class Route < ActiveRecord::Base + belongs_to :source, polymorphic: true + + validates :source, presence: true + + validates :path, + length: { within: 1..255 }, + presence: true, + uniqueness: { case_sensitive: false } + + after_update :rename_children, if: :path_changed? + + def rename_children + # We update each row separately because MySQL does not have regexp_replace. + # rubocop:disable Rails/FindEach + Route.where('path LIKE ?', "#{path_was}%").each do |route| + # Note that update column skips validation and callbacks. + # We need this to avoid recursive call of rename_children method + route.update_column(:path, route.path.sub(path_was, path)) + end + end +end diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb index a880952e274..2316c57bf1e 100644 --- a/app/services/destroy_group_service.rb +++ b/app/services/destroy_group_service.rb @@ -20,6 +20,10 @@ class DestroyGroupService ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute end + group.children.each do |group| + DestroyGroupService.new(group, current_user).async_execute + end + group.really_destroy! end end diff --git a/app/views/projects/forks/error.html.haml b/app/views/projects/forks/error.html.haml index 3d0ab5b85d6..98d81308407 100644 --- a/app/views/projects/forks/error.html.haml +++ b/app/views/projects/forks/error.html.haml @@ -13,7 +13,11 @@ - if @forked_project && @forked_project.errors.any? %p – - = @forked_project.errors.full_messages.first + - error = @forked_project.errors.full_messages.first + - if error.include?("already been taken") + Name has already been taken + - else + = error %p = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork", class: "btn" do diff --git a/changelogs/unreleased/dz-nested-groups.yml b/changelogs/unreleased/dz-nested-groups.yml new file mode 100644 index 00000000000..c227c5a8ea5 --- /dev/null +++ b/changelogs/unreleased/dz-nested-groups.yml @@ -0,0 +1,4 @@ +--- +title: Add nested groups support on data level +merge_request: +author: diff --git a/db/migrate/20161124111390_add_parent_id_to_namespace.rb b/db/migrate/20161124111390_add_parent_id_to_namespace.rb new file mode 100644 index 00000000000..a6fa1b70a9d --- /dev/null +++ b/db/migrate/20161124111390_add_parent_id_to_namespace.rb @@ -0,0 +1,12 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddParentIdToNamespace < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column(:namespaces, :parent_id, :integer) + end +end diff --git a/db/migrate/20161124111395_add_index_to_parent_id.rb b/db/migrate/20161124111395_add_index_to_parent_id.rb new file mode 100644 index 00000000000..eab74c01dfd --- /dev/null +++ b/db/migrate/20161124111395_add_index_to_parent_id.rb @@ -0,0 +1,14 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddIndexToParentId < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def change + add_concurrent_index(:namespaces, [:parent_id, :id], unique: true) + end +end diff --git a/db/migrate/20161124111402_add_routes_table.rb b/db/migrate/20161124111402_add_routes_table.rb new file mode 100644 index 00000000000..a02e046a18e --- /dev/null +++ b/db/migrate/20161124111402_add_routes_table.rb @@ -0,0 +1,18 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddRoutesTable < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + create_table :routes do |t| + t.integer :source_id, null: false + t.string :source_type, null: false + t.string :path, null: false + + t.timestamps + end + end +end diff --git a/db/migrate/20161130095245_fill_routes_table.rb b/db/migrate/20161130095245_fill_routes_table.rb new file mode 100644 index 00000000000..6754e583000 --- /dev/null +++ b/db/migrate/20161130095245_fill_routes_table.rb @@ -0,0 +1,21 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class FillRoutesTable < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = 'No new namespaces should be created during data copy' + + def up + execute <<-EOF + INSERT INTO routes + (source_id, source_type, path) + (SELECT id, 'Namespace', path FROM namespaces) + EOF + end + + def down + Route.delete_all(source_type: 'Namespace') + end +end diff --git a/db/migrate/20161130101252_fill_projects_routes_table.rb b/db/migrate/20161130101252_fill_projects_routes_table.rb new file mode 100644 index 00000000000..14700583be5 --- /dev/null +++ b/db/migrate/20161130101252_fill_projects_routes_table.rb @@ -0,0 +1,22 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class FillProjectsRoutesTable < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = 'No new projects should be created during data copy' + + def up + execute <<-EOF + INSERT INTO routes + (source_id, source_type, path) + (SELECT projects.id, 'Project', concat(namespaces.path, '/', projects.path) FROM projects + INNER JOIN namespaces ON projects.namespace_id = namespaces.id) + EOF + end + + def down + Route.delete_all(source_type: 'Project') + end +end diff --git a/db/migrate/20161202152031_remove_duplicates_from_routes.rb b/db/migrate/20161202152031_remove_duplicates_from_routes.rb new file mode 100644 index 00000000000..510796e05f2 --- /dev/null +++ b/db/migrate/20161202152031_remove_duplicates_from_routes.rb @@ -0,0 +1,28 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveDuplicatesFromRoutes < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + select_all("SELECT path FROM #{quote_table_name(:routes)} GROUP BY path HAVING COUNT(*) > 1").each do |row| + path = connection.quote(row['path']) + execute(%Q{ + DELETE FROM #{quote_table_name(:routes)} + WHERE path = #{path} + AND id != ( + SELECT id FROM ( + SELECT max(id) AS id + FROM #{quote_table_name(:routes)} + WHERE path = #{path} + ) max_ids + ) + }) + end + end + + def down + end +end diff --git a/db/migrate/20161202152035_add_index_to_routes.rb b/db/migrate/20161202152035_add_index_to_routes.rb new file mode 100644 index 00000000000..4a51337bda6 --- /dev/null +++ b/db/migrate/20161202152035_add_index_to_routes.rb @@ -0,0 +1,16 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddIndexToRoutes < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def change + add_concurrent_index(:routes, :path, unique: true) + add_concurrent_index(:routes, [:source_type, :source_id], unique: true) + end +end diff --git a/db/schema.rb b/db/schema.rb index 0d510c8a269..9c46f573719 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: 20161128161412) do +ActiveRecord::Schema.define(version: 20161202152035) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -98,14 +98,14 @@ ActiveRecord::Schema.define(version: 20161128161412) do t.text "help_page_text_html" t.text "shared_runners_text_html" t.text "after_sign_up_text_html" - t.boolean "sidekiq_throttling_enabled", default: false - t.string "sidekiq_throttling_queues" - t.decimal "sidekiq_throttling_factor" t.boolean "housekeeping_enabled", default: true, null: false t.boolean "housekeeping_bitmaps_enabled", default: true, null: false t.integer "housekeeping_incremental_repack_period", default: 10, null: false t.integer "housekeeping_full_repack_period", default: 50, null: false t.integer "housekeeping_gc_period", default: 200, null: false + t.boolean "sidekiq_throttling_enabled", default: false + t.string "sidekiq_throttling_queues" + t.decimal "sidekiq_throttling_factor" t.boolean "html_emails_enabled", default: true end @@ -737,8 +737,9 @@ ActiveRecord::Schema.define(version: 20161128161412) do t.integer "visibility_level", default: 20, null: false t.boolean "request_access_enabled", default: false, null: false t.datetime "deleted_at" - t.boolean "lfs_enabled" t.text "description_html" + t.boolean "lfs_enabled" + t.integer "parent_id" end add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree @@ -746,6 +747,7 @@ ActiveRecord::Schema.define(version: 20161128161412) do add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"} add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree + add_index "namespaces", ["parent_id", "id"], name: "index_namespaces_on_parent_id_and_id", unique: true, using: :btree add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"} add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree @@ -991,6 +993,17 @@ ActiveRecord::Schema.define(version: 20161128161412) do add_index "releases", ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree add_index "releases", ["project_id"], name: "index_releases_on_project_id", using: :btree + create_table "routes", force: :cascade do |t| + t.integer "source_id", null: false + t.string "source_type", null: false + t.string "path", null: false + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "routes", ["path"], name: "index_routes_on_path", unique: true, using: :btree + add_index "routes", ["source_type", "source_id"], name: "index_routes_on_source_type_and_source_id", unique: true, using: :btree + create_table "sent_notifications", force: :cascade do |t| t.integer "project_id" t.integer "noteable_id" @@ -1206,8 +1219,8 @@ ActiveRecord::Schema.define(version: 20161128161412) do t.datetime "otp_grace_period_started_at" t.boolean "ldap_email", default: false, null: false t.boolean "external", default: false - t.string "organization" t.string "incoming_email_token" + t.string "organization" t.boolean "authorized_projects_populated" end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 7f94ede7940..96ccde760db 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -108,7 +108,7 @@ module API if id =~ /^\d+$/ Group.find_by(id: id) else - Group.find_by(path: id) + Group.find_by_full_path(id) end end diff --git a/lib/constraints/group_url_constrainer.rb b/lib/constraints/group_url_constrainer.rb index 5711d96a586..bae4db1ca4d 100644 --- a/lib/constraints/group_url_constrainer.rb +++ b/lib/constraints/group_url_constrainer.rb @@ -4,7 +4,7 @@ class GroupUrlConstrainer return false unless valid?(id) - Group.find_by(path: id).present? + Group.find_by_full_path(id).present? end private diff --git a/spec/lib/constraints/group_url_constrainer_spec.rb b/spec/lib/constraints/group_url_constrainer_spec.rb index 892554f2870..96dacdc5cd2 100644 --- a/spec/lib/constraints/group_url_constrainer_spec.rb +++ b/spec/lib/constraints/group_url_constrainer_spec.rb @@ -10,6 +10,13 @@ describe GroupUrlConstrainer, lib: true do it { expect(subject.matches?(request)).to be_truthy } end + context 'valid request for nested group' do + let!(:nested_group) { create(:group, path: 'nested', parent: group) } + let!(:request) { build_request('gitlab/nested') } + + it { expect(subject.matches?(request)).to be_truthy } + end + context 'invalid request' do let(:request) { build_request('foo') } diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 7e00e214c6e..8e1a28f2723 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -188,6 +188,7 @@ project: - project_feature - authorized_users - project_authorizations +- route award_emoji: - awardable - user diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb new file mode 100644 index 00000000000..0acefc0c1d5 --- /dev/null +++ b/spec/models/concerns/routable_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe Group, 'Routable' do + let!(:group) { create(:group) } + + describe 'Associations' do + it { is_expected.to have_one(:route).dependent(:destroy) } + end + + describe 'Callbacks' do + it 'creates route record on create' do + expect(group.route.path).to eq(group.path) + end + + it 'updates route record on path change' do + group.update_attributes(path: 'wow') + + expect(group.route.path).to eq('wow') + end + + it 'ensure route path uniqueness across different objects' do + create(:group, parent: group, path: 'xyz') + duplicate = build(:project, namespace: group, path: 'xyz') + + expect { duplicate.save! }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Route path has already been taken, Route is invalid') + end + end + + describe '.find_by_full_path' do + let!(:nested_group) { create(:group, parent: group) } + + it { expect(described_class.find_by_full_path(group.to_param)).to eq(group) } + it { expect(described_class.find_by_full_path(group.to_param.upcase)).to eq(group) } + it { expect(described_class.find_by_full_path(nested_group.to_param)).to eq(nested_group) } + it { expect(described_class.find_by_full_path('unknown')).to eq(nil) } + end + + describe '.where_paths_in' do + context 'without any paths' do + it 'returns an empty relation' do + expect(described_class.where_paths_in([])).to eq([]) + end + end + + context 'without any valid paths' do + it 'returns an empty relation' do + expect(described_class.where_paths_in(%w[unknown])).to eq([]) + end + end + + context 'with valid paths' do + let!(:nested_group) { create(:group, parent: group) } + + it 'returns the projects matching the paths' do + result = described_class.where_paths_in([group.to_param, nested_group.to_param]) + + expect(result).to contain_exactly(group, nested_group) + end + + it 'returns projects regardless of the casing of paths' do + result = described_class.where_paths_in([group.to_param.upcase, nested_group.to_param.upcase]) + + expect(result).to contain_exactly(group, nested_group) + end + end + end +end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 431b3e4435f..67f6a0bd4fe 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -117,4 +117,12 @@ describe Namespace, models: true do expect(Namespace.clean_path("--%+--valid_*&%name=.git.%.atom.atom.@email.com")).to eq("valid_name") end end + + describe '#full_path' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + + it { expect(group.full_path).to eq(group.path) } + it { expect(nested_group.full_path).to eq("#{group.path}/#{nested_group.path}") } + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 587ca1936a3..1b9e69933c1 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -474,35 +474,6 @@ describe Project, models: true do end end - describe '.find_with_namespace' do - context 'with namespace' do - before do - @group = create :group, name: 'gitlab' - @project = create(:project, name: 'gitlabhq', namespace: @group) - 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 - - context 'when multiple projects using a similar name exist' do - let(:group) { create(:group, name: 'gitlab') } - - let!(:project1) do - create(:empty_project, name: 'gitlab1', path: 'gitlab', namespace: group) - end - - let!(:project2) do - create(:empty_project, name: 'gitlab2', path: 'GITLAB', namespace: group) - end - - it 'returns the row where the path matches literally' do - expect(Project.find_with_namespace('gitlab/GITLAB')).to eq(project2) - end - end - end - describe '#to_param' do context 'with namespace' do before do @@ -1544,39 +1515,6 @@ describe Project, models: true do end end - describe '.where_paths_in' do - context 'without any paths' do - it 'returns an empty relation' do - expect(Project.where_paths_in([])).to eq([]) - end - end - - context 'without any valid paths' do - it 'returns an empty relation' do - expect(Project.where_paths_in(%w[foo])).to eq([]) - end - end - - context 'with valid paths' do - let!(:project1) { create(:project) } - let!(:project2) { create(:project) } - - it 'returns the projects matching the paths' do - projects = Project.where_paths_in([project1.path_with_namespace, - project2.path_with_namespace]) - - expect(projects).to contain_exactly(project1, project2) - end - - it 'returns projects regardless of the casing of paths' do - projects = Project.where_paths_in([project1.path_with_namespace.upcase, - project2.path_with_namespace.upcase]) - - expect(projects).to contain_exactly(project1, project2) - end - end - end - describe 'change_head' do let(:project) { create(:project) } diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb new file mode 100644 index 00000000000..6f491fdf9a0 --- /dev/null +++ b/spec/models/route_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Route, models: true do + let!(:group) { create(:group) } + let!(:route) { group.route } + + describe 'relationships' do + it { is_expected.to belong_to(:source) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:source) } + it { is_expected.to validate_presence_of(:path) } + it { is_expected.to validate_uniqueness_of(:path) } + end + + describe '#rename_children' do + let!(:nested_group) { create(:group, path: "test", parent: group) } + let!(:deep_nested_group) { create(:group, path: "foo", parent: nested_group) } + + it "updates children routes with new path" do + route.update_attributes(path: 'bar') + + expect(described_class.exists?(path: 'bar')).to be_truthy + expect(described_class.exists?(path: 'bar/test')).to be_truthy + expect(described_class.exists?(path: 'bar/test/foo')).to be_truthy + end + end +end -- cgit v1.2.1 From 82ee1d29fd3806982afe98678e056194059c64ce Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 8 Dec 2016 10:48:08 +0100 Subject: Introduce `cancelable` and `returnable` [ci skip] --- lib/gitlab/ci/status/build/cancelable.rb | 31 +++++++++++++++++++++++++++++++ lib/gitlab/ci/status/build/common.rb | 25 ------------------------- lib/gitlab/ci/status/build/factory.rb | 2 +- lib/gitlab/ci/status/build/play.rb | 10 +--------- lib/gitlab/ci/status/build/retryable.rb | 31 +++++++++++++++++++++++++++++++ 5 files changed, 64 insertions(+), 35 deletions(-) create mode 100644 lib/gitlab/ci/status/build/cancelable.rb create mode 100644 lib/gitlab/ci/status/build/retryable.rb diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb new file mode 100644 index 00000000000..bff0464ef0c --- /dev/null +++ b/lib/gitlab/ci/status/build/cancelable.rb @@ -0,0 +1,31 @@ +module Gitlab + module Ci + module Status + module Status + class Cancelable < SimpleDelegator + extend Status::Extended + + def has_action?(current_user) + can?(current_user, :update_build, subject) + end + + def action_icon + 'remove' + end + + def action_path + cancel_namespace_project_build_path(subject.project.namespace, subject.project, subject) + end + + def action_method + :post + end + + def self.matches?(build) + build.cancelable? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/build/common.rb b/lib/gitlab/ci/status/build/common.rb index 2bed68d1a11..3e47d7dfd20 100644 --- a/lib/gitlab/ci/status/build/common.rb +++ b/lib/gitlab/ci/status/build/common.rb @@ -12,31 +12,6 @@ module Gitlab @subject.project, @subject.pipeline) end - - def has_action?(current_user) - (subject.cancelable? || subject.retryable?) && - can?(current_user, :update_build, @subject) - end - - def action_icon - case - when subject.cancelable? then 'icon_play' - when subject.retryable? then 'repeat' - end - end - - def action_path - case - when subject.cancelable? - cancel_namespace_project_build_path(subject.project.namespace, subject.project, subject) - when subject.retryable? - retry_namespace_project_build_path(subject.project.namespace, subject.project, subject) - end - end - - def action_method - :post - end end end end diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb index d8a9f53f236..8f420a93954 100644 --- a/lib/gitlab/ci/status/build/factory.rb +++ b/lib/gitlab/ci/status/build/factory.rb @@ -6,7 +6,7 @@ module Gitlab private def extended_statuses - [Stop, Play] + [Stop, Play, Cancelable, Retryable] end def core_status diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb index 581c81d0175..d295850137b 100644 --- a/lib/gitlab/ci/status/build/play.rb +++ b/lib/gitlab/ci/status/build/play.rb @@ -13,20 +13,12 @@ module Gitlab 'play' end - def icon - 'icon_status_skipped' - end - - def to_s - 'play' - end - def has_action?(current_user) can?(current_user, :update_build, subject) end def action_icon - :play + 'play' end def action_path diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb new file mode 100644 index 00000000000..b3c0eedadf8 --- /dev/null +++ b/lib/gitlab/ci/status/build/retryable.rb @@ -0,0 +1,31 @@ +module Gitlab + module Ci + module Status + module Status + class Retryable < SimpleDelegator + extend Status::Extended + + def has_action?(current_user) + can?(current_user, :update_build, subject) + end + + def action_icon + 'repeat' + end + + def action_path + retry_namespace_project_build_path(subject.project.namespace, subject.project, subject) + end + + def action_method + :post + end + + def self.matches?(build) + build.retryable? + end + end + end + end + end +end -- cgit v1.2.1 From 609bc0a0b67354d6e3df0eba11da1acde6a9d033 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 8 Dec 2016 10:52:44 +0100 Subject: Check permission of details --- lib/gitlab/ci/status/build/common.rb | 4 ++-- lib/gitlab/ci/status/core.rb | 2 +- lib/gitlab/ci/status/pipeline/common.rb | 4 ++-- lib/gitlab/ci/status/stage/common.rb | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/gitlab/ci/status/build/common.rb b/lib/gitlab/ci/status/build/common.rb index 3e47d7dfd20..2fb79afa3d3 100644 --- a/lib/gitlab/ci/status/build/common.rb +++ b/lib/gitlab/ci/status/build/common.rb @@ -3,8 +3,8 @@ module Gitlab module Status module Build module Common - def has_details? - true + def has_details?(current_user) + can?(current_user, :read_build, subject) end def details_path diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb index 60c559248aa..6b47096f811 100644 --- a/lib/gitlab/ci/status/core.rb +++ b/lib/gitlab/ci/status/core.rb @@ -33,7 +33,7 @@ module Gitlab self.class.name.demodulize.downcase.underscore end - def has_details? + def has_details?(_user = nil) false end diff --git a/lib/gitlab/ci/status/pipeline/common.rb b/lib/gitlab/ci/status/pipeline/common.rb index 25e52bec3da..5f79044a496 100644 --- a/lib/gitlab/ci/status/pipeline/common.rb +++ b/lib/gitlab/ci/status/pipeline/common.rb @@ -3,8 +3,8 @@ module Gitlab module Status module Pipeline module Common - def has_details? - true + def has_details?(current_user) + can?(current_user, :read_pipeline, subject) end def details_path diff --git a/lib/gitlab/ci/status/stage/common.rb b/lib/gitlab/ci/status/stage/common.rb index 14c437d2b98..e6ee2f92341 100644 --- a/lib/gitlab/ci/status/stage/common.rb +++ b/lib/gitlab/ci/status/stage/common.rb @@ -3,8 +3,8 @@ module Gitlab module Status module Stage module Common - def has_details? - true + def has_details?(current_user) + can?(current_user, :read_pipeline, subject) end def details_path -- cgit v1.2.1 From 65f3206024778b934171c9d9ece8ab627ba6c4c5 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 8 Dec 2016 11:03:01 +0100 Subject: Remove ci_status_with_icon helper and replace it with partial [ci skip] --- app/helpers/ci_status_helper.rb | 20 +------------------- app/views/admin/runners/show.html.haml | 2 +- app/views/ci/status/_icon_with_label.html.haml | 10 ++++++++++ app/views/projects/builds/_header.html.haml | 2 +- app/views/projects/ci/builds/_build.html.haml | 5 +---- app/views/projects/ci/pipelines/_pipeline.html.haml | 5 +---- .../_generic_commit_status.html.haml | 5 +---- app/views/projects/pipelines/_info.html.haml | 2 +- 8 files changed, 17 insertions(+), 34 deletions(-) create mode 100644 app/views/ci/status/_icon_with_label.html.haml diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 8e19752a8a1..d9f5e01f0dc 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -4,25 +4,7 @@ module CiStatusHelper builds_namespace_project_commit_path(project.namespace, project, pipeline.sha) end - def ci_status_with_icon(status, target = nil) - content = ci_icon_for_status(status) + ci_text_for_status(status) - klass = "ci-status ci-#{status}" - - if target - link_to content, target, class: klass - else - content_tag :span, content, class: klass - end - end - - def ci_text_for_status(status) - if detailed_status?(status) - status.text - else - status - end - end - + # Is used by Commit and Merge Request Widget def ci_label_for_status(status) if detailed_status?(status) return status.label diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 73038164056..fa8be25ffa8 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -91,7 +91,7 @@ %strong ##{build.id} %td.status - = ci_status_with_icon(build.status) + = render "ci/status/icon_with_label", subject: build %td.status - if project diff --git a/app/views/ci/status/_icon_with_label.html.haml b/app/views/ci/status/_icon_with_label.html.haml new file mode 100644 index 00000000000..65a74e88444 --- /dev/null +++ b/app/views/ci/status/_icon_with_label.html.haml @@ -0,0 +1,10 @@ +- details_path = subject.details_path if subject.has_details?(current_user) +- klass = "ci-status ci-#{subject.status}" +- if details_path + = link_to details_path, class: klass do + = custom_icon(status.icon) + = status.text +- else + %span{ class: klass } + = custom_icon(status.icon) + = status.text diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml index f6aa20c4579..5e4e30f08d5 100644 --- a/app/views/projects/builds/_header.html.haml +++ b/app/views/projects/builds/_header.html.haml @@ -1,6 +1,6 @@ .content-block.build-header .header-content - = ci_status_with_icon(@build.status) + = render "ci/status/icon_with_label", subject: build Build %strong ##{@build.id} in pipeline diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 18b3b04154f..6b0cd3e49a0 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -9,10 +9,7 @@ %tr.build.commit{class: ('retried' if retried)} %td.status - - if can?(current_user, :read_build, build) - = ci_status_with_icon(build.status, namespace_project_build_url(build.project.namespace, build.project, build)) - - else - = ci_status_with_icon(build.status) + = render "ci/status/icon_with_label", subject: build %td.branch-commit - if can?(current_user, :read_build, build) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index b58dceb58c9..84243e4306d 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -1,13 +1,10 @@ - status = pipeline.status -- detailed_status = pipeline.detailed_status - show_commit = local_assigns.fetch(:show_commit, true) - show_branch = local_assigns.fetch(:show_branch, true) %tr.commit %td.commit-link - = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "ci-status ci-#{detailed_status}" do - = ci_icon_for_status(detailed_status) - = ci_text_for_status(detailed_status) + = render "ci/status/icon_with_label", subject: pipeline %td = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index 7f751d9ae2e..69cb1631ee8 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -8,10 +8,7 @@ %tr.generic_commit_status{class: ('retried' if retried)} %td.status - - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url - = ci_status_with_icon(generic_commit_status.status, generic_commit_status.target_url) - - else - = ci_status_with_icon(generic_commit_status.status) + = render "ci/status/icon_with_label", subject: generic_commit_status %td.generic_commit_status-link - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 229bdfb0e8d..f7385184a2b 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -1,6 +1,6 @@ .page-content-header .header-main-content - = ci_status_with_icon(@pipeline.detailed_status) + = render "ci/status/icon_with_label", subject: @pipeline %strong Pipeline ##{@commit.pipelines.last.id} triggered #{time_ago_with_tooltip(@commit.authored_date)} by = author_avatar(@commit, size: 24) -- cgit v1.2.1 From fe447b43ac8a965b533c056f823d392f0c9db5ac Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <jacob@gitlab.com> Date: Thu, 8 Dec 2016 11:53:21 +0100 Subject: Use gitlab-workhose 1.1.1 Confines API rate limiting feature to builds/register.json CI requests only. --- GITLAB_WORKHORSE_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 9084fa2f716..524cb55242b 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -1.1.0 +1.1.1 -- cgit v1.2.1 From 89cc2064a2ebfe947d257c9c15c63825edc73bee Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Thu, 8 Dec 2016 14:41:10 +0200 Subject: Update documentation for BitBucket --- doc/integration/bitbucket.md | 99 ++------------------- .../img/bitbucket_oauth_settings_page.png | Bin 30081 -> 30275 bytes 2 files changed, 7 insertions(+), 92 deletions(-) diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index 9122dc62e39..9cdb101f457 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -44,14 +44,12 @@ you to use. And grant at least the following permissions: ``` - Account: Email - Repositories: Read, Admin + Account: Email, Read + Repositories: Read + Pull Requests: Read + Issues: Read ``` - >**Note:** - It may seem a little odd to giving GitLab admin permissions to repositories, - but this is needed in order for GitLab to be able to clone the repositories. - ![Bitbucket OAuth settings page](img/bitbucket_oauth_settings_page.png) 1. Select **Save**. @@ -93,7 +91,8 @@ you to use. ```yaml - { name: 'bitbucket', app_id: 'BITBUCKET_APP_KEY', - app_secret: 'BITBUCKET_APP_SECRET' } + app_secret: 'BITBUCKET_APP_SECRET', + url: 'https://bitbucket.org/' } ``` --- @@ -112,91 +111,7 @@ well, the user will be returned to GitLab and will be signed in. ## Bitbucket project import -To allow projects to be imported directly into GitLab, Bitbucket requires two -extra setup steps compared to [GitHub](github.md) and [GitLab.com](gitlab.md). - -Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and -instead requires GitLab to use SSH and identify itself using your GitLab -server's SSH key. - -To be able to access repositories on Bitbucket, GitLab will automatically -register your public key with Bitbucket as a deploy key for the repositories to -be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa` which -translates to `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages and to -`/home/git/.ssh/bitbucket_rsa` for installations from source. - ---- - -Below are the steps that will allow GitLab to be able to import your projects -from Bitbucket. - -1. Make sure you [have enabled the Bitbucket OAuth support](#bitbucket-omniauth-provider). -1. Create a new SSH key with an **empty passphrase**: - - ```sh - sudo -u git -H ssh-keygen - ``` - - When asked to 'Enter file in which to save the key' enter: - `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages or - `/home/git/.ssh/bitbucket_rsa` for installations from source. The name is - important so make sure to get it right. - - > **Warning:** - This key must NOT be associated with ANY existing Bitbucket accounts. If it - is, the import will fail with an `Access denied! Please verify you can add - deploy keys to this repository.` error. - -1. Next, you need to to configure the SSH client to use your new key. Open the - SSH configuration file of the `git` user: - - ``` - # For Omnibus packages - sudo editor /var/opt/gitlab/.ssh/config - - # For installations from source - sudo editor /home/git/.ssh/config - ``` - -1. Add a host configuration for `bitbucket.org`: - - ```sh - Host bitbucket.org - IdentityFile ~/.ssh/bitbucket_rsa - User git - ``` - -1. Save the file and exit. -1. Manually connect to `bitbucket.org` over SSH, while logged in as the `git` - user that GitLab will use: - - ```sh - sudo -u git -H ssh bitbucket.org - ``` - - That step is performed because GitLab needs to connect to Bitbucket over SSH, - in order to add `bitbucket.org` to your GitLab server's known SSH hosts. - -1. Verify the RSA key fingerprint you'll see in the response matches the one - in the [Bitbucket documentation][bitbucket-docs] (the specific IP address - doesn't matter): - - ```sh - The authenticity of host 'bitbucket.org (104.192.143.1)' can't be established. - RSA key fingerprint is SHA256:zzXQOXSRBEiUtuE8AikJYKwbHaxvSc0ojez9YXaGp1A. - Are you sure you want to continue connecting (yes/no)? - ``` - -1. If the fingerprint matches, type `yes` to continue connecting and have - `bitbucket.org` be added to your known SSH hosts. After confirming you should - see a permission denied message. If you see an authentication successful - message you have done something wrong. The key you are using has already been - added to a Bitbucket account and will cause the import script to fail. Ensure - the key you are using CANNOT authenticate with Bitbucket. -1. Restart GitLab to allow it to find the new public key. - -Your GitLab server is now able to connect to Bitbucket over SSH. You should be -able to see the "Import projects from Bitbucket" option on the New Project page +You should be able to see the "Import projects from Bitbucket" option on the New Project page enabled. ## Acknowledgements diff --git a/doc/integration/img/bitbucket_oauth_settings_page.png b/doc/integration/img/bitbucket_oauth_settings_page.png index 8dbee9762d7..24acc6e1f5a 100644 Binary files a/doc/integration/img/bitbucket_oauth_settings_page.png and b/doc/integration/img/bitbucket_oauth_settings_page.png differ -- cgit v1.2.1 From 3acf4323797346437f74e6bf5950fcac917ae56c Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Thu, 8 Dec 2016 13:51:06 +0100 Subject: Refactor ci status factories to DRY code a little --- lib/gitlab/ci/status/build/factory.rb | 11 +++++------ lib/gitlab/ci/status/extended.rb | 2 +- lib/gitlab/ci/status/factory.rb | 30 +++++++++++++++++------------- lib/gitlab/ci/status/pipeline/factory.rb | 8 +++----- lib/gitlab/ci/status/stage/factory.rb | 6 ++---- 5 files changed, 28 insertions(+), 29 deletions(-) diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb index 8f420a93954..eee9a64120b 100644 --- a/lib/gitlab/ci/status/build/factory.rb +++ b/lib/gitlab/ci/status/build/factory.rb @@ -3,14 +3,13 @@ module Gitlab module Status module Build class Factory < Status::Factory - private - - def extended_statuses - [Stop, Play, Cancelable, Retryable] + def self.extended_statuses + [Status::Build::Stop, Status::Build::Play, + Status::Build::Cancelable, Status::Build::Retryable] end - def core_status - super.extend(Status::Build::Common) + def self.common_helpers + Status::Build::Common end end end diff --git a/lib/gitlab/ci/status/extended.rb b/lib/gitlab/ci/status/extended.rb index 6bfb5d38c1f..93e6eff1c94 100644 --- a/lib/gitlab/ci/status/extended.rb +++ b/lib/gitlab/ci/status/extended.rb @@ -2,7 +2,7 @@ module Gitlab module Ci module Status module Extended - def matches?(_subject) + def matches?(_subject, _user) raise NotImplementedError end end diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb index b2f896f2211..944e0fdde2d 100644 --- a/lib/gitlab/ci/status/factory.rb +++ b/lib/gitlab/ci/status/factory.rb @@ -2,10 +2,9 @@ module Gitlab module Ci module Status class Factory - attr_reader :subject - - def initialize(subject) + def initialize(subject, user = nil) @subject = subject + @user = user end def fabricate! @@ -16,27 +15,32 @@ module Gitlab end end + def self.extended_statuses + [] + end + + def self.common_helpers + Module.new + end + private - def subject_status - @subject_status ||= subject.status + def simple_status + @simple_status ||= @subject.status || :created end def core_status Gitlab::Ci::Status - .const_get(subject_status.capitalize) - .new(subject) + .const_get(simple_status.capitalize) + .new(@subject) + .extend(self.class.common_helpers) end def extended_status - @extended ||= extended_statuses.find do |status| - status.matches?(subject) + @extended ||= self.class.extended_statuses.find do |status| + status.matches?(@subject, @user) end end - - def extended_statuses - [] - end end end end diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb index 4ac4ec671d0..16dcb326be9 100644 --- a/lib/gitlab/ci/status/pipeline/factory.rb +++ b/lib/gitlab/ci/status/pipeline/factory.rb @@ -3,14 +3,12 @@ module Gitlab module Status module Pipeline class Factory < Status::Factory - private - - def extended_statuses + def self.extended_statuses [Pipeline::SuccessWithWarnings] end - def core_status - super.extend(Status::Pipeline::Common) + def self.common_helpers + Status::Pipeline::Common end end end diff --git a/lib/gitlab/ci/status/stage/factory.rb b/lib/gitlab/ci/status/stage/factory.rb index c6522d5ada1..689a5dd45bc 100644 --- a/lib/gitlab/ci/status/stage/factory.rb +++ b/lib/gitlab/ci/status/stage/factory.rb @@ -3,10 +3,8 @@ module Gitlab module Status module Stage class Factory < Status::Factory - private - - def core_status - super.extend(Status::Stage::Common) + def self.common_helpers + Status::Stage::Common end end end -- cgit v1.2.1 From 9c0a8cb5b6311c217f7c5c98721da78eae09391f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Thu, 8 Dec 2016 14:28:49 +0100 Subject: Incorporate permission checks into new CI statuses [ci skip] --- lib/gitlab/ci/status/build/cancelable.rb | 12 +++++++----- lib/gitlab/ci/status/build/common.rb | 10 +++++----- lib/gitlab/ci/status/build/play.rb | 12 +++++++----- lib/gitlab/ci/status/build/retryable.rb | 12 +++++++----- lib/gitlab/ci/status/build/stop.rb | 12 +++++++----- lib/gitlab/ci/status/core.rb | 13 ++++++------- lib/gitlab/ci/status/extended.rb | 8 ++++++-- lib/gitlab/ci/status/factory.rb | 4 ++-- lib/gitlab/ci/status/pipeline/common.rb | 10 +++++----- lib/gitlab/ci/status/pipeline/success_with_warnings.rb | 4 ++-- lib/gitlab/ci/status/stage/common.rb | 12 ++++++------ 11 files changed, 60 insertions(+), 49 deletions(-) diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb index bff0464ef0c..a8830b04715 100644 --- a/lib/gitlab/ci/status/build/cancelable.rb +++ b/lib/gitlab/ci/status/build/cancelable.rb @@ -3,10 +3,10 @@ module Gitlab module Status module Status class Cancelable < SimpleDelegator - extend Status::Extended + include Status::Extended - def has_action?(current_user) - can?(current_user, :update_build, subject) + def has_action? + can?(user, :update_build, subject) end def action_icon @@ -14,14 +14,16 @@ module Gitlab end def action_path - cancel_namespace_project_build_path(subject.project.namespace, subject.project, subject) + cancel_namespace_project_build_path(subject.project.namespace, + subject.project, + subject) end def action_method :post end - def self.matches?(build) + def self.matches?(build, user) build.cancelable? end end diff --git a/lib/gitlab/ci/status/build/common.rb b/lib/gitlab/ci/status/build/common.rb index 2fb79afa3d3..2b602f1e247 100644 --- a/lib/gitlab/ci/status/build/common.rb +++ b/lib/gitlab/ci/status/build/common.rb @@ -3,14 +3,14 @@ module Gitlab module Status module Build module Common - def has_details?(current_user) - can?(current_user, :read_build, subject) + def has_details? + can?(user, :read_build, subject) end def details_path - namespace_project_build_path(@subject.project.namespace, - @subject.project, - @subject.pipeline) + namespace_project_build_path(subject.project.namespace, + subject.project, + subject.pipeline) end end end diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb index d295850137b..70c08197ea1 100644 --- a/lib/gitlab/ci/status/build/play.rb +++ b/lib/gitlab/ci/status/build/play.rb @@ -3,7 +3,7 @@ module Gitlab module Status module Status class Play < SimpleDelegator - extend Status::Extended + include Status::Extended def text 'play' @@ -13,8 +13,8 @@ module Gitlab 'play' end - def has_action?(current_user) - can?(current_user, :update_build, subject) + def has_action? + can?(user, :update_build, subject) end def action_icon @@ -22,14 +22,16 @@ module Gitlab end def action_path - play_namespace_project_build_path(subject.project.namespace, subject.project, subject) + play_namespace_project_build_path(subject.project.namespace, + subject.project, + subject) end def action_method :post end - def self.matches?(build) + def self.matches?(build, user) build.playable? && !build.stops_environment? end end diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb index b3c0eedadf8..3309e8808e1 100644 --- a/lib/gitlab/ci/status/build/retryable.rb +++ b/lib/gitlab/ci/status/build/retryable.rb @@ -3,10 +3,10 @@ module Gitlab module Status module Status class Retryable < SimpleDelegator - extend Status::Extended + include Status::Extended - def has_action?(current_user) - can?(current_user, :update_build, subject) + def has_action? + can?(user, :update_build, subject) end def action_icon @@ -14,14 +14,16 @@ module Gitlab end def action_path - retry_namespace_project_build_path(subject.project.namespace, subject.project, subject) + retry_namespace_project_build_path(subject.project.namespace, + subject.project, + subject) end def action_method :post end - def self.matches?(build) + def self.matches?(build, user) build.retryable? end end diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb index 261de9695c5..6fb51890bec 100644 --- a/lib/gitlab/ci/status/build/stop.rb +++ b/lib/gitlab/ci/status/build/stop.rb @@ -3,7 +3,7 @@ module Gitlab module Status module Status class Play < SimpleDelegator - extend Status::Extended + include Status::Extended def text 'stop' @@ -21,8 +21,8 @@ module Gitlab 'stop' end - def has_action?(current_user) - can?(current_user, :update_build, subject) + def has_action? + can?(user, :update_build, subject) end def action_icon @@ -30,14 +30,16 @@ module Gitlab end def action_path - play_namespace_project_build_path(subject.project.namespace, subject.project, subject) + play_namespace_project_build_path(subject.project.namespace, + subject.project, + subject) end def action_method :post end - def self.matches?(build) + def self.matches?(build, user) build.playable? && build.stops_environment? end end diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb index 6b47096f811..df371363736 100644 --- a/lib/gitlab/ci/status/core.rb +++ b/lib/gitlab/ci/status/core.rb @@ -6,8 +6,11 @@ module Gitlab class Core include Gitlab::Routing.url_helpers - def initialize(subject) + attr_reader :subject, :user + + def initialize(subject, user) @subject = subject + @user = user end def icon @@ -18,10 +21,6 @@ module Gitlab raise NotImplementedError end - def title - "#{@subject.class.name.demodulize}: #{label}" - end - # Deprecation warning: this method is here because we need to maintain # backwards compatibility with legacy statuses. We often do something # like "ci-status ci-status-#{status}" to set CSS class. @@ -33,7 +32,7 @@ module Gitlab self.class.name.demodulize.downcase.underscore end - def has_details?(_user = nil) + def has_details? false end @@ -41,7 +40,7 @@ module Gitlab raise NotImplementedError end - def has_action?(_user = nil) + def has_action? false end diff --git a/lib/gitlab/ci/status/extended.rb b/lib/gitlab/ci/status/extended.rb index 93e6eff1c94..d367c9bda69 100644 --- a/lib/gitlab/ci/status/extended.rb +++ b/lib/gitlab/ci/status/extended.rb @@ -2,8 +2,12 @@ module Gitlab module Ci module Status module Extended - def matches?(_subject, _user) - raise NotImplementedError + extend ActiveSupport::Concern + + class_methods do + def matches?(_subject, _user) + raise NotImplementedError + end end end end diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb index 944e0fdde2d..ae9ef895df4 100644 --- a/lib/gitlab/ci/status/factory.rb +++ b/lib/gitlab/ci/status/factory.rb @@ -2,7 +2,7 @@ module Gitlab module Ci module Status class Factory - def initialize(subject, user = nil) + def initialize(subject, user) @subject = subject @user = user end @@ -32,7 +32,7 @@ module Gitlab def core_status Gitlab::Ci::Status .const_get(simple_status.capitalize) - .new(@subject) + .new(@subject, @user) .extend(self.class.common_helpers) end diff --git a/lib/gitlab/ci/status/pipeline/common.rb b/lib/gitlab/ci/status/pipeline/common.rb index 5f79044a496..76bfd18bf40 100644 --- a/lib/gitlab/ci/status/pipeline/common.rb +++ b/lib/gitlab/ci/status/pipeline/common.rb @@ -3,14 +3,14 @@ module Gitlab module Status module Pipeline module Common - def has_details?(current_user) - can?(current_user, :read_pipeline, subject) + def has_details? + can?(user, :read_pipeline, subject) end def details_path - namespace_project_pipeline_path(@subject.project.namespace, - @subject.project, - @subject) + namespace_project_pipeline_path(subject.project.namespace, + subject.project, + subject) end def has_action? diff --git a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb index 4b040d60df8..a7c98f9e909 100644 --- a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb +++ b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb @@ -3,7 +3,7 @@ module Gitlab module Status module Pipeline class SuccessWithWarnings < SimpleDelegator - extend Status::Extended + include Status::Extended def text 'passed' @@ -21,7 +21,7 @@ module Gitlab 'success_with_warnings' end - def self.matches?(pipeline) + def self.matches?(pipeline, user) pipeline.success? && pipeline.has_warnings? end end diff --git a/lib/gitlab/ci/status/stage/common.rb b/lib/gitlab/ci/status/stage/common.rb index e6ee2f92341..6851ceda317 100644 --- a/lib/gitlab/ci/status/stage/common.rb +++ b/lib/gitlab/ci/status/stage/common.rb @@ -3,15 +3,15 @@ module Gitlab module Status module Stage module Common - def has_details?(current_user) - can?(current_user, :read_pipeline, subject) + def has_details? + can?(user, :read_pipeline, subject) end def details_path - namespace_project_pipeline_path(@subject.project.namespace, - @subject.project, - @subject.pipeline, - anchor: @subject.name) + namespace_project_pipeline_path(subject.project.namespace, + subject.project, + subject.pipeline, + anchor: subject.name) end def has_action? -- cgit v1.2.1 From fd87bf3c2c4d3064a1a4eb1867d9a093172fed6e Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra <dimitriehoekstra@gmail.com> Date: Thu, 8 Dec 2016 14:35:17 +0100 Subject: Various small emoji positioning adjustments --- app/assets/stylesheets/framework/awards.scss | 5 +++-- app/assets/stylesheets/framework/common.scss | 1 + app/assets/stylesheets/pages/notes.scss | 1 - changelogs/unreleased/small-emoji-adjustments.yml | 4 ++++ 4 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/small-emoji-adjustments.yml diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index c13cb4a02b2..dece5c3202b 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -1,7 +1,7 @@ .awards { .emoji-icon { - width: 19px; - height: 19px; + width: 20px; + height: 20px; } } @@ -136,5 +136,6 @@ .award-control-icon { color: $award-emoji-new-btn-icon-color; + margin-top: 1px; } } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 600bf17259b..251e43d2edd 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -255,6 +255,7 @@ img.emoji { height: 20px; vertical-align: top; width: 20px; + margin-top: 1px; } .chart { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 16b099c09eb..30f41a37ab3 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -413,7 +413,6 @@ ul.notes { .fa { color: $notes-action-color; position: relative; - top: 1px; font-size: 17px; } diff --git a/changelogs/unreleased/small-emoji-adjustments.yml b/changelogs/unreleased/small-emoji-adjustments.yml new file mode 100644 index 00000000000..804bd05b613 --- /dev/null +++ b/changelogs/unreleased/small-emoji-adjustments.yml @@ -0,0 +1,4 @@ +--- +title: Various small emoji positioning adjustments +merge_request: +author: -- cgit v1.2.1 From 7d88399454b5c71c9af84561c858001d1a733c41 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Thu, 8 Dec 2016 14:40:21 +0100 Subject: Fix tests related to detailed statuses and permissions [ci skip] --- spec/lib/gitlab/ci/status/canceled_spec.rb | 4 +++- spec/lib/gitlab/ci/status/created_spec.rb | 8 +++----- spec/lib/gitlab/ci/status/extended_spec.rb | 2 +- spec/lib/gitlab/ci/status/factory_spec.rb | 6 ++++-- spec/lib/gitlab/ci/status/failed_spec.rb | 8 +++----- spec/lib/gitlab/ci/status/pending_spec.rb | 8 +++----- spec/lib/gitlab/ci/status/pipeline/common_spec.rb | 4 +++- spec/lib/gitlab/ci/status/pipeline/factory_spec.rb | 4 +++- spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb | 2 +- spec/lib/gitlab/ci/status/running_spec.rb | 8 +++----- spec/lib/gitlab/ci/status/skipped_spec.rb | 8 +++----- spec/lib/gitlab/ci/status/stage/common_spec.rb | 8 ++++++-- spec/lib/gitlab/ci/status/stage/factory_spec.rb | 8 ++++++-- spec/lib/gitlab/ci/status/success_spec.rb | 8 +++----- 14 files changed, 45 insertions(+), 41 deletions(-) diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb index 619ecbcba67..eaf974bb953 100644 --- a/spec/lib/gitlab/ci/status/canceled_spec.rb +++ b/spec/lib/gitlab/ci/status/canceled_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Canceled do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'canceled' } diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb index 157302c65a8..2ce176a29d6 100644 --- a/spec/lib/gitlab/ci/status/created_spec.rb +++ b/spec/lib/gitlab/ci/status/created_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Created do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'created' } @@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Created do describe '#icon' do it { expect(subject.icon).to eq 'icon_status_created' } end - - describe '#title' do - it { expect(subject.title).to eq 'Double: created' } - end end diff --git a/spec/lib/gitlab/ci/status/extended_spec.rb b/spec/lib/gitlab/ci/status/extended_spec.rb index 120e121aae5..864121dec4b 100644 --- a/spec/lib/gitlab/ci/status/extended_spec.rb +++ b/spec/lib/gitlab/ci/status/extended_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Extended do end it 'requires subclass to implement matcher' do - expect { subject.matches?(double) } + expect { subject.matches?(double, double) } .to raise_error(NotImplementedError) end end diff --git a/spec/lib/gitlab/ci/status/factory_spec.rb b/spec/lib/gitlab/ci/status/factory_spec.rb index d5bd7f7102b..f92a1c149bf 100644 --- a/spec/lib/gitlab/ci/status/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/factory_spec.rb @@ -2,15 +2,17 @@ require 'spec_helper' describe Gitlab::Ci::Status::Factory do subject do - described_class.new(object) + described_class.new(resource, user) end + let(:user) { create(:user) } + let(:status) { subject.fabricate! } context 'when object has a core status' do HasStatus::AVAILABLE_STATUSES.each do |core_status| context "when core status is #{core_status}" do - let(:object) { double(status: core_status) } + let(:resource) { double(status: core_status) } it "fabricates a core status #{core_status}" do expect(status).to be_a( diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb index 0b3cb8168e6..9d527e6a7ef 100644 --- a/spec/lib/gitlab/ci/status/failed_spec.rb +++ b/spec/lib/gitlab/ci/status/failed_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Failed do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'failed' } @@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Failed do describe '#icon' do it { expect(subject.icon).to eq 'icon_status_failed' } end - - describe '#title' do - it { expect(subject.title).to eq 'Double: failed' } - end end diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb index 57c901c1202..d03f595d3c7 100644 --- a/spec/lib/gitlab/ci/status/pending_spec.rb +++ b/spec/lib/gitlab/ci/status/pending_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Pending do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'pending' } @@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Pending do describe '#icon' do it { expect(subject.icon).to eq 'icon_status_pending' } end - - describe '#title' do - it { expect(subject.title).to eq 'Double: pending' } - end end diff --git a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb index 21adee3f8e7..4f32ae5d809 100644 --- a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb @@ -1,11 +1,13 @@ require 'spec_helper' describe Gitlab::Ci::Status::Pipeline::Common do + let(:user) { create(:user) } let(:pipeline) { create(:ci_pipeline) } subject do Class.new(Gitlab::Ci::Status::Core) - .new(pipeline).extend(described_class) + .new(pipeline, user) + .extend(described_class) end it 'does not have action' do diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb index d6243940f2e..c6b2582652d 100644 --- a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb @@ -1,8 +1,10 @@ require 'spec_helper' describe Gitlab::Ci::Status::Pipeline::Factory do + let(:user) { create(:user) } + subject do - described_class.new(pipeline) + described_class.new(pipeline, user) end let(:status) do diff --git a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb index 02e526e3de2..634f80088d5 100644 --- a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do subject do - described_class.new(double('status')) + described_class.new(double('status'), double('user')) end describe '#test' do diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb index c023f1872cc..9f47090d396 100644 --- a/spec/lib/gitlab/ci/status/running_spec.rb +++ b/spec/lib/gitlab/ci/status/running_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Running do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'running' } @@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Running do describe '#icon' do it { expect(subject.icon).to eq 'icon_status_running' } end - - describe '#title' do - it { expect(subject.title).to eq 'Double: running' } - end end diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb index d4f7f4b3b70..94601648a8d 100644 --- a/spec/lib/gitlab/ci/status/skipped_spec.rb +++ b/spec/lib/gitlab/ci/status/skipped_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Skipped do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'skipped' } @@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Skipped do describe '#icon' do it { expect(subject.icon).to eq 'icon_status_skipped' } end - - describe '#title' do - it { expect(subject.title).to eq 'Double: skipped' } - end end diff --git a/spec/lib/gitlab/ci/status/stage/common_spec.rb b/spec/lib/gitlab/ci/status/stage/common_spec.rb index f3259c6f23e..9b7e6777dc1 100644 --- a/spec/lib/gitlab/ci/status/stage/common_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/common_spec.rb @@ -1,12 +1,16 @@ require 'spec_helper' describe Gitlab::Ci::Status::Stage::Common do + let(:user) { create(:user) } let(:pipeline) { create(:ci_empty_pipeline) } - let(:stage) { build(:ci_stage, pipeline: pipeline, name: 'test') } + + let(:stage) do + build(:ci_stage, pipeline: pipeline, name: 'test') + end subject do Class.new(Gitlab::Ci::Status::Core) - .new(stage).extend(described_class) + .new(stage, user).extend(described_class) end it 'does not have action' do diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb index 17929665c83..5a281564415 100644 --- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb @@ -1,11 +1,15 @@ require 'spec_helper' describe Gitlab::Ci::Status::Stage::Factory do + let(:user) { create(:user) } let(:pipeline) { create(:ci_empty_pipeline) } - let(:stage) { build(:ci_stage, pipeline: pipeline, name: 'test') } + + let(:stage) do + build(:ci_stage, pipeline: pipeline, name: 'test') + end subject do - described_class.new(stage) + described_class.new(stage, user) end let(:status) do diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb index 9e261a3aa5f..90f9f615e0d 100644 --- a/spec/lib/gitlab/ci/status/success_spec.rb +++ b/spec/lib/gitlab/ci/status/success_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Success do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'passed' } @@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Success do describe '#icon' do it { expect(subject.icon).to eq 'icon_status_success' } end - - describe '#title' do - it { expect(subject.title).to eq 'Double: passed' } - end end -- cgit v1.2.1 From c1db5b91207f4e3f7144c5cb62ce9160cf2e32e9 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Thu, 8 Dec 2016 14:51:38 +0100 Subject: Fix some detailed statuses specs related to abilities --- app/models/ability.rb | 6 ++++++ lib/gitlab/ci/status/core.rb | 1 + lib/gitlab/ci/status/stage/common.rb | 2 +- spec/lib/gitlab/ci/status/canceled_spec.rb | 4 ---- spec/lib/gitlab/ci/status/extended_spec.rb | 2 +- spec/lib/gitlab/ci/status/pipeline/common_spec.rb | 7 ++++++- spec/lib/gitlab/ci/status/pipeline/factory_spec.rb | 5 +++++ .../status/pipeline/success_with_warnings_spec.rb | 10 +++++----- spec/lib/gitlab/ci/status/stage/common_spec.rb | 23 +++++++++++++++++----- spec/lib/gitlab/ci/status/stage/factory_spec.rb | 7 ++++++- 10 files changed, 49 insertions(+), 18 deletions(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index fa8f8bc3a5f..ce461caf686 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -1,4 +1,10 @@ class Ability + module Allowable + def can?(user, action, subject) + Ability.allowed?(user, action, subject) + end + end + class << self # Given a list of users and a project this method returns the users that can # read the given project. diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb index df371363736..7e9f6e35012 100644 --- a/lib/gitlab/ci/status/core.rb +++ b/lib/gitlab/ci/status/core.rb @@ -5,6 +5,7 @@ module Gitlab # class Core include Gitlab::Routing.url_helpers + include Ability::Allowable attr_reader :subject, :user diff --git a/lib/gitlab/ci/status/stage/common.rb b/lib/gitlab/ci/status/stage/common.rb index 6851ceda317..7852f492e1d 100644 --- a/lib/gitlab/ci/status/stage/common.rb +++ b/lib/gitlab/ci/status/stage/common.rb @@ -4,7 +4,7 @@ module Gitlab module Stage module Common def has_details? - can?(user, :read_pipeline, subject) + can?(user, :read_pipeline, subject.pipeline) end def details_path diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb index eaf974bb953..4639278ad45 100644 --- a/spec/lib/gitlab/ci/status/canceled_spec.rb +++ b/spec/lib/gitlab/ci/status/canceled_spec.rb @@ -16,8 +16,4 @@ describe Gitlab::Ci::Status::Canceled do describe '#icon' do it { expect(subject.icon).to eq 'icon_status_canceled' } end - - describe '#title' do - it { expect(subject.title).to eq 'Double: canceled' } - end end diff --git a/spec/lib/gitlab/ci/status/extended_spec.rb b/spec/lib/gitlab/ci/status/extended_spec.rb index 864121dec4b..c2d74ca5cde 100644 --- a/spec/lib/gitlab/ci/status/extended_spec.rb +++ b/spec/lib/gitlab/ci/status/extended_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::Ci::Status::Extended do subject do - Class.new.extend(described_class) + Class.new.include(described_class) end it 'requires subclass to implement matcher' do diff --git a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb index 4f32ae5d809..2df9d574677 100644 --- a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' describe Gitlab::Ci::Status::Pipeline::Common do let(:user) { create(:user) } - let(:pipeline) { create(:ci_pipeline) } + let(:project) { create(:empty_project) } + let(:pipeline) { create(:ci_pipeline, project: project) } subject do Class.new(Gitlab::Ci::Status::Core) @@ -10,6 +11,10 @@ describe Gitlab::Ci::Status::Pipeline::Common do .extend(described_class) end + before do + project.team << [user, :developer] + end + it 'does not have action' do expect(subject).not_to have_action end diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb index c6b2582652d..d4a2dc7fcc1 100644 --- a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe Gitlab::Ci::Status::Pipeline::Factory do let(:user) { create(:user) } + let(:project) { pipeline.project } subject do described_class.new(pipeline, user) @@ -11,6 +12,10 @@ describe Gitlab::Ci::Status::Pipeline::Factory do subject.fabricate! end + before do + project.team << [user, :developer] + end + context 'when pipeline has a core status' do HasStatus::AVAILABLE_STATUSES.each do |core_status| context "when core status is #{core_status}" do diff --git a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb index 634f80088d5..7e3383c307f 100644 --- a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do subject do - described_class.new(double('status'), double('user')) + described_class.new(double('status')) end describe '#test' do @@ -29,13 +29,13 @@ describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do end it 'is a correct match' do - expect(described_class.matches?(pipeline)).to eq true + expect(described_class.matches?(pipeline, double)).to eq true end end context 'when pipeline does not have warnings' do it 'does not match' do - expect(described_class.matches?(pipeline)).to eq false + expect(described_class.matches?(pipeline, double)).to eq false end end end @@ -51,13 +51,13 @@ describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do end it 'does not match' do - expect(described_class.matches?(pipeline)).to eq false + expect(described_class.matches?(pipeline, double)).to eq false end end context 'when pipeline does not have warnings' do it 'does not match' do - expect(described_class.matches?(pipeline)).to eq false + expect(described_class.matches?(pipeline, double)).to eq false end end end diff --git a/spec/lib/gitlab/ci/status/stage/common_spec.rb b/spec/lib/gitlab/ci/status/stage/common_spec.rb index 9b7e6777dc1..8814a7614a0 100644 --- a/spec/lib/gitlab/ci/status/stage/common_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/common_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' describe Gitlab::Ci::Status::Stage::Common do let(:user) { create(:user) } - let(:pipeline) { create(:ci_empty_pipeline) } + let(:project) { create(:empty_project) } + let(:pipeline) { create(:ci_empty_pipeline, project: project) } let(:stage) do build(:ci_stage, pipeline: pipeline, name: 'test') @@ -17,14 +18,26 @@ describe Gitlab::Ci::Status::Stage::Common do expect(subject).not_to have_action end - it 'has details' do - expect(subject).to have_details - end - it 'links to the pipeline details page' do expect(subject.details_path) .to include "pipelines/#{pipeline.id}" expect(subject.details_path) .to include "##{stage.name}" end + + context 'when user has permission to read pipeline' do + before do + project.team << [user, :master] + end + + it 'has details' do + expect(subject).to have_details + end + end + + context 'when user does not have permission to read pipeline' do + it 'does not have details' do + expect(subject).not_to have_details + end + end end diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb index 5a281564415..6f8721d30c2 100644 --- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' describe Gitlab::Ci::Status::Stage::Factory do let(:user) { create(:user) } - let(:pipeline) { create(:ci_empty_pipeline) } + let(:project) { create(:empty_project) } + let(:pipeline) { create(:ci_empty_pipeline, project: project) } let(:stage) do build(:ci_stage, pipeline: pipeline, name: 'test') @@ -16,6 +17,10 @@ describe Gitlab::Ci::Status::Stage::Factory do subject.fabricate! end + before do + project.team << [user, :developer] + end + context 'when stage has a core status' do HasStatus::AVAILABLE_STATUSES.each do |core_status| context "when core status is #{core_status}" do -- cgit v1.2.1 From aba894b94fd0f602781d2438c5419cfa2aa0cfaa Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 8 Dec 2016 15:48:46 +0100 Subject: Don't check if stage name doesn't exist --- app/views/projects/stage/_graph.html.haml | 3 +-- app/views/projects/stage/_stage.html.haml | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml index d8c87fae5a1..1d8fa10db0c 100644 --- a/app/views/projects/stage/_graph.html.haml +++ b/app/views/projects/stage/_graph.html.haml @@ -4,8 +4,7 @@ %li.stage-column .stage-name %a{ name: stage.name } - - if stage.name - = stage.name.titleize + = stage.name.titleize .builds-container %ul - status_groups.each do |group_name, grouped_statuses| diff --git a/app/views/projects/stage/_stage.html.haml b/app/views/projects/stage/_stage.html.haml index 1eca375db3d..1684e02fbad 100644 --- a/app/views/projects/stage/_stage.html.haml +++ b/app/views/projects/stage/_stage.html.haml @@ -4,9 +4,8 @@ %a{ name: stage.name } %span{class: "ci-status-link ci-status-icon-#{stage.status}"} = ci_icon_for_status(stage.status) - - if stage.name -   - = stage.name.titleize +   + = stage.name.titleize = render stage.statuses.latest_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, allow_retry: true = render stage.statuses.retried_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, retried: true %tr -- cgit v1.2.1 From e374ab7b42967975f783098c6b825a6bca0eda56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= <alejorro70@gmail.com> Date: Thu, 8 Dec 2016 13:21:21 -0300 Subject: Update CHANGELOG.md for 8.14.4 [ci skip] --- CHANGELOG.md | 13 +++++++++++++ changelogs/unreleased/23696-fix-diff-view-highlighting.yml | 4 ---- .../unreleased/24537-reenable-private-token-with-sudo.yml | 5 ----- changelogs/unreleased/24814-pipeline-tabs.yml | 4 ---- .../fix-authorize-users-into-imported-gitlab-project.yml | 4 ---- .../fix-compatibility-with-ie11-for-merge-requests.yml | 4 ---- changelogs/unreleased/fix-slack-pipeline-event.yml | 4 ---- .../unreleased/remove-has-visible-content-caching.yml | 4 ---- 8 files changed, 13 insertions(+), 29 deletions(-) delete mode 100644 changelogs/unreleased/23696-fix-diff-view-highlighting.yml delete mode 100644 changelogs/unreleased/24537-reenable-private-token-with-sudo.yml delete mode 100644 changelogs/unreleased/24814-pipeline-tabs.yml delete mode 100644 changelogs/unreleased/fix-authorize-users-into-imported-gitlab-project.yml delete mode 100644 changelogs/unreleased/fix-compatibility-with-ie11-for-merge-requests.yml delete mode 100644 changelogs/unreleased/fix-slack-pipeline-event.yml delete mode 100644 changelogs/unreleased/remove-has-visible-content-caching.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index e03123111c3..82fbdf89a68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 8.14.4 (2016-12-08) + +- Fix diff view permalink highlighting. !7090 +- Fix pipeline author for Slack and use pipeline id for pipeline link. !7506 +- Fix compatibility with Internet Explorer 11 for merge requests. !7525 (Steffen Rauh) +- Reenables /user API request to return private-token if user is admin and request is made with sudo. !7615 +- Fix Cicking on tabs on pipeline page should set URL. !7709 +- Authorize users into imported GitLab project. +- Destroy a user's session when they delete their own account. +- Don't accidentally mark unsafe diff lines as HTML safe. +- Replace MR access checks with use of MergeRequestsFinder. +- Remove visible content caching. + ## 8.14.3 (2016-12-02) - Pass commit data to ProcessCommitWorker to reduce Git overhead. !7744 diff --git a/changelogs/unreleased/23696-fix-diff-view-highlighting.yml b/changelogs/unreleased/23696-fix-diff-view-highlighting.yml deleted file mode 100644 index db523caffed..00000000000 --- a/changelogs/unreleased/23696-fix-diff-view-highlighting.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix diff view permalink highlighting -merge_request: 7090 -author: diff --git a/changelogs/unreleased/24537-reenable-private-token-with-sudo.yml b/changelogs/unreleased/24537-reenable-private-token-with-sudo.yml deleted file mode 100644 index 9fbbaeb914d..00000000000 --- a/changelogs/unreleased/24537-reenable-private-token-with-sudo.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Reenables /user API request to return private-token if user is admin and request - is made with sudo -merge_request: 7615 -author: diff --git a/changelogs/unreleased/24814-pipeline-tabs.yml b/changelogs/unreleased/24814-pipeline-tabs.yml deleted file mode 100644 index f85e7576905..00000000000 --- a/changelogs/unreleased/24814-pipeline-tabs.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix Cicking on tabs on pipeline page should set URL -merge_request: 7709 -author: diff --git a/changelogs/unreleased/fix-authorize-users-into-imported-gitlab-project.yml b/changelogs/unreleased/fix-authorize-users-into-imported-gitlab-project.yml deleted file mode 100644 index 9f14463fdd1..00000000000 --- a/changelogs/unreleased/fix-authorize-users-into-imported-gitlab-project.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Authorize users into imported GitLab project -merge_request: -author: diff --git a/changelogs/unreleased/fix-compatibility-with-ie11-for-merge-requests.yml b/changelogs/unreleased/fix-compatibility-with-ie11-for-merge-requests.yml deleted file mode 100644 index db92e45d8f1..00000000000 --- a/changelogs/unreleased/fix-compatibility-with-ie11-for-merge-requests.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix compatibility with Internet Explorer 11 for merge requests -merge_request: 7525 -author: Steffen Rauh diff --git a/changelogs/unreleased/fix-slack-pipeline-event.yml b/changelogs/unreleased/fix-slack-pipeline-event.yml deleted file mode 100644 index fec864eeb3d..00000000000 --- a/changelogs/unreleased/fix-slack-pipeline-event.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix pipeline author for Slack and use pipeline id for pipeline link -merge_request: 7506 -author: diff --git a/changelogs/unreleased/remove-has-visible-content-caching.yml b/changelogs/unreleased/remove-has-visible-content-caching.yml deleted file mode 100644 index e2940c60443..00000000000 --- a/changelogs/unreleased/remove-has-visible-content-caching.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove visible content caching -merge_request: -author: -- cgit v1.2.1 From 55da922b86ed81f967b6fcac618171f61b4fb2c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= <alejorro70@gmail.com> Date: Thu, 8 Dec 2016 13:28:50 -0300 Subject: Update CHANGELOG.md for 8.13.9 [ci skip] --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82fbdf89a68..9f0e29a5d82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -264,6 +264,11 @@ entry. - Fix "Without projects" filter. !6611 (Ben Bodenmiller) - Fix 404 when visit /projects page +## 8.13.9 (2016-12-08) + +- Reenables /user API request to return private-token if user is admin and request is made with sudo. !7615 +- Replace MR access checks with use of MergeRequestsFinder. + ## 8.13.8 (2016-12-02) - Pass tag SHA to post-receive hook when tag is created via UI. !7700 -- cgit v1.2.1 From 5b89ec0dbf76784ccc1e41b66498dd0dc4f05042 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 8 Dec 2016 17:52:24 +0100 Subject: Fix auto loading of constants for Ci Statuses --- app/models/ci/build.rb | 6 +++--- app/models/ci/pipeline.rb | 4 ++-- app/models/ci/stage.rb | 4 ++-- app/models/commit_status.rb | 4 ++-- app/views/ci/status/_icon_with_label.html.haml | 13 +++++++------ lib/gitlab/ci/status/build/cancelable.rb | 2 +- lib/gitlab/ci/status/build/play.rb | 2 +- lib/gitlab/ci/status/build/retryable.rb | 2 +- lib/gitlab/ci/status/build/stop.rb | 4 ++-- 9 files changed, 21 insertions(+), 20 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 73564dd2aa0..65ee327a8e5 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -100,8 +100,8 @@ module Ci end end - def detailed_status - Gitlab::Ci::Status::Build::Factory.new(self).fabricate! + def detailed_status(current_user) + Gitlab::Ci::Status::Build::Factory.new(self, current_user).fabricate! end def manual? @@ -156,7 +156,7 @@ module Ci end def environment_action - self.options.fetch(:environment, {}).fetch(:action, 'start') + self.options.fetch(:environment, {}).fetch(:action, 'start') if self.options end def outdated_deployment? diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index fda8228a1e9..1f33106d358 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -336,8 +336,8 @@ module Ci .select { |merge_request| merge_request.head_pipeline.try(:id) == self.id } end - def detailed_status - Gitlab::Ci::Status::Pipeline::Factory.new(self).fabricate! + def detailed_status(current_user) + Gitlab::Ci::Status::Pipeline::Factory.new(self, current_user).fabricate! end private diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index d2a37c0a827..be52cce20f1 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -22,8 +22,8 @@ module Ci @status ||= statuses.latest.status end - def detailed_status - Gitlab::Ci::Status::Stage::Factory.new(self).fabricate! + def detailed_status(current_user) + Gitlab::Ci::Status::Stage::Factory.new(self, current_user).fabricate! end def statuses diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index fce16174e22..6548a7dda2c 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -132,7 +132,7 @@ class CommitStatus < ActiveRecord::Base false end - def detailed_status - Gitlab::Ci::Status::Factory.new(self).fabricate! + def detailed_status(current_user) + Gitlab::Ci::Status::Factory.new(self, current_user).fabricate! end end diff --git a/app/views/ci/status/_icon_with_label.html.haml b/app/views/ci/status/_icon_with_label.html.haml index 65a74e88444..d3fe332cc78 100644 --- a/app/views/ci/status/_icon_with_label.html.haml +++ b/app/views/ci/status/_icon_with_label.html.haml @@ -1,10 +1,11 @@ -- details_path = subject.details_path if subject.has_details?(current_user) -- klass = "ci-status ci-#{subject.status}" +- detailed_status = subject.detailed_status(current_user) +- details_path = detailed_status.details_path if detailed_status.has_details? +- klass = "ci-status ci-#{detailed_status}" - if details_path = link_to details_path, class: klass do - = custom_icon(status.icon) - = status.text + = custom_icon(detailed_status.icon) + = detailed_status.text - else %span{ class: klass } - = custom_icon(status.icon) - = status.text + = custom_icon(detailed_status.icon) + = detailed_status.text diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb index a8830b04715..88be0cd924b 100644 --- a/lib/gitlab/ci/status/build/cancelable.rb +++ b/lib/gitlab/ci/status/build/cancelable.rb @@ -1,7 +1,7 @@ module Gitlab module Ci module Status - module Status + module Build class Cancelable < SimpleDelegator include Status::Extended diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb index 70c08197ea1..57c7058fe84 100644 --- a/lib/gitlab/ci/status/build/play.rb +++ b/lib/gitlab/ci/status/build/play.rb @@ -1,7 +1,7 @@ module Gitlab module Ci module Status - module Status + module Build class Play < SimpleDelegator include Status::Extended diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb index 3309e8808e1..69f2ad1d277 100644 --- a/lib/gitlab/ci/status/build/retryable.rb +++ b/lib/gitlab/ci/status/build/retryable.rb @@ -1,7 +1,7 @@ module Gitlab module Ci module Status - module Status + module Build class Retryable < SimpleDelegator include Status::Extended diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb index 6fb51890bec..cd9bd959a7c 100644 --- a/lib/gitlab/ci/status/build/stop.rb +++ b/lib/gitlab/ci/status/build/stop.rb @@ -1,8 +1,8 @@ module Gitlab module Ci module Status - module Status - class Play < SimpleDelegator + module Build + class Stop < SimpleDelegator include Status::Extended def text -- cgit v1.2.1 From f6240862c884a0290f4ffcce9071946c785b4027 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 8 Dec 2016 18:16:23 +0100 Subject: Rename icon_with_label to icon_with_description --- app/views/admin/runners/show.html.haml | 2 +- app/views/ci/status/_icon_with_description.html.haml | 12 ++++++++++++ app/views/ci/status/_icon_with_label.html.haml | 11 ----------- app/views/projects/builds/_header.html.haml | 2 +- app/views/projects/ci/builds/_build.html.haml | 2 +- app/views/projects/ci/pipelines/_pipeline.html.haml | 2 +- .../generic_commit_statuses/_generic_commit_status.html.haml | 2 +- app/views/projects/pipelines/_info.html.haml | 2 +- app/views/projects/stage/_graph.html.haml | 4 ++-- app/views/projects/stage/_in_stage_group.html.haml | 2 +- 10 files changed, 21 insertions(+), 20 deletions(-) create mode 100644 app/views/ci/status/_icon_with_description.html.haml delete mode 100644 app/views/ci/status/_icon_with_label.html.haml diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index fa8be25ffa8..badeb11b208 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -91,7 +91,7 @@ %strong ##{build.id} %td.status - = render "ci/status/icon_with_label", subject: build + = render "ci/status/icon_with_description", subject: build %td.status - if project diff --git a/app/views/ci/status/_icon_with_description.html.haml b/app/views/ci/status/_icon_with_description.html.haml new file mode 100644 index 00000000000..34c923440d0 --- /dev/null +++ b/app/views/ci/status/_icon_with_description.html.haml @@ -0,0 +1,12 @@ +- detailed_status = subject.detailed_status(current_user) +- details_path = detailed_status.details_path if detailed_status.has_details? +- klass = "ci-status ci-#{detailed_status}" + +- if details_path + = link_to details_path, class: klass do + = custom_icon(detailed_status.icon) + = detailed_status.text +- else + %span{ class: klass } + = custom_icon(detailed_status.icon) + = detailed_status.text diff --git a/app/views/ci/status/_icon_with_label.html.haml b/app/views/ci/status/_icon_with_label.html.haml deleted file mode 100644 index d3fe332cc78..00000000000 --- a/app/views/ci/status/_icon_with_label.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -- detailed_status = subject.detailed_status(current_user) -- details_path = detailed_status.details_path if detailed_status.has_details? -- klass = "ci-status ci-#{detailed_status}" -- if details_path - = link_to details_path, class: klass do - = custom_icon(detailed_status.icon) - = detailed_status.text -- else - %span{ class: klass } - = custom_icon(detailed_status.icon) - = detailed_status.text diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml index 5e4e30f08d5..85d1793ecb9 100644 --- a/app/views/projects/builds/_header.html.haml +++ b/app/views/projects/builds/_header.html.haml @@ -1,6 +1,6 @@ .content-block.build-header .header-content - = render "ci/status/icon_with_label", subject: build + = render "ci/status/icon_with_description", subject: build Build %strong ##{@build.id} in pipeline diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 6b0cd3e49a0..4257bb86859 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -9,7 +9,7 @@ %tr.build.commit{class: ('retried' if retried)} %td.status - = render "ci/status/icon_with_label", subject: build + = render "ci/status/icon_with_description", subject: build %td.branch-commit - if can?(current_user, :read_build, build) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 84243e4306d..6dff955ea3d 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -4,7 +4,7 @@ %tr.commit %td.commit-link - = render "ci/status/icon_with_label", subject: pipeline + = render "ci/status/icon_with_description", subject: pipeline %td = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index 69cb1631ee8..1dd07ae1a2a 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -8,7 +8,7 @@ %tr.generic_commit_status{class: ('retried' if retried)} %td.status - = render "ci/status/icon_with_label", subject: generic_commit_status + = render "ci/status/icon_with_description", subject: generic_commit_status %td.generic_commit_status-link - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index f7385184a2b..d05697b4ee3 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -1,6 +1,6 @@ .page-content-header .header-main-content - = render "ci/status/icon_with_label", subject: @pipeline + = render "ci/status/icon_with_description", subject: @pipeline %strong Pipeline ##{@commit.pipelines.last.id} triggered #{time_ago_with_tooltip(@commit.authored_date)} by = author_avatar(@commit, size: 24) diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml index d8c87fae5a1..cb845ae7f92 100644 --- a/app/views/projects/stage/_graph.html.haml +++ b/app/views/projects/stage/_graph.html.haml @@ -15,9 +15,9 @@ %li.build{ class: ("playable" if is_playable) } .curve .build-content - = render "projects/#{status.to_partial_path}_pipeline", subject: status + = render 'ci/status/icon_with_name_and_action', subject: status - else %li.build .curve .dropdown.inline.build-content - = render "projects/stage/in_stage_group", name: group_name, subject: grouped_statuses + = render 'projects/stage/in_stage_group', name: group_name, subject: grouped_statuses diff --git a/app/views/projects/stage/_in_stage_group.html.haml b/app/views/projects/stage/_in_stage_group.html.haml index 2d198d1b389..5c9b6549b37 100644 --- a/app/views/projects/stage/_in_stage_group.html.haml +++ b/app/views/projects/stage/_in_stage_group.html.haml @@ -10,4 +10,4 @@ %ul - subject.each do |status| %li.dropdown-build - = render "projects/#{status.to_partial_path}_pipeline", subject: status + = render 'ci/status/icon_with_name_and_action', subject: status -- cgit v1.2.1 From 7c6984752fb6557ac754bb13a554f9b76ee39989 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 8 Dec 2016 18:18:30 +0100 Subject: Add action_class/action_title --- lib/gitlab/ci/status/build/cancelable.rb | 6 +++++- lib/gitlab/ci/status/build/play.rb | 8 ++++++++ lib/gitlab/ci/status/build/retryable.rb | 6 +++++- lib/gitlab/ci/status/build/stop.rb | 6 +++++- lib/gitlab/ci/status/core.rb | 7 +++++++ 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb index 88be0cd924b..a979fe7d573 100644 --- a/lib/gitlab/ci/status/build/cancelable.rb +++ b/lib/gitlab/ci/status/build/cancelable.rb @@ -10,7 +10,7 @@ module Gitlab end def action_icon - 'remove' + 'ban' end def action_path @@ -23,6 +23,10 @@ module Gitlab :post end + def action_title + 'Cancel' + end + def self.matches?(build, user) build.cancelable? end diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb index 57c7058fe84..e3066d40a37 100644 --- a/lib/gitlab/ci/status/build/play.rb +++ b/lib/gitlab/ci/status/build/play.rb @@ -17,10 +17,18 @@ module Gitlab can?(user, :update_build, subject) end + def action_title + 'Play' + end + def action_icon 'play' end + def action_class + 'ci-play-icon' + end + def action_path play_namespace_project_build_path(subject.project.namespace, subject.project, diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb index 69f2ad1d277..8e38d6a8523 100644 --- a/lib/gitlab/ci/status/build/retryable.rb +++ b/lib/gitlab/ci/status/build/retryable.rb @@ -10,7 +10,11 @@ module Gitlab end def action_icon - 'repeat' + 'refresh' + end + + def action_title + 'Retry' end def action_path diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb index cd9bd959a7c..487fd033960 100644 --- a/lib/gitlab/ci/status/build/stop.rb +++ b/lib/gitlab/ci/status/build/stop.rb @@ -26,7 +26,11 @@ module Gitlab end def action_icon - :play + 'stop' + end + + def action_title + 'Stop' end def action_path diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb index 7e9f6e35012..dd3a824e486 100644 --- a/lib/gitlab/ci/status/core.rb +++ b/lib/gitlab/ci/status/core.rb @@ -49,6 +49,9 @@ module Gitlab raise NotImplementedError end + def action_class + end + def action_path raise NotImplementedError end @@ -56,6 +59,10 @@ module Gitlab def action_method raise NotImplementedError end + + def action_title + raise NotImplementedError + end end end end -- cgit v1.2.1 From 203c9040571944f573d18db2bd477521b6c76535 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 8 Dec 2016 18:19:17 +0100 Subject: Add icon_with_name_and_action and use it everywhere --- app/views/ci/status/_icon_with_name.html.haml | 11 ++++++++ .../ci/status/_icon_with_name_and_action.html.haml | 8 ++++++ .../projects/ci/builds/_build_pipeline.html.haml | 31 ---------------------- .../_generic_commit_status_pipeline.html.haml | 10 ------- 4 files changed, 19 insertions(+), 41 deletions(-) create mode 100644 app/views/ci/status/_icon_with_name.html.haml create mode 100644 app/views/ci/status/_icon_with_name_and_action.html.haml delete mode 100644 app/views/projects/ci/builds/_build_pipeline.html.haml delete mode 100644 app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml diff --git a/app/views/ci/status/_icon_with_name.html.haml b/app/views/ci/status/_icon_with_name.html.haml new file mode 100644 index 00000000000..028e1fe9402 --- /dev/null +++ b/app/views/ci/status/_icon_with_name.html.haml @@ -0,0 +1,11 @@ +- detailed_status = subject.detailed_status(current_user) +- details_path = detailed_status.details_path if detailed_status.has_details? +- klass = "ci-status-icon ci-status-icon-#{detailed_status}" + +- if details_path + = link_to details_path, class: klass, data: { toggle: 'tooltip', title: "#{subject.name} - #{detailed_status}" } do + %span{ class: klass }= custom_icon(detailed_status.icon) + .ci-status-text= subject.name +- else + %span{ class: klass }= custom_icon(detailed_status.icon) + .ci-status-text= subject.name diff --git a/app/views/ci/status/_icon_with_name_and_action.html.haml b/app/views/ci/status/_icon_with_name_and_action.html.haml new file mode 100644 index 00000000000..76db3b7f38a --- /dev/null +++ b/app/views/ci/status/_icon_with_name_and_action.html.haml @@ -0,0 +1,8 @@ += render "ci/status/icon_with_name", subject: subject + +- detailed_status = subject.detailed_status(current_user) +- if detailed_status.has_action? + = link_to detailed_status.action_path, method: detailed_status.action_method, + title: "#{subject.name}: #{detailed_status.action_title}", class: 'ci-action-icon-container' do + %i.ci-action-icon-wrapper + = icon(detailed_status.action_icon, class: detailed_status.action_class) diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml deleted file mode 100644 index 41b9265fe5e..00000000000 --- a/app/views/projects/ci/builds/_build_pipeline.html.haml +++ /dev/null @@ -1,31 +0,0 @@ -- is_playable = subject.playable? && can?(current_user, :update_build, @project) -- can_cancel = subject.active? && can?(current_user, :update_build, @project) -- can_retry = subject.retryable? && can?(current_user, :update_build, @project) -- can_stop = subject.complete? && subject.stops_environment? && can?(current_user, :update_build, @project) - -- if can?(current_user, :read_build, @project) - = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } do - %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} - = ci_icon_for_status(subject.status) - .ci-status-text= subject.name - - - if is_playable - = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: "#{subject.name} - Play", class: 'ci-action-icon-container' do - %i.ci-action-icon-wrapper - = icon('play', class: 'ci-play-icon') - - elsif can_cancel - = link_to cancel_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: "#{subject.name} - Cancel", class: 'ci-action-icon-container' do - %i.ci-action-icon-wrapper - = icon('ban') - - elsif can_retry - = link_to retry_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: "#{subject.name} - Retry", class: 'ci-action-icon-container' do - %i.ci-action-icon-wrapper - = icon('refresh') - - elsif can_stop - = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: "#{subject.name} - Stop", class: 'ci-action-icon-container' do - %i.ci-action-icon-wrapper - = icon('stop') - -- else - %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} - = ci_icon_for_status(subject.status) diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml deleted file mode 100644 index 7b82d913d29..00000000000 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } } - - if subject.target_url - = link_to subject.target_url do - %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} - = ci_icon_for_status(subject.status) - %span.ci-status-text= subject.name - - else - %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} - = ci_icon_for_status(subject.status) - %span.ci-status-text= subject.name -- cgit v1.2.1 From 6296a1cc5d70e2e32a0a98e903415832ca29f281 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 8 Dec 2016 17:52:24 +0100 Subject: Fix auto loading of constants for Ci Statuses --- app/models/ci/build.rb | 6 +++--- app/models/ci/pipeline.rb | 4 ++-- app/models/ci/stage.rb | 4 ++-- app/models/commit_status.rb | 4 ++-- app/views/ci/status/_icon_with_label.html.haml | 13 +++++++------ lib/gitlab/ci/status/build/cancelable.rb | 2 +- lib/gitlab/ci/status/build/play.rb | 2 +- lib/gitlab/ci/status/build/retryable.rb | 2 +- lib/gitlab/ci/status/build/stop.rb | 4 ++-- 9 files changed, 21 insertions(+), 20 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 73564dd2aa0..65ee327a8e5 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -100,8 +100,8 @@ module Ci end end - def detailed_status - Gitlab::Ci::Status::Build::Factory.new(self).fabricate! + def detailed_status(current_user) + Gitlab::Ci::Status::Build::Factory.new(self, current_user).fabricate! end def manual? @@ -156,7 +156,7 @@ module Ci end def environment_action - self.options.fetch(:environment, {}).fetch(:action, 'start') + self.options.fetch(:environment, {}).fetch(:action, 'start') if self.options end def outdated_deployment? diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index fda8228a1e9..1f33106d358 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -336,8 +336,8 @@ module Ci .select { |merge_request| merge_request.head_pipeline.try(:id) == self.id } end - def detailed_status - Gitlab::Ci::Status::Pipeline::Factory.new(self).fabricate! + def detailed_status(current_user) + Gitlab::Ci::Status::Pipeline::Factory.new(self, current_user).fabricate! end private diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index d2a37c0a827..be52cce20f1 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -22,8 +22,8 @@ module Ci @status ||= statuses.latest.status end - def detailed_status - Gitlab::Ci::Status::Stage::Factory.new(self).fabricate! + def detailed_status(current_user) + Gitlab::Ci::Status::Stage::Factory.new(self, current_user).fabricate! end def statuses diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index fce16174e22..6548a7dda2c 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -132,7 +132,7 @@ class CommitStatus < ActiveRecord::Base false end - def detailed_status - Gitlab::Ci::Status::Factory.new(self).fabricate! + def detailed_status(current_user) + Gitlab::Ci::Status::Factory.new(self, current_user).fabricate! end end diff --git a/app/views/ci/status/_icon_with_label.html.haml b/app/views/ci/status/_icon_with_label.html.haml index 65a74e88444..d3fe332cc78 100644 --- a/app/views/ci/status/_icon_with_label.html.haml +++ b/app/views/ci/status/_icon_with_label.html.haml @@ -1,10 +1,11 @@ -- details_path = subject.details_path if subject.has_details?(current_user) -- klass = "ci-status ci-#{subject.status}" +- detailed_status = subject.detailed_status(current_user) +- details_path = detailed_status.details_path if detailed_status.has_details? +- klass = "ci-status ci-#{detailed_status}" - if details_path = link_to details_path, class: klass do - = custom_icon(status.icon) - = status.text + = custom_icon(detailed_status.icon) + = detailed_status.text - else %span{ class: klass } - = custom_icon(status.icon) - = status.text + = custom_icon(detailed_status.icon) + = detailed_status.text diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb index a8830b04715..88be0cd924b 100644 --- a/lib/gitlab/ci/status/build/cancelable.rb +++ b/lib/gitlab/ci/status/build/cancelable.rb @@ -1,7 +1,7 @@ module Gitlab module Ci module Status - module Status + module Build class Cancelable < SimpleDelegator include Status::Extended diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb index 70c08197ea1..57c7058fe84 100644 --- a/lib/gitlab/ci/status/build/play.rb +++ b/lib/gitlab/ci/status/build/play.rb @@ -1,7 +1,7 @@ module Gitlab module Ci module Status - module Status + module Build class Play < SimpleDelegator include Status::Extended diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb index 3309e8808e1..69f2ad1d277 100644 --- a/lib/gitlab/ci/status/build/retryable.rb +++ b/lib/gitlab/ci/status/build/retryable.rb @@ -1,7 +1,7 @@ module Gitlab module Ci module Status - module Status + module Build class Retryable < SimpleDelegator include Status::Extended diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb index 6fb51890bec..cd9bd959a7c 100644 --- a/lib/gitlab/ci/status/build/stop.rb +++ b/lib/gitlab/ci/status/build/stop.rb @@ -1,8 +1,8 @@ module Gitlab module Ci module Status - module Status - class Play < SimpleDelegator + module Build + class Stop < SimpleDelegator include Status::Extended def text -- cgit v1.2.1 From a30dcc6771ec9244223ddb1de9293fe3fefeedf0 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 8 Dec 2016 18:16:23 +0100 Subject: Rename icon_with_label to icon_with_description --- app/views/admin/runners/show.html.haml | 2 +- app/views/ci/status/_icon_with_description.html.haml | 12 ++++++++++++ app/views/ci/status/_icon_with_label.html.haml | 11 ----------- app/views/projects/builds/_header.html.haml | 2 +- app/views/projects/ci/builds/_build.html.haml | 2 +- app/views/projects/ci/pipelines/_pipeline.html.haml | 2 +- .../generic_commit_statuses/_generic_commit_status.html.haml | 2 +- app/views/projects/pipelines/_info.html.haml | 2 +- app/views/projects/stage/_graph.html.haml | 4 ++-- 9 files changed, 20 insertions(+), 19 deletions(-) create mode 100644 app/views/ci/status/_icon_with_description.html.haml delete mode 100644 app/views/ci/status/_icon_with_label.html.haml diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index fa8be25ffa8..badeb11b208 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -91,7 +91,7 @@ %strong ##{build.id} %td.status - = render "ci/status/icon_with_label", subject: build + = render "ci/status/icon_with_description", subject: build %td.status - if project diff --git a/app/views/ci/status/_icon_with_description.html.haml b/app/views/ci/status/_icon_with_description.html.haml new file mode 100644 index 00000000000..34c923440d0 --- /dev/null +++ b/app/views/ci/status/_icon_with_description.html.haml @@ -0,0 +1,12 @@ +- detailed_status = subject.detailed_status(current_user) +- details_path = detailed_status.details_path if detailed_status.has_details? +- klass = "ci-status ci-#{detailed_status}" + +- if details_path + = link_to details_path, class: klass do + = custom_icon(detailed_status.icon) + = detailed_status.text +- else + %span{ class: klass } + = custom_icon(detailed_status.icon) + = detailed_status.text diff --git a/app/views/ci/status/_icon_with_label.html.haml b/app/views/ci/status/_icon_with_label.html.haml deleted file mode 100644 index d3fe332cc78..00000000000 --- a/app/views/ci/status/_icon_with_label.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -- detailed_status = subject.detailed_status(current_user) -- details_path = detailed_status.details_path if detailed_status.has_details? -- klass = "ci-status ci-#{detailed_status}" -- if details_path - = link_to details_path, class: klass do - = custom_icon(detailed_status.icon) - = detailed_status.text -- else - %span{ class: klass } - = custom_icon(detailed_status.icon) - = detailed_status.text diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml index 5e4e30f08d5..85d1793ecb9 100644 --- a/app/views/projects/builds/_header.html.haml +++ b/app/views/projects/builds/_header.html.haml @@ -1,6 +1,6 @@ .content-block.build-header .header-content - = render "ci/status/icon_with_label", subject: build + = render "ci/status/icon_with_description", subject: build Build %strong ##{@build.id} in pipeline diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 6b0cd3e49a0..4257bb86859 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -9,7 +9,7 @@ %tr.build.commit{class: ('retried' if retried)} %td.status - = render "ci/status/icon_with_label", subject: build + = render "ci/status/icon_with_description", subject: build %td.branch-commit - if can?(current_user, :read_build, build) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 84243e4306d..6dff955ea3d 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -4,7 +4,7 @@ %tr.commit %td.commit-link - = render "ci/status/icon_with_label", subject: pipeline + = render "ci/status/icon_with_description", subject: pipeline %td = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index 69cb1631ee8..1dd07ae1a2a 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -8,7 +8,7 @@ %tr.generic_commit_status{class: ('retried' if retried)} %td.status - = render "ci/status/icon_with_label", subject: generic_commit_status + = render "ci/status/icon_with_description", subject: generic_commit_status %td.generic_commit_status-link - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index f7385184a2b..d05697b4ee3 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -1,6 +1,6 @@ .page-content-header .header-main-content - = render "ci/status/icon_with_label", subject: @pipeline + = render "ci/status/icon_with_description", subject: @pipeline %strong Pipeline ##{@commit.pipelines.last.id} triggered #{time_ago_with_tooltip(@commit.authored_date)} by = author_avatar(@commit, size: 24) diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml index d8c87fae5a1..cb845ae7f92 100644 --- a/app/views/projects/stage/_graph.html.haml +++ b/app/views/projects/stage/_graph.html.haml @@ -15,9 +15,9 @@ %li.build{ class: ("playable" if is_playable) } .curve .build-content - = render "projects/#{status.to_partial_path}_pipeline", subject: status + = render 'ci/status/icon_with_name_and_action', subject: status - else %li.build .curve .dropdown.inline.build-content - = render "projects/stage/in_stage_group", name: group_name, subject: grouped_statuses + = render 'projects/stage/in_stage_group', name: group_name, subject: grouped_statuses -- cgit v1.2.1 From e8aae61ad07c3fc3d62394ba620c3b8654c29f65 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 8 Dec 2016 18:18:30 +0100 Subject: Add action_class/action_title --- lib/gitlab/ci/status/build/cancelable.rb | 6 +++++- lib/gitlab/ci/status/build/play.rb | 8 ++++++++ lib/gitlab/ci/status/build/retryable.rb | 6 +++++- lib/gitlab/ci/status/build/stop.rb | 6 +++++- lib/gitlab/ci/status/core.rb | 7 +++++++ 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb index 88be0cd924b..a979fe7d573 100644 --- a/lib/gitlab/ci/status/build/cancelable.rb +++ b/lib/gitlab/ci/status/build/cancelable.rb @@ -10,7 +10,7 @@ module Gitlab end def action_icon - 'remove' + 'ban' end def action_path @@ -23,6 +23,10 @@ module Gitlab :post end + def action_title + 'Cancel' + end + def self.matches?(build, user) build.cancelable? end diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb index 57c7058fe84..e3066d40a37 100644 --- a/lib/gitlab/ci/status/build/play.rb +++ b/lib/gitlab/ci/status/build/play.rb @@ -17,10 +17,18 @@ module Gitlab can?(user, :update_build, subject) end + def action_title + 'Play' + end + def action_icon 'play' end + def action_class + 'ci-play-icon' + end + def action_path play_namespace_project_build_path(subject.project.namespace, subject.project, diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb index 69f2ad1d277..8e38d6a8523 100644 --- a/lib/gitlab/ci/status/build/retryable.rb +++ b/lib/gitlab/ci/status/build/retryable.rb @@ -10,7 +10,11 @@ module Gitlab end def action_icon - 'repeat' + 'refresh' + end + + def action_title + 'Retry' end def action_path diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb index cd9bd959a7c..487fd033960 100644 --- a/lib/gitlab/ci/status/build/stop.rb +++ b/lib/gitlab/ci/status/build/stop.rb @@ -26,7 +26,11 @@ module Gitlab end def action_icon - :play + 'stop' + end + + def action_title + 'Stop' end def action_path diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb index 7e9f6e35012..dd3a824e486 100644 --- a/lib/gitlab/ci/status/core.rb +++ b/lib/gitlab/ci/status/core.rb @@ -49,6 +49,9 @@ module Gitlab raise NotImplementedError end + def action_class + end + def action_path raise NotImplementedError end @@ -56,6 +59,10 @@ module Gitlab def action_method raise NotImplementedError end + + def action_title + raise NotImplementedError + end end end end -- cgit v1.2.1 From be1d3376c6525f4ad24a6050d776fc3967e201a1 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 8 Dec 2016 18:21:11 +0100 Subject: Revert some unneeded changes --- app/views/projects/stage/_graph.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml index cb845ae7f92..c0ad7b07c77 100644 --- a/app/views/projects/stage/_graph.html.haml +++ b/app/views/projects/stage/_graph.html.haml @@ -15,7 +15,7 @@ %li.build{ class: ("playable" if is_playable) } .curve .build-content - = render 'ci/status/icon_with_name_and_action', subject: status + = render "projects/#{status.to_partial_path}_pipeline", subject: status - else %li.build .curve -- cgit v1.2.1 From 67c2e741195083b9c9d5d5f741c7909d22bc988b Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 8 Dec 2016 17:24:30 +0000 Subject: Adds tests for Custom Event polyfill Update changelog with MR ID --- .../24927-custom-event-polyfill-test.yml | 4 ++ .../lib/utils/custom_event_polyfill_spec.js.es6 | 43 ++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 changelogs/unreleased/24927-custom-event-polyfill-test.yml create mode 100644 spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6 diff --git a/changelogs/unreleased/24927-custom-event-polyfill-test.yml b/changelogs/unreleased/24927-custom-event-polyfill-test.yml new file mode 100644 index 00000000000..879c28a951e --- /dev/null +++ b/changelogs/unreleased/24927-custom-event-polyfill-test.yml @@ -0,0 +1,4 @@ +--- +title: Adds tests for custom event polyfill +merge_request: 7996 +author: diff --git a/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6 b/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6 new file mode 100644 index 00000000000..ad51367bb32 --- /dev/null +++ b/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6 @@ -0,0 +1,43 @@ +//= require lib/utils/custom_event_polyfill + +describe('Custom Event Polyfill', () => { + it('should be defined', () => { + expect(window.CustomEvent).toBeDefined(); + }); + + it('should create a `CustomEvent` instance', () => { + const e = new window.CustomEvent('foo'); + + expect(e.type).toEqual('foo'); + expect(e.bubbles).toBe(false); + expect(e.cancelable).toBe(false); + expect(e.detail).toBe(null); + }); + + it('should create a `CustomEvent` instance with a `details` object', () => { + const e = new window.CustomEvent('bar', { detail: { foo: 'bar' } }); + + expect(e.type).toEqual('bar'); + expect(e.bubbles).toBe(false); + expect(e.cancelable).toBe(false); + expect(e.detail.foo).toEqual('bar'); + }); + + it('should create a `CustomEvent` instance with a `bubbles` boolean', () => { + const e = new window.CustomEvent('bar', { bubbles: true }); + + expect(e.type).toEqual('bar'); + expect(e.bubbles).toBe(true); + expect(e.cancelable).toBe(false); + expect(e.detail).toBe(null); + }); + + it('should create a `CustomEvent` instance with a `cancelable` boolean', () => { + const e = new window.CustomEvent('bar', { cancelable: true }); + + expect(e.type).toEqual('bar'); + expect(e.bubbles).toBe(false); + expect(e.cancelable).toBe(true); + expect(e.detail).toBe(null); + }); +}); -- cgit v1.2.1 From a8cfb85a1ad5c835abe4b7cceefb9d8304717553 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" <lbennett@gitlab.com> Date: Sat, 3 Dec 2016 10:44:18 +0000 Subject: Added special char test to the default beforeInsert callback. I removed the quotes from the milestone displayTpl and added a skip setting for emoji instance Review changes --- app/assets/javascripts/gfm_auto_complete.js.es6 | 21 ++++---- spec/features/issues/gfm_autocomplete_spec.rb | 67 +++++++++++++++++++++---- 2 files changed, 69 insertions(+), 19 deletions(-) diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 6f9d6283071..2f3da745119 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -52,6 +52,10 @@ return $.fn.atwho["default"].callbacks.filter(query, data, searchKey); }, beforeInsert: function(value) { + if (value && !this.setting.skipSpecialCharacterTest) { + var withoutAt = value.substring(1); + if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"'; + } if (!GitLab.GfmAutoComplete.dataLoaded) { return this.at; } else { @@ -117,6 +121,7 @@ insertTpl: ':${name}:', data: ['loading'], startWithSpace: false, + skipSpecialCharacterTest: true, callbacks: { sorter: this.DefaultOptions.sorter, filter: this.DefaultOptions.filter, @@ -141,6 +146,7 @@ data: ['loading'], startWithSpace: false, alwaysHighlightFirst: true, + skipSpecialCharacterTest: true, callbacks: { sorter: this.DefaultOptions.sorter, filter: this.DefaultOptions.filter, @@ -219,12 +225,13 @@ } }; })(this), - insertTpl: '${atwho-at}"${title}"', + insertTpl: '${atwho-at}${title}', data: ['loading'], startWithSpace: false, callbacks: { matcher: this.DefaultOptions.matcher, sorter: this.DefaultOptions.sorter, + beforeInsert: this.DefaultOptions.beforeInsert, beforeSave: function(milestones) { return $.map(milestones, function(m) { if (m.title == null) { @@ -284,18 +291,11 @@ callbacks: { matcher: this.DefaultOptions.matcher, sorter: this.DefaultOptions.sorter, + beforeInsert: this.DefaultOptions.beforeInsert, beforeSave: function(merges) { - var sanitizeLabelTitle; - sanitizeLabelTitle = function(title) { - if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) { - return "\"" + (sanitize(title)) + "\""; - } else { - return sanitize(title); - } - }; return $.map(merges, function(m) { return { - title: sanitizeLabelTitle(m.title), + title: sanitize(m.title), color: m.color, search: "" + m.title }; @@ -308,6 +308,7 @@ at: '/', alias: 'commands', searchKey: 'search', + skipSpecialCharacterTest: true, displayTpl: function(value) { var tpl = '<li>/${name}'; if (value.aliases.length > 0) { diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index c421da97d76..cd0512a37e6 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -2,8 +2,9 @@ require 'rails_helper' feature 'GFM autocomplete', feature: true, js: true do include WaitForAjax - let(:user) { create(:user) } + let(:user) { create(:user, username: 'someone.special') } let(:project) { create(:project) } + let(:label) { create(:label, project: project, title: 'special+') } let(:issue) { create(:issue, project: project) } before do @@ -23,21 +24,69 @@ feature 'GFM autocomplete', feature: true, js: true do expect(page).to have_selector('.atwho-container') end - it 'opens autocomplete menu when field is prefixed with non-text character' do + it 'doesnt open autocomplete menu character is prefixed with text' do page.within '.timeline-content-form' do - find('#note_note').native.send_keys('') + find('#note_note').native.send_keys('testing') find('#note_note').native.send_keys('@') end - expect(page).to have_selector('.atwho-container') + expect(page).not_to have_selector('.atwho-view') end - it 'doesnt open autocomplete menu character is prefixed with text' do - page.within '.timeline-content-form' do - find('#note_note').native.send_keys('testing') - find('#note_note').native.send_keys('@') + context 'if a selected value has special characters' do + it 'wraps the result in double quotes' do + note = find('#note_note') + page.within '.timeline-content-form' do + note.native.send_keys('') + note.native.send_keys("~#{label.title[0]}") + sleep 1 + note.click + end + + label_item = find('.atwho-view li', text: label.title) + + expect_to_wrap(true, label_item, note, label.title) end - expect(page).not_to have_selector('.atwho-view') + it 'doesn\'t wrap for assignee values' do + note = find('#note_note') + page.within '.timeline-content-form' do + note.native.send_keys('') + note.native.send_keys("@#{user.username[0]}") + sleep 1 + note.click + end + + user_item = find('.atwho-view li', text: user.username) + + expect_to_wrap(false, user_item, note, user.username) + end + + it 'doesn\'t wrap for emoji values' do + note = find('#note_note') + page.within '.timeline-content-form' do + note.native.send_keys('') + note.native.send_keys(":cartwheel") + sleep 1 + note.click + end + + emoji_item = find('.atwho-view li', text: 'cartwheel_tone1') + + expect_to_wrap(false, emoji_item, note, 'cartwheel_tone1') + end + + def expect_to_wrap(should_wrap, item, note, value) + expect(item).to have_content(value) + expect(item).not_to have_content("\"#{value}\"") + + item.click + + if should_wrap + expect(note.value).to include("\"#{value}\"") + else + expect(note.value).not_to include("\"#{value}\"") + end + end end end -- cgit v1.2.1 From d74ad9263048549e4d90e6a22313768109eaf2c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Thu, 8 Dec 2016 18:29:25 +0100 Subject: Use a single query in Projects::ProjectMembersController to fetch members MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- app/controllers/projects/project_members_controller.rb | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 3fb8bba3cd0..53308948f62 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -35,13 +35,12 @@ class Projects::ProjectMembersController < Projects::ApplicationController @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) end - member_ids = @project_members.pluck(:id) + wheres = ["id IN (#{@project_members.select(:id).to_sql})"] + wheres << "id IN (#{group_members.select(:id).to_sql})" if group_members - if group_members - member_ids += group_members.pluck(:id) - end - - @project_members = Member.where(id: member_ids).order(access_level: :desc).page(params[:page]) + @project_members = Member. + where(wheres.join(' OR ')). + order(access_level: :desc).page(params[:page]) @requesters = AccessRequestsFinder.new(@project).execute(current_user) -- cgit v1.2.1 From b64cf8405c551e79520e8ce4eef2dc17259c41e3 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 8 Dec 2016 17:58:32 +0000 Subject: Renders new icons for the pipeline graph --- app/helpers/ci_status_helper.rb | 66 ++++++++-------------- app/views/ci/status/_icon_with_name.html.haml | 3 +- .../ci/status/_icon_with_name_and_action.html.haml | 2 +- .../shared/icons/_icon_graph_job_cancelled.svg | 1 - app/views/shared/icons/_icon_graph_job_created.svg | 1 - app/views/shared/icons/_icon_graph_job_failed.svg | 1 - app/views/shared/icons/_icon_graph_job_manual.svg | 1 - app/views/shared/icons/_icon_graph_job_pending.svg | 1 - app/views/shared/icons/_icon_graph_job_running.svg | 1 - app/views/shared/icons/_icon_graph_job_skipped.svg | 1 - app/views/shared/icons/_icon_graph_job_success.svg | 1 - app/views/shared/icons/_icon_graph_job_warning.svg | 1 - .../shared/icons/_icon_status_canceled_graph.svg | 1 + .../shared/icons/_icon_status_created_graph.svg | 1 + .../shared/icons/_icon_status_failed_graph.svg | 1 + .../shared/icons/_icon_status_manual_graph.svg | 1 + .../shared/icons/_icon_status_pending_graph.svg | 1 + .../shared/icons/_icon_status_running_graph.svg | 1 + .../shared/icons/_icon_status_skipped_graph.svg | 1 + .../shared/icons/_icon_status_success_graph.svg | 1 + .../shared/icons/_icon_status_warning_graph.svg | 1 + 21 files changed, 34 insertions(+), 55 deletions(-) delete mode 100755 app/views/shared/icons/_icon_graph_job_cancelled.svg delete mode 100755 app/views/shared/icons/_icon_graph_job_created.svg delete mode 100755 app/views/shared/icons/_icon_graph_job_failed.svg delete mode 100755 app/views/shared/icons/_icon_graph_job_manual.svg delete mode 100755 app/views/shared/icons/_icon_graph_job_pending.svg delete mode 100755 app/views/shared/icons/_icon_graph_job_running.svg delete mode 100755 app/views/shared/icons/_icon_graph_job_skipped.svg delete mode 100755 app/views/shared/icons/_icon_graph_job_success.svg delete mode 100755 app/views/shared/icons/_icon_graph_job_warning.svg create mode 100755 app/views/shared/icons/_icon_status_canceled_graph.svg create mode 100755 app/views/shared/icons/_icon_status_created_graph.svg create mode 100755 app/views/shared/icons/_icon_status_failed_graph.svg create mode 100755 app/views/shared/icons/_icon_status_manual_graph.svg create mode 100755 app/views/shared/icons/_icon_status_pending_graph.svg create mode 100755 app/views/shared/icons/_icon_status_running_graph.svg create mode 100755 app/views/shared/icons/_icon_status_skipped_graph.svg create mode 100755 app/views/shared/icons/_icon_status_success_graph.svg create mode 100755 app/views/shared/icons/_icon_status_warning_graph.svg diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 7ddef2cdd5f..d9f5e01f0dc 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -25,54 +25,32 @@ module CiStatusHelper status.humanize end - def ci_icon_for_status(status, graph: nil) + def ci_icon_for_status(status) if detailed_status?(status) return custom_icon(status.icon) end - if graph - icon_name = - case status - when 'success' - 'icon_graph_job_success' - when 'success_with_warnings' - 'icon_graph_job_warning' - when 'failed' - 'icon_graph_job_failed' - when 'pending' - 'icon_graph_job_pending' - when 'running' - 'icon_graph_job_running' - when 'created' - 'icon_graph_job_created' - when 'skipped' - 'icon_graph_job_skipped' - else - 'icon_graph_job_canceled' - end - else - icon_name = - case status - when 'success' - 'icon_status_success' - when 'success_with_warnings' - 'icon_status_warning' - when 'failed' - 'icon_status_failed' - when 'pending' - 'icon_status_pending' - when 'running' - 'icon_status_running' - when 'play' - 'icon_play' - when 'created' - 'icon_status_created' - when 'skipped' - 'icon_status_skipped' - else - 'icon_status_canceled' - end - end + icon_name = + case status + when 'success' + 'icon_status_success' + when 'success_with_warnings' + 'icon_status_warning' + when 'failed' + 'icon_status_failed' + when 'pending' + 'icon_status_pending' + when 'running' + 'icon_status_running' + when 'play' + 'icon_play' + when 'created' + 'icon_status_created' + when 'skipped' + 'icon_status_skipped' + else + 'icon_status_canceled' + end custom_icon(icon_name) end diff --git a/app/views/ci/status/_icon_with_name.html.haml b/app/views/ci/status/_icon_with_name.html.haml index 028e1fe9402..a467316ef47 100644 --- a/app/views/ci/status/_icon_with_name.html.haml +++ b/app/views/ci/status/_icon_with_name.html.haml @@ -1,10 +1,11 @@ - detailed_status = subject.detailed_status(current_user) - details_path = detailed_status.details_path if detailed_status.has_details? - klass = "ci-status-icon ci-status-icon-#{detailed_status}" +- status_icon = graph ? "#{detailed_status.icon}_graph" : detailed_status.icon - if details_path = link_to details_path, class: klass, data: { toggle: 'tooltip', title: "#{subject.name} - #{detailed_status}" } do - %span{ class: klass }= custom_icon(detailed_status.icon) + %span{ class: klass }= custom_icon(status_icon) .ci-status-text= subject.name - else %span{ class: klass }= custom_icon(detailed_status.icon) diff --git a/app/views/ci/status/_icon_with_name_and_action.html.haml b/app/views/ci/status/_icon_with_name_and_action.html.haml index 76db3b7f38a..b912c212534 100644 --- a/app/views/ci/status/_icon_with_name_and_action.html.haml +++ b/app/views/ci/status/_icon_with_name_and_action.html.haml @@ -1,4 +1,4 @@ -= render "ci/status/icon_with_name", subject: subject += render "ci/status/icon_with_name", subject: subject, graph: true - detailed_status = subject.detailed_status(current_user) - if detailed_status.has_action? diff --git a/app/views/shared/icons/_icon_graph_job_cancelled.svg b/app/views/shared/icons/_icon_graph_job_cancelled.svg deleted file mode 100755 index bd5d04e1cd7..00000000000 --- a/app/views/shared/icons/_icon_graph_job_cancelled.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></svg> diff --git a/app/views/shared/icons/_icon_graph_job_created.svg b/app/views/shared/icons/_icon_graph_job_created.svg deleted file mode 100755 index 326ad04e017..00000000000 --- a/app/views/shared/icons/_icon_graph_job_created.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></svg> diff --git a/app/views/shared/icons/_icon_graph_job_failed.svg b/app/views/shared/icons/_icon_graph_job_failed.svg deleted file mode 100755 index 64da5aa31fc..00000000000 --- a/app/views/shared/icons/_icon_graph_job_failed.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></svg> diff --git a/app/views/shared/icons/_icon_graph_job_manual.svg b/app/views/shared/icons/_icon_graph_job_manual.svg deleted file mode 100755 index c98839f51a9..00000000000 --- a/app/views/shared/icons/_icon_graph_job_manual.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></svg> diff --git a/app/views/shared/icons/_icon_graph_job_pending.svg b/app/views/shared/icons/_icon_graph_job_pending.svg deleted file mode 100755 index 02d5da407e3..00000000000 --- a/app/views/shared/icons/_icon_graph_job_pending.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></svg> diff --git a/app/views/shared/icons/_icon_graph_job_running.svg b/app/views/shared/icons/_icon_graph_job_running.svg deleted file mode 100755 index 532f4fee33c..00000000000 --- a/app/views/shared/icons/_icon_graph_job_running.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></svg> diff --git a/app/views/shared/icons/_icon_graph_job_skipped.svg b/app/views/shared/icons/_icon_graph_job_skipped.svg deleted file mode 100755 index 1998dfef9ea..00000000000 --- a/app/views/shared/icons/_icon_graph_job_skipped.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7.69 7.7l-.905.905a.7.7 0 0 0 .99.99l1.85-1.85c.411-.412.411-1.078 0-1.49l-1.85-1.85a.7.7 0 0 0-.99.99l.905.905H4.48a.7.7 0 0 0 0 1.4h3.21z"/></g></svg> diff --git a/app/views/shared/icons/_icon_graph_job_success.svg b/app/views/shared/icons/_icon_graph_job_success.svg deleted file mode 100755 index eed5006bebe..00000000000 --- a/app/views/shared/icons/_icon_graph_job_success.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></svg> diff --git a/app/views/shared/icons/_icon_graph_job_warning.svg b/app/views/shared/icons/_icon_graph_job_warning.svg deleted file mode 100755 index cb785635b7e..00000000000 --- a/app/views/shared/icons/_icon_graph_job_warning.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_canceled_graph.svg b/app/views/shared/icons/_icon_status_canceled_graph.svg new file mode 100755 index 00000000000..bd5d04e1cd7 --- /dev/null +++ b/app/views/shared/icons/_icon_status_canceled_graph.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_created_graph.svg b/app/views/shared/icons/_icon_status_created_graph.svg new file mode 100755 index 00000000000..326ad04e017 --- /dev/null +++ b/app/views/shared/icons/_icon_status_created_graph.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_failed_graph.svg b/app/views/shared/icons/_icon_status_failed_graph.svg new file mode 100755 index 00000000000..64da5aa31fc --- /dev/null +++ b/app/views/shared/icons/_icon_status_failed_graph.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_manual_graph.svg b/app/views/shared/icons/_icon_status_manual_graph.svg new file mode 100755 index 00000000000..c98839f51a9 --- /dev/null +++ b/app/views/shared/icons/_icon_status_manual_graph.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_pending_graph.svg b/app/views/shared/icons/_icon_status_pending_graph.svg new file mode 100755 index 00000000000..02d5da407e3 --- /dev/null +++ b/app/views/shared/icons/_icon_status_pending_graph.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_running_graph.svg b/app/views/shared/icons/_icon_status_running_graph.svg new file mode 100755 index 00000000000..532f4fee33c --- /dev/null +++ b/app/views/shared/icons/_icon_status_running_graph.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_skipped_graph.svg b/app/views/shared/icons/_icon_status_skipped_graph.svg new file mode 100755 index 00000000000..1998dfef9ea --- /dev/null +++ b/app/views/shared/icons/_icon_status_skipped_graph.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7.69 7.7l-.905.905a.7.7 0 0 0 .99.99l1.85-1.85c.411-.412.411-1.078 0-1.49l-1.85-1.85a.7.7 0 0 0-.99.99l.905.905H4.48a.7.7 0 0 0 0 1.4h3.21z"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_success_graph.svg b/app/views/shared/icons/_icon_status_success_graph.svg new file mode 100755 index 00000000000..eed5006bebe --- /dev/null +++ b/app/views/shared/icons/_icon_status_success_graph.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_warning_graph.svg b/app/views/shared/icons/_icon_status_warning_graph.svg new file mode 100755 index 00000000000..cb785635b7e --- /dev/null +++ b/app/views/shared/icons/_icon_status_warning_graph.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></svg> -- cgit v1.2.1 From c1a71d9f58d76973c5ceae3605df1cd3c5f9f287 Mon Sep 17 00:00:00 2001 From: Felipe Artur <felipefac@gmail.com> Date: Wed, 7 Dec 2016 16:00:07 -0200 Subject: Update JIRA troubleshoot documentation --- doc/project_services/jira.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index 366e4b2d306..390066c9989 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -197,6 +197,7 @@ incorrectly the JIRA-GitLab integration. Make sure that the user you set up for GitLab to communicate with JIRA has the correct access permission to post comments on a ticket and to also transition the ticket, if you'd like GitLab to also take care of closing them. +JIRA issue references and update comments will not work if the GitLab issue tracker is disabled. ### GitLab is unable to close a ticket -- cgit v1.2.1 From 4a0ca85834b6cf7decb58857986e535ba4e37c53 Mon Sep 17 00:00:00 2001 From: Felipe Artur <felipefac@gmail.com> Date: Tue, 6 Dec 2016 18:56:59 -0200 Subject: Allow branch names with dots on API endpoint --- changelogs/unreleased/issue_25030.yml | 4 ++++ lib/api/branches.rb | 16 ++++++++-------- spec/requests/api/branches_spec.rb | 31 +++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 changelogs/unreleased/issue_25030.yml diff --git a/changelogs/unreleased/issue_25030.yml b/changelogs/unreleased/issue_25030.yml new file mode 100644 index 00000000000..e18b8d6a79b --- /dev/null +++ b/changelogs/unreleased/issue_25030.yml @@ -0,0 +1,4 @@ +--- +title: Allow branch names with dots on API endpoint +merge_request: +author: diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 73aed624ea7..0950c3d2e88 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -23,9 +23,9 @@ module API success Entities::RepoBranch end params do - requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch' + requires :branch, type: String, desc: 'The name of the branch' end - get ':id/repository/branches/:branch' do + get ':id/repository/branches/:branch', requirements: { branch: /.+/ } do branch = user_project.repository.find_branch(params[:branch]) not_found!("Branch") unless branch @@ -39,11 +39,11 @@ module API success Entities::RepoBranch end params do - requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch' + requires :branch, type: String, desc: 'The name of the branch' optional :developers_can_push, type: Boolean, desc: 'Flag if developers can push to that branch' optional :developers_can_merge, type: Boolean, desc: 'Flag if developers can merge to that branch' end - put ':id/repository/branches/:branch/protect' do + put ':id/repository/branches/:branch/protect', requirements: { branch: /.+/ } do authorize_admin_project branch = user_project.repository.find_branch(params[:branch]) @@ -76,9 +76,9 @@ module API success Entities::RepoBranch end params do - requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch' + requires :branch, type: String, desc: 'The name of the branch' end - put ':id/repository/branches/:branch/unprotect' do + put ':id/repository/branches/:branch/unprotect', requirements: { branch: /.+/ } do authorize_admin_project branch = user_project.repository.find_branch(params[:branch]) @@ -112,9 +112,9 @@ module API desc 'Delete a branch' params do - requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch' + requires :branch, type: String, desc: 'The name of the branch' end - delete ":id/repository/branches/:branch" do + delete ":id/repository/branches/:branch", requirements: { branch: /.+/ } do authorize_push_project result = DeleteBranchService.new(user_project, current_user). diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index 55b8c8c0c69..2878e0cb59b 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -11,6 +11,7 @@ describe API::Branches, api: true do let!(:guest) { create(:project_member, :guest, user: user2, project: project) } let!(:branch_name) { 'feature' } let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } + let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") } describe "GET /projects/:id/repository/branches" do it "returns an array of project branches" do @@ -37,6 +38,13 @@ describe API::Branches, api: true do expect(json_response['developers_can_merge']).to eq(false) end + it "returns the branch information for a single branch with dots in the name" do + get api("/projects/#{project.id}/repository/branches/with.1.2.3", user) + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq("with.1.2.3") + end + context 'on a merged branch' do it "returns the branch information for a single branch" do get api("/projects/#{project.id}/repository/branches/merge-test", user) @@ -71,6 +79,14 @@ describe API::Branches, api: true do expect(json_response['developers_can_merge']).to eq(false) end + it "protects a single branch with dots in the name" do + put api("/projects/#{project.id}/repository/branches/with.1.2.3/protect", user) + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq("with.1.2.3") + expect(json_response['protected']).to eq(true) + end + it 'protects a single branch and developers can push' do put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_push: true @@ -220,6 +236,14 @@ describe API::Branches, api: true do expect(json_response['protected']).to eq(false) end + it "update branches with dots in branch name" do + put api("/projects/#{project.id}/repository/branches/with.1.2.3/unprotect", user) + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq("with.1.2.3") + expect(json_response['protected']).to eq(false) + end + it "returns success when unprotect branch" do put api("/projects/#{project.id}/repository/branches/unknown/unprotect", user) expect(response).to have_http_status(404) @@ -292,6 +316,13 @@ describe API::Branches, api: true do expect(json_response['branch_name']).to eq(branch_name) end + it "removes a branch with dots in the branch name" do + delete api("/projects/#{project.id}/repository/branches/with.1.2.3", user) + + expect(response).to have_http_status(200) + expect(json_response['branch_name']).to eq("with.1.2.3") + end + it 'returns 404 if branch not exists' do delete api("/projects/#{project.id}/repository/branches/foobar", user) expect(response).to have_http_status(404) -- cgit v1.2.1 From e948a466249be28655e794d63724e1c003f8621d Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Thu, 8 Dec 2016 17:09:06 -0200 Subject: Displays milestone remaining days only when it's present --- app/views/shared/milestones/_summary.html.haml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml index 0a237136959..d27fba805a3 100644 --- a/app/views/shared/milestones/_summary.html.haml +++ b/app/views/shared/milestones/_summary.html.haml @@ -25,8 +25,10 @@ %span.milestone-stat %strong== #{milestone.percent_complete(current_user)}% complete - %span.milestone-stat - %span.remaining-days= milestone_remaining_days(milestone) + - remaining_days = milestone_remaining_days(milestone) + - if remaining_days.present? + %span.milestone-stat + %span.remaining-days= remaining_days .milestone-progress-buttons %span.tab-issues-buttons -- cgit v1.2.1 From ef848c5ce2beaab7a102a3e8c9484f6aa3461a42 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Thu, 8 Dec 2016 17:09:26 -0200 Subject: Add CHANGELOG entry --- changelogs/unreleased/fix-milestone-summary.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-milestone-summary.yml diff --git a/changelogs/unreleased/fix-milestone-summary.yml b/changelogs/unreleased/fix-milestone-summary.yml new file mode 100644 index 00000000000..3045a15054c --- /dev/null +++ b/changelogs/unreleased/fix-milestone-summary.yml @@ -0,0 +1,4 @@ +--- +title: Displays milestone remaining days only when it's present +merge_request: +author: -- cgit v1.2.1 From 44b082c7cce6d2a48c1511b5ffacd2d41671bad1 Mon Sep 17 00:00:00 2001 From: Ian Baum <ibaum@gitlab.com> Date: Thu, 8 Dec 2016 13:47:16 -0600 Subject: Updating reference to database password postgresql['sql_password'] is not used, should be gitlab_rails['db_password'] --- doc/administration/high_availability/database.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md index 76f3a0fb387..b36cf18d459 100644 --- a/doc/administration/high_availability/database.md +++ b/doc/administration/high_availability/database.md @@ -41,7 +41,7 @@ If you use a cloud-managed service, or provide your own PostgreSQL: mailroom['enable'] = false # PostgreSQL configuration - postgresql['sql_password'] = 'DB password' + gitlab_rails['db_password'] = 'DB password' postgresql['md5_auth_cidr_addresses'] = ['0.0.0.0/0'] postgresql['listen_address'] = '0.0.0.0' ``` @@ -80,7 +80,7 @@ If you use a cloud-managed service, or provide your own PostgreSQL: 1. Similarly, set the password for the `gitlab` database user. Use the same password that you specified in the `/etc/gitlab/gitlab.rb` file for - `postgresql['sql_password']`. + `gitlab_rails['db_password']`. ``` \password gitlab -- cgit v1.2.1 From 4af62042bb7ddeff742a62048438673811e5344a Mon Sep 17 00:00:00 2001 From: Rydkin Maxim <maks.rydkin@gmail.com> Date: Sat, 3 Dec 2016 23:26:02 +0300 Subject: remove unnecessary issues event filter on comments tab fix features_visibility_spec.rb remove strange part of spec fix conditions of comments tab disappearing and fix spec generate changelog rewrite spec for more coplex check step-by-step move conditional logic into helper and fix changelog fix indentation in helper --- app/helpers/events_helper.rb | 6 ++++ app/views/shared/_event_filter.html.haml | 2 +- .../25272_fix_comments_tab_disappearing.yml | 4 +++ spec/features/projects/features_visibility_spec.rb | 38 ++++++++++++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/25272_fix_comments_tab_disappearing.yml diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index f1a0b929d82..362046c0270 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -45,6 +45,12 @@ module EventsHelper @project.feature_available?(feature_key, current_user) end + def comments_visible? + event_filter_visible(:repository) || + event_filter_visible(:merge_requests) || + event_filter_visible(:issues) + end + def event_preposition(event) if event.push? || event.commented? || event.target "at" diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml index 67c145cef17..e50ab5fea09 100644 --- a/app/views/shared/_event_filter.html.haml +++ b/app/views/shared/_event_filter.html.haml @@ -6,6 +6,6 @@ = event_filter_link EventFilter.merged, 'Merge events' - if event_filter_visible(:issues) = event_filter_link EventFilter.issue, 'Issue events' - - if event_filter_visible(:issues) + - if comments_visible? = event_filter_link EventFilter.comments, 'Comments' = event_filter_link EventFilter.team, 'Team' diff --git a/changelogs/unreleased/25272_fix_comments_tab_disappearing.yml b/changelogs/unreleased/25272_fix_comments_tab_disappearing.yml new file mode 100644 index 00000000000..79cb2c6d843 --- /dev/null +++ b/changelogs/unreleased/25272_fix_comments_tab_disappearing.yml @@ -0,0 +1,4 @@ +--- +title: 'Fix comments activity tab visibility condition' +merge_request: 7913 +author: Rydkin Maxim diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb index 09aa6758b5c..3bb33394be7 100644 --- a/spec/features/projects/features_visibility_spec.rb +++ b/spec/features/projects/features_visibility_spec.rb @@ -182,6 +182,44 @@ describe 'Edit Project Settings', feature: true do expect(page).not_to have_content("Comments") end end + + # Regression spec for https://gitlab.com/gitlab-org/gitlab-ce/issues/25272 + it "hides comments activity tab only on disabled issues, merge requests and repository" do + select "Disabled", from: "project_project_feature_attributes_issues_access_level" + + save_changes_and_check_activity_tab do + expect(page).to have_content("Comments") + end + + visit edit_namespace_project_path(project.namespace, project) + + select "Disabled", from: "project_project_feature_attributes_merge_requests_access_level" + + save_changes_and_check_activity_tab do + expect(page).to have_content("Comments") + end + + visit edit_namespace_project_path(project.namespace, project) + + select "Disabled", from: "project_project_feature_attributes_repository_access_level" + + save_changes_and_check_activity_tab do + expect(page).not_to have_content("Comments") + end + + visit edit_namespace_project_path(project.namespace, project) + end + + def save_changes_and_check_activity_tab + click_button "Save changes" + wait_for_ajax + + visit activity_namespace_project_path(project.namespace, project) + + page.within(".event-filter") do + yield + end + end end # Regression spec for https://gitlab.com/gitlab-org/gitlab-ce/issues/24056 -- cgit v1.2.1 From 9ab1fe5e6536e311142d1daddd0d7c8e29eec20a Mon Sep 17 00:00:00 2001 From: Munken <mm.munk@gmail.com> Date: Thu, 8 Dec 2016 21:52:57 +0000 Subject: Working inline math filter --- lib/banzai/filter/inline_math_filter.rb | 29 +++++++++++++++++++++ lib/banzai/pipeline/gfm_pipeline.rb | 1 + spec/lib/banzai/filter/inline_math_filter_spec.rb | 31 +++++++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 lib/banzai/filter/inline_math_filter.rb create mode 100644 spec/lib/banzai/filter/inline_math_filter_spec.rb diff --git a/lib/banzai/filter/inline_math_filter.rb b/lib/banzai/filter/inline_math_filter.rb new file mode 100644 index 00000000000..a63116c12ce --- /dev/null +++ b/lib/banzai/filter/inline_math_filter.rb @@ -0,0 +1,29 @@ +require 'uri' + +module Banzai + module Filter + # HTML filter that adds class="code math" and removes the dolar sign in $`2+2`$. + # + class InlineMathFilter < HTML::Pipeline::Filter + def call + doc.xpath("descendant-or-self::text()[substring(., string-length(.)) = '$']"\ + "/following-sibling::*[name() = 'code']"\ + "/following-sibling::text()[starts-with(.,'$')]").each do |el| + closing = el + code = el.previous + code[:class] = 'code math' + opening = code.previous + + closing.content = closing.content[1..-1] + opening.content = opening.content[0..-2] + + closing + end + + puts doc + + doc + end + end + end +end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 5da2d0b008c..2c81cbe56b3 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -6,6 +6,7 @@ module Banzai Filter::SyntaxHighlightFilter, Filter::SanitizationFilter, + Filter::InlineMathFilter, Filter::UploadLinkFilter, Filter::VideoLinkFilter, Filter::ImageLinkFilter, diff --git a/spec/lib/banzai/filter/inline_math_filter_spec.rb b/spec/lib/banzai/filter/inline_math_filter_spec.rb new file mode 100644 index 00000000000..1edec83a6f2 --- /dev/null +++ b/spec/lib/banzai/filter/inline_math_filter_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Banzai::Filter::InlineMathFilter, lib: true do + include FilterSpecHelper + + it 'leaves regular inline code unchanged' do + doc = filter("<code>2+2</code>") + expect(doc.to_s).to eq "<code>2+2</code>" + end + + it 'removes surrounding dollar signs and adds class' do + doc = filter("$<code>2+2</code>$") + expect(doc.to_s).to eq '<code class="code math">2+2</code>' + end + + it 'only removes surrounding dollar signs' do + doc = filter("test $<code>2+2</code>$ test") + expect(doc.to_s).to eq 'test <code class="code math">2+2</code> test' + end + + it 'only removes surrounding single dollar sign' do + doc = filter("test $$<code>2+2</code>$$ test") + expect(doc.to_s).to eq 'test $<code class="code math">2+2</code>$ test' + end + + it 'ignores cases with missing dolar sign' do + doc = filter("test $<code>2+2</code> test") + expect(doc.to_s).to eq 'test $<code>2+2</code> test' + end + +end -- cgit v1.2.1 From 525c2a782e03afafdf9cf1948ab75e73092704fa Mon Sep 17 00:00:00 2001 From: Munken <mm.munk@gmail.com> Date: Thu, 8 Dec 2016 22:39:37 +0000 Subject: Math works for inline syntax --- app/assets/javascripts/syntax_highlight.js | 30 - app/assets/javascripts/syntax_highlight.js.erb | 75 + lib/banzai/filter/inline_math_filter.rb | 2 - vendor/assets/fonts/KaTeX_AMS-Regular.eot | Bin 0 -> 71656 bytes vendor/assets/fonts/KaTeX_AMS-Regular.ttf | Bin 0 -> 71428 bytes vendor/assets/fonts/KaTeX_AMS-Regular.woff | Bin 0 -> 40200 bytes vendor/assets/fonts/KaTeX_AMS-Regular.woff2 | Bin 0 -> 33188 bytes vendor/assets/fonts/KaTeX_Caligraphic-Bold.eot | Bin 0 -> 19836 bytes vendor/assets/fonts/KaTeX_Caligraphic-Bold.ttf | Bin 0 -> 19588 bytes vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff | Bin 0 -> 12136 bytes vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff2 | Bin 0 -> 10604 bytes vendor/assets/fonts/KaTeX_Caligraphic-Regular.eot | Bin 0 -> 19220 bytes vendor/assets/fonts/KaTeX_Caligraphic-Regular.ttf | Bin 0 -> 18960 bytes vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff | Bin 0 -> 11868 bytes .../assets/fonts/KaTeX_Caligraphic-Regular.woff2 | Bin 0 -> 10396 bytes vendor/assets/fonts/KaTeX_Fraktur-Bold.eot | Bin 0 -> 36200 bytes vendor/assets/fonts/KaTeX_Fraktur-Bold.ttf | Bin 0 -> 35968 bytes vendor/assets/fonts/KaTeX_Fraktur-Bold.woff | Bin 0 -> 23388 bytes vendor/assets/fonts/KaTeX_Fraktur-Bold.woff2 | Bin 0 -> 20476 bytes vendor/assets/fonts/KaTeX_Fraktur-Regular.eot | Bin 0 -> 34896 bytes vendor/assets/fonts/KaTeX_Fraktur-Regular.ttf | Bin 0 -> 34652 bytes vendor/assets/fonts/KaTeX_Fraktur-Regular.woff | Bin 0 -> 22844 bytes vendor/assets/fonts/KaTeX_Fraktur-Regular.woff2 | Bin 0 -> 19868 bytes vendor/assets/fonts/KaTeX_Main-Bold.eot | Bin 0 -> 60688 bytes vendor/assets/fonts/KaTeX_Main-Bold.ttf | Bin 0 -> 60468 bytes vendor/assets/fonts/KaTeX_Main-Bold.woff | Bin 0 -> 35480 bytes vendor/assets/fonts/KaTeX_Main-Bold.woff2 | Bin 0 -> 29492 bytes vendor/assets/fonts/KaTeX_Main-Italic.eot | Bin 0 -> 44132 bytes vendor/assets/fonts/KaTeX_Main-Italic.ttf | Bin 0 -> 43904 bytes vendor/assets/fonts/KaTeX_Main-Italic.woff | Bin 0 -> 24880 bytes vendor/assets/fonts/KaTeX_Main-Italic.woff2 | Bin 0 -> 21032 bytes vendor/assets/fonts/KaTeX_Main-Regular.eot | Bin 0 -> 68228 bytes vendor/assets/fonts/KaTeX_Main-Regular.ttf | Bin 0 -> 67996 bytes vendor/assets/fonts/KaTeX_Main-Regular.woff | Bin 0 -> 37620 bytes vendor/assets/fonts/KaTeX_Main-Regular.woff2 | Bin 0 -> 31220 bytes vendor/assets/fonts/KaTeX_Math-BoldItalic.eot | Bin 0 -> 39990 bytes vendor/assets/fonts/KaTeX_Math-BoldItalic.ttf | Bin 0 -> 39744 bytes vendor/assets/fonts/KaTeX_Math-BoldItalic.woff | Bin 0 -> 23192 bytes vendor/assets/fonts/KaTeX_Math-BoldItalic.woff2 | Bin 0 -> 20036 bytes vendor/assets/fonts/KaTeX_Math-Italic.eot | Bin 0 -> 41676 bytes vendor/assets/fonts/KaTeX_Math-Italic.ttf | Bin 0 -> 41448 bytes vendor/assets/fonts/KaTeX_Math-Italic.woff | Bin 0 -> 23820 bytes vendor/assets/fonts/KaTeX_Math-Italic.woff2 | Bin 0 -> 20432 bytes vendor/assets/fonts/KaTeX_Math-Regular.eot | Bin 0 -> 41536 bytes vendor/assets/fonts/KaTeX_Math-Regular.ttf | Bin 0 -> 41304 bytes vendor/assets/fonts/KaTeX_Math-Regular.woff | Bin 0 -> 23712 bytes vendor/assets/fonts/KaTeX_Math-Regular.woff2 | Bin 0 -> 20344 bytes vendor/assets/fonts/KaTeX_SansSerif-Bold.eot | Bin 0 -> 34204 bytes vendor/assets/fonts/KaTeX_SansSerif-Bold.ttf | Bin 0 -> 33964 bytes vendor/assets/fonts/KaTeX_SansSerif-Bold.woff | Bin 0 -> 19196 bytes vendor/assets/fonts/KaTeX_SansSerif-Bold.woff2 | Bin 0 -> 16020 bytes vendor/assets/fonts/KaTeX_SansSerif-Italic.eot | Bin 0 -> 31320 bytes vendor/assets/fonts/KaTeX_SansSerif-Italic.ttf | Bin 0 -> 31072 bytes vendor/assets/fonts/KaTeX_SansSerif-Italic.woff | Bin 0 -> 18080 bytes vendor/assets/fonts/KaTeX_SansSerif-Italic.woff2 | Bin 0 -> 15152 bytes vendor/assets/fonts/KaTeX_SansSerif-Regular.eot | Bin 0 -> 30212 bytes vendor/assets/fonts/KaTeX_SansSerif-Regular.ttf | Bin 0 -> 29960 bytes vendor/assets/fonts/KaTeX_SansSerif-Regular.woff | Bin 0 -> 16744 bytes vendor/assets/fonts/KaTeX_SansSerif-Regular.woff2 | Bin 0 -> 13908 bytes vendor/assets/fonts/KaTeX_Script-Regular.eot | Bin 0 -> 25104 bytes vendor/assets/fonts/KaTeX_Script-Regular.ttf | Bin 0 -> 24864 bytes vendor/assets/fonts/KaTeX_Script-Regular.woff | Bin 0 -> 13856 bytes vendor/assets/fonts/KaTeX_Script-Regular.woff2 | Bin 0 -> 12276 bytes vendor/assets/fonts/KaTeX_Size1-Regular.eot | Bin 0 -> 13408 bytes vendor/assets/fonts/KaTeX_Size1-Regular.ttf | Bin 0 -> 13172 bytes vendor/assets/fonts/KaTeX_Size1-Regular.woff | Bin 0 -> 6980 bytes vendor/assets/fonts/KaTeX_Size1-Regular.woff2 | Bin 0 -> 5820 bytes vendor/assets/fonts/KaTeX_Size2-Regular.eot | Bin 0 -> 12648 bytes vendor/assets/fonts/KaTeX_Size2-Regular.ttf | Bin 0 -> 12412 bytes vendor/assets/fonts/KaTeX_Size2-Regular.woff | Bin 0 -> 6684 bytes vendor/assets/fonts/KaTeX_Size2-Regular.woff2 | Bin 0 -> 5560 bytes vendor/assets/fonts/KaTeX_Size3-Regular.eot | Bin 0 -> 8596 bytes vendor/assets/fonts/KaTeX_Size3-Regular.ttf | Bin 0 -> 8360 bytes vendor/assets/fonts/KaTeX_Size3-Regular.woff | Bin 0 -> 4776 bytes vendor/assets/fonts/KaTeX_Size3-Regular.woff2 | Bin 0 -> 3856 bytes vendor/assets/fonts/KaTeX_Size4-Regular.eot | Bin 0 -> 11520 bytes vendor/assets/fonts/KaTeX_Size4-Regular.ttf | Bin 0 -> 11284 bytes vendor/assets/fonts/KaTeX_Size4-Regular.woff | Bin 0 -> 6456 bytes vendor/assets/fonts/KaTeX_Size4-Regular.woff2 | Bin 0 -> 5172 bytes vendor/assets/fonts/KaTeX_Typewriter-Regular.eot | Bin 0 -> 35784 bytes vendor/assets/fonts/KaTeX_Typewriter-Regular.ttf | Bin 0 -> 35528 bytes vendor/assets/fonts/KaTeX_Typewriter-Regular.woff | Bin 0 -> 20712 bytes vendor/assets/fonts/KaTeX_Typewriter-Regular.woff2 | Bin 0 -> 17344 bytes vendor/assets/javascripts/katex.js | 8642 ++++++++++++++++++++ vendor/assets/stylesheets/katex.css | 934 +++ 85 files changed, 9651 insertions(+), 32 deletions(-) delete mode 100644 app/assets/javascripts/syntax_highlight.js create mode 100644 app/assets/javascripts/syntax_highlight.js.erb create mode 100644 vendor/assets/fonts/KaTeX_AMS-Regular.eot create mode 100644 vendor/assets/fonts/KaTeX_AMS-Regular.ttf create mode 100644 vendor/assets/fonts/KaTeX_AMS-Regular.woff create mode 100644 vendor/assets/fonts/KaTeX_AMS-Regular.woff2 create mode 100644 vendor/assets/fonts/KaTeX_Caligraphic-Bold.eot create mode 100644 vendor/assets/fonts/KaTeX_Caligraphic-Bold.ttf create mode 100644 vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff create mode 100644 vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff2 create mode 100644 vendor/assets/fonts/KaTeX_Caligraphic-Regular.eot create mode 100644 vendor/assets/fonts/KaTeX_Caligraphic-Regular.ttf create mode 100644 vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff create mode 100644 vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff2 create mode 100644 vendor/assets/fonts/KaTeX_Fraktur-Bold.eot create mode 100644 vendor/assets/fonts/KaTeX_Fraktur-Bold.ttf create mode 100644 vendor/assets/fonts/KaTeX_Fraktur-Bold.woff create mode 100644 vendor/assets/fonts/KaTeX_Fraktur-Bold.woff2 create mode 100644 vendor/assets/fonts/KaTeX_Fraktur-Regular.eot create mode 100644 vendor/assets/fonts/KaTeX_Fraktur-Regular.ttf create mode 100644 vendor/assets/fonts/KaTeX_Fraktur-Regular.woff create mode 100644 vendor/assets/fonts/KaTeX_Fraktur-Regular.woff2 create mode 100644 vendor/assets/fonts/KaTeX_Main-Bold.eot create mode 100644 vendor/assets/fonts/KaTeX_Main-Bold.ttf create mode 100644 vendor/assets/fonts/KaTeX_Main-Bold.woff create mode 100644 vendor/assets/fonts/KaTeX_Main-Bold.woff2 create mode 100644 vendor/assets/fonts/KaTeX_Main-Italic.eot create mode 100644 vendor/assets/fonts/KaTeX_Main-Italic.ttf create mode 100644 vendor/assets/fonts/KaTeX_Main-Italic.woff create mode 100644 vendor/assets/fonts/KaTeX_Main-Italic.woff2 create mode 100644 vendor/assets/fonts/KaTeX_Main-Regular.eot create mode 100644 vendor/assets/fonts/KaTeX_Main-Regular.ttf create mode 100644 vendor/assets/fonts/KaTeX_Main-Regular.woff create mode 100644 vendor/assets/fonts/KaTeX_Main-Regular.woff2 create mode 100644 vendor/assets/fonts/KaTeX_Math-BoldItalic.eot create mode 100644 vendor/assets/fonts/KaTeX_Math-BoldItalic.ttf create mode 100644 vendor/assets/fonts/KaTeX_Math-BoldItalic.woff create mode 100644 vendor/assets/fonts/KaTeX_Math-BoldItalic.woff2 create mode 100644 vendor/assets/fonts/KaTeX_Math-Italic.eot create mode 100644 vendor/assets/fonts/KaTeX_Math-Italic.ttf create mode 100644 vendor/assets/fonts/KaTeX_Math-Italic.woff create mode 100644 vendor/assets/fonts/KaTeX_Math-Italic.woff2 create mode 100644 vendor/assets/fonts/KaTeX_Math-Regular.eot create mode 100644 vendor/assets/fonts/KaTeX_Math-Regular.ttf create mode 100644 vendor/assets/fonts/KaTeX_Math-Regular.woff create mode 100644 vendor/assets/fonts/KaTeX_Math-Regular.woff2 create mode 100644 vendor/assets/fonts/KaTeX_SansSerif-Bold.eot create mode 100644 vendor/assets/fonts/KaTeX_SansSerif-Bold.ttf create mode 100644 vendor/assets/fonts/KaTeX_SansSerif-Bold.woff create mode 100644 vendor/assets/fonts/KaTeX_SansSerif-Bold.woff2 create mode 100644 vendor/assets/fonts/KaTeX_SansSerif-Italic.eot create mode 100644 vendor/assets/fonts/KaTeX_SansSerif-Italic.ttf create mode 100644 vendor/assets/fonts/KaTeX_SansSerif-Italic.woff create mode 100644 vendor/assets/fonts/KaTeX_SansSerif-Italic.woff2 create mode 100644 vendor/assets/fonts/KaTeX_SansSerif-Regular.eot create mode 100644 vendor/assets/fonts/KaTeX_SansSerif-Regular.ttf create mode 100644 vendor/assets/fonts/KaTeX_SansSerif-Regular.woff create mode 100644 vendor/assets/fonts/KaTeX_SansSerif-Regular.woff2 create mode 100644 vendor/assets/fonts/KaTeX_Script-Regular.eot create mode 100644 vendor/assets/fonts/KaTeX_Script-Regular.ttf create mode 100644 vendor/assets/fonts/KaTeX_Script-Regular.woff create mode 100644 vendor/assets/fonts/KaTeX_Script-Regular.woff2 create mode 100644 vendor/assets/fonts/KaTeX_Size1-Regular.eot create mode 100644 vendor/assets/fonts/KaTeX_Size1-Regular.ttf create mode 100644 vendor/assets/fonts/KaTeX_Size1-Regular.woff create mode 100644 vendor/assets/fonts/KaTeX_Size1-Regular.woff2 create mode 100644 vendor/assets/fonts/KaTeX_Size2-Regular.eot create mode 100644 vendor/assets/fonts/KaTeX_Size2-Regular.ttf create mode 100644 vendor/assets/fonts/KaTeX_Size2-Regular.woff create mode 100644 vendor/assets/fonts/KaTeX_Size2-Regular.woff2 create mode 100644 vendor/assets/fonts/KaTeX_Size3-Regular.eot create mode 100644 vendor/assets/fonts/KaTeX_Size3-Regular.ttf create mode 100644 vendor/assets/fonts/KaTeX_Size3-Regular.woff create mode 100644 vendor/assets/fonts/KaTeX_Size3-Regular.woff2 create mode 100644 vendor/assets/fonts/KaTeX_Size4-Regular.eot create mode 100644 vendor/assets/fonts/KaTeX_Size4-Regular.ttf create mode 100644 vendor/assets/fonts/KaTeX_Size4-Regular.woff create mode 100644 vendor/assets/fonts/KaTeX_Size4-Regular.woff2 create mode 100644 vendor/assets/fonts/KaTeX_Typewriter-Regular.eot create mode 100644 vendor/assets/fonts/KaTeX_Typewriter-Regular.ttf create mode 100644 vendor/assets/fonts/KaTeX_Typewriter-Regular.woff create mode 100644 vendor/assets/fonts/KaTeX_Typewriter-Regular.woff2 create mode 100644 vendor/assets/javascripts/katex.js create mode 100644 vendor/assets/stylesheets/katex.css diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js deleted file mode 100644 index bd37d69165f..00000000000 --- a/app/assets/javascripts/syntax_highlight.js +++ /dev/null @@ -1,30 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */ -// Syntax Highlighter -// -// Applies a syntax highlighting color scheme CSS class to any element with the -// `js-syntax-highlight` class -// -// ### Example Markup -// -// <div class="js-syntax-highlight"></div> -// -(function() { - $.fn.syntaxHighlight = function() { - var $children; - if ($(this).hasClass('js-syntax-highlight')) { - // Given the element itself, apply highlighting - return $(this).addClass(gon.user_color_scheme); - } else { - // Given a parent element, recurse to any of its applicable children - $children = $(this).find('.js-syntax-highlight'); - if ($children.length) { - return $children.syntaxHighlight(); - } - } - }; - - $(document).on('ready page:load', function() { - return $('.js-syntax-highlight').syntaxHighlight(); - }); - -}).call(this); diff --git a/app/assets/javascripts/syntax_highlight.js.erb b/app/assets/javascripts/syntax_highlight.js.erb new file mode 100644 index 00000000000..5664c32d7b6 --- /dev/null +++ b/app/assets/javascripts/syntax_highlight.js.erb @@ -0,0 +1,75 @@ +/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */ +// Syntax Highlighter +// +// Applies a syntax highlighting color scheme CSS class to any element with the +// `js-syntax-highlight` class +// +// ### Example Markup +// +// <div class="js-syntax-highlight"></div> +// +(function() { + // CSS and JS for KaTeX + CSS_PATH = "<%= asset_path('katex.css') %>"; + JS_PATH = "<%= asset_path('katex.js') %>"; + + // Only load once + var katexLoaded = false; + + // Loop over all math elements and render math + var renderWithKaTeX = function (elements) { + elements.each(function () { + $(this).hide(); + var mathNode = $( "<math>Test</math>" ); + mathNode.insertAfter($(this)); + + katex.render($(this).text(), mathNode.get(0), { displayMode: false }) + }) + }; + var handleMath = function () { + var mathElements = $('.code.math'); + + if (mathElements.length == 0) return; + + if (katexLoaded) renderWithKaTeX(mathElements); + else { + // Request CSS file so it is in the cache + $.get(CSS_PATH, function(){ + var css = $('<link>', + {rel:'stylesheet', + type:'text/css', + href: CSS_PATH + }); + css.appendTo('head'); + + // Load KaTeX js + $.getScript(JS_PATH, function() { + katexLoaded = true; + renderWithKaTeX(mathElements); // Run KaTeX + }) + }); + } + }; + + $.fn.syntaxHighlight = function() { + var $children; + + handleMath(); + + if ($(this).hasClass('js-syntax-highlight')) { + // Given the element itself, apply highlighting + return $(this).addClass(gon.user_color_scheme); + } else { + // Given a parent element, recurse to any of its applicable children + $children = $(this).find('.js-syntax-highlight'); + if ($children.length) { + return $children.syntaxHighlight(); + } + } + }; + + $(document).on('ready page:load', function() { + return $('.js-syntax-highlight').syntaxHighlight(); + }); + +}).call(this); diff --git a/lib/banzai/filter/inline_math_filter.rb b/lib/banzai/filter/inline_math_filter.rb index a63116c12ce..1bbe602237a 100644 --- a/lib/banzai/filter/inline_math_filter.rb +++ b/lib/banzai/filter/inline_math_filter.rb @@ -20,8 +20,6 @@ module Banzai closing end - puts doc - doc end end diff --git a/vendor/assets/fonts/KaTeX_AMS-Regular.eot b/vendor/assets/fonts/KaTeX_AMS-Regular.eot new file mode 100644 index 00000000000..784276a3cbf Binary files /dev/null and b/vendor/assets/fonts/KaTeX_AMS-Regular.eot differ diff --git a/vendor/assets/fonts/KaTeX_AMS-Regular.ttf b/vendor/assets/fonts/KaTeX_AMS-Regular.ttf new file mode 100644 index 00000000000..6f1e0be2028 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_AMS-Regular.ttf differ diff --git a/vendor/assets/fonts/KaTeX_AMS-Regular.woff b/vendor/assets/fonts/KaTeX_AMS-Regular.woff new file mode 100644 index 00000000000..4dded4733b3 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_AMS-Regular.woff differ diff --git a/vendor/assets/fonts/KaTeX_AMS-Regular.woff2 b/vendor/assets/fonts/KaTeX_AMS-Regular.woff2 new file mode 100644 index 00000000000..ea81079c4e2 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_AMS-Regular.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Bold.eot b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.eot new file mode 100644 index 00000000000..1a0db0c568e Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.eot differ diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Bold.ttf b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.ttf new file mode 100644 index 00000000000..b94907dad11 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff new file mode 100644 index 00000000000..799fa8122ca Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff differ diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff2 b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff2 new file mode 100644 index 00000000000..73bb5422878 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Regular.eot b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.eot new file mode 100644 index 00000000000..6cc83d0922c Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.eot differ diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Regular.ttf b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.ttf new file mode 100644 index 00000000000..cf51e2021e4 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff new file mode 100644 index 00000000000..f5e5c623577 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff differ diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff2 b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff2 new file mode 100644 index 00000000000..dd76d3488d5 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Bold.eot b/vendor/assets/fonts/KaTeX_Fraktur-Bold.eot new file mode 100644 index 00000000000..1960b106656 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Fraktur-Bold.eot differ diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Bold.ttf b/vendor/assets/fonts/KaTeX_Fraktur-Bold.ttf new file mode 100644 index 00000000000..7b0790f1ae8 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Fraktur-Bold.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff b/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff new file mode 100644 index 00000000000..dc325713291 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff differ diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff2 b/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff2 new file mode 100644 index 00000000000..fdc429227ad Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Regular.eot b/vendor/assets/fonts/KaTeX_Fraktur-Regular.eot new file mode 100644 index 00000000000..e4e73796aea Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Fraktur-Regular.eot differ diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Regular.ttf b/vendor/assets/fonts/KaTeX_Fraktur-Regular.ttf new file mode 100644 index 00000000000..063bc0263eb Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Fraktur-Regular.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff b/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff new file mode 100644 index 00000000000..c4b18d863f3 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff differ diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff2 b/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff2 new file mode 100644 index 00000000000..4318d938e26 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Main-Bold.eot b/vendor/assets/fonts/KaTeX_Main-Bold.eot new file mode 100644 index 00000000000..80fbd022363 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Bold.eot differ diff --git a/vendor/assets/fonts/KaTeX_Main-Bold.ttf b/vendor/assets/fonts/KaTeX_Main-Bold.ttf new file mode 100644 index 00000000000..8e10722afae Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Bold.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Main-Bold.woff b/vendor/assets/fonts/KaTeX_Main-Bold.woff new file mode 100644 index 00000000000..43b361a6005 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Bold.woff differ diff --git a/vendor/assets/fonts/KaTeX_Main-Bold.woff2 b/vendor/assets/fonts/KaTeX_Main-Bold.woff2 new file mode 100644 index 00000000000..af57a96c148 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Bold.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Main-Italic.eot b/vendor/assets/fonts/KaTeX_Main-Italic.eot new file mode 100644 index 00000000000..fc770166b5e Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Italic.eot differ diff --git a/vendor/assets/fonts/KaTeX_Main-Italic.ttf b/vendor/assets/fonts/KaTeX_Main-Italic.ttf new file mode 100644 index 00000000000..d124495d7b6 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Italic.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Main-Italic.woff b/vendor/assets/fonts/KaTeX_Main-Italic.woff new file mode 100644 index 00000000000..e623236bc44 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Italic.woff differ diff --git a/vendor/assets/fonts/KaTeX_Main-Italic.woff2 b/vendor/assets/fonts/KaTeX_Main-Italic.woff2 new file mode 100644 index 00000000000..944e9740bdf Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Italic.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Main-Regular.eot b/vendor/assets/fonts/KaTeX_Main-Regular.eot new file mode 100644 index 00000000000..dc60c090c7a Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Regular.eot differ diff --git a/vendor/assets/fonts/KaTeX_Main-Regular.ttf b/vendor/assets/fonts/KaTeX_Main-Regular.ttf new file mode 100644 index 00000000000..da5797ffcce Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Regular.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Main-Regular.woff b/vendor/assets/fonts/KaTeX_Main-Regular.woff new file mode 100644 index 00000000000..37db672e821 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Regular.woff differ diff --git a/vendor/assets/fonts/KaTeX_Main-Regular.woff2 b/vendor/assets/fonts/KaTeX_Main-Regular.woff2 new file mode 100644 index 00000000000..48820424893 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Regular.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Math-BoldItalic.eot b/vendor/assets/fonts/KaTeX_Math-BoldItalic.eot new file mode 100644 index 00000000000..52c8b8c6b40 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-BoldItalic.eot differ diff --git a/vendor/assets/fonts/KaTeX_Math-BoldItalic.ttf b/vendor/assets/fonts/KaTeX_Math-BoldItalic.ttf new file mode 100644 index 00000000000..a8b527c7ef6 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-BoldItalic.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff b/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff new file mode 100644 index 00000000000..8940e0b5801 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff differ diff --git a/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff2 b/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff2 new file mode 100644 index 00000000000..15cf56d3408 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Math-Italic.eot b/vendor/assets/fonts/KaTeX_Math-Italic.eot new file mode 100644 index 00000000000..64c8992c477 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-Italic.eot differ diff --git a/vendor/assets/fonts/KaTeX_Math-Italic.ttf b/vendor/assets/fonts/KaTeX_Math-Italic.ttf new file mode 100644 index 00000000000..06f39d3a299 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-Italic.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Math-Italic.woff b/vendor/assets/fonts/KaTeX_Math-Italic.woff new file mode 100644 index 00000000000..cf3b4b79e5b Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-Italic.woff differ diff --git a/vendor/assets/fonts/KaTeX_Math-Italic.woff2 b/vendor/assets/fonts/KaTeX_Math-Italic.woff2 new file mode 100644 index 00000000000..5f8c4bfa455 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-Italic.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Math-Regular.eot b/vendor/assets/fonts/KaTeX_Math-Regular.eot new file mode 100644 index 00000000000..5521e6a564d Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-Regular.eot differ diff --git a/vendor/assets/fonts/KaTeX_Math-Regular.ttf b/vendor/assets/fonts/KaTeX_Math-Regular.ttf new file mode 100644 index 00000000000..73127082370 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-Regular.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Math-Regular.woff b/vendor/assets/fonts/KaTeX_Math-Regular.woff new file mode 100644 index 00000000000..0e2ebdf18af Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-Regular.woff differ diff --git a/vendor/assets/fonts/KaTeX_Math-Regular.woff2 b/vendor/assets/fonts/KaTeX_Math-Regular.woff2 new file mode 100644 index 00000000000..ebe3d028a34 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-Regular.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Bold.eot b/vendor/assets/fonts/KaTeX_SansSerif-Bold.eot new file mode 100644 index 00000000000..1660e76a2b6 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Bold.eot differ diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Bold.ttf b/vendor/assets/fonts/KaTeX_SansSerif-Bold.ttf new file mode 100644 index 00000000000..dbeb7b92ab5 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Bold.ttf differ diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff b/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff new file mode 100644 index 00000000000..8f144a8bb31 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff differ diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff2 b/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff2 new file mode 100644 index 00000000000..329e85557fa Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Italic.eot b/vendor/assets/fonts/KaTeX_SansSerif-Italic.eot new file mode 100644 index 00000000000..289ae3ff8b7 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Italic.eot differ diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Italic.ttf b/vendor/assets/fonts/KaTeX_SansSerif-Italic.ttf new file mode 100644 index 00000000000..b3a2f38f224 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Italic.ttf differ diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff b/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff new file mode 100644 index 00000000000..bddf7ea6579 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff differ diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff2 b/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff2 new file mode 100644 index 00000000000..5fa767bddd6 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Regular.eot b/vendor/assets/fonts/KaTeX_SansSerif-Regular.eot new file mode 100644 index 00000000000..1b38b98a180 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Regular.eot differ diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Regular.ttf b/vendor/assets/fonts/KaTeX_SansSerif-Regular.ttf new file mode 100644 index 00000000000..e4712f84775 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Regular.ttf differ diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff b/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff new file mode 100644 index 00000000000..33be368048f Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff differ diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff2 b/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff2 new file mode 100644 index 00000000000..4fcb2e29a05 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Script-Regular.eot b/vendor/assets/fonts/KaTeX_Script-Regular.eot new file mode 100644 index 00000000000..7870d7f319b Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Script-Regular.eot differ diff --git a/vendor/assets/fonts/KaTeX_Script-Regular.ttf b/vendor/assets/fonts/KaTeX_Script-Regular.ttf new file mode 100644 index 00000000000..da4d11308ae Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Script-Regular.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Script-Regular.woff b/vendor/assets/fonts/KaTeX_Script-Regular.woff new file mode 100644 index 00000000000..d6ae79f998a Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Script-Regular.woff differ diff --git a/vendor/assets/fonts/KaTeX_Script-Regular.woff2 b/vendor/assets/fonts/KaTeX_Script-Regular.woff2 new file mode 100644 index 00000000000..1b43deb45a8 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Script-Regular.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Size1-Regular.eot b/vendor/assets/fonts/KaTeX_Size1-Regular.eot new file mode 100644 index 00000000000..29950f95ff6 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size1-Regular.eot differ diff --git a/vendor/assets/fonts/KaTeX_Size1-Regular.ttf b/vendor/assets/fonts/KaTeX_Size1-Regular.ttf new file mode 100644 index 00000000000..194466a655d Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size1-Regular.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Size1-Regular.woff b/vendor/assets/fonts/KaTeX_Size1-Regular.woff new file mode 100644 index 00000000000..237f271edd1 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size1-Regular.woff differ diff --git a/vendor/assets/fonts/KaTeX_Size1-Regular.woff2 b/vendor/assets/fonts/KaTeX_Size1-Regular.woff2 new file mode 100644 index 00000000000..39b6f8f746c Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size1-Regular.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Size2-Regular.eot b/vendor/assets/fonts/KaTeX_Size2-Regular.eot new file mode 100644 index 00000000000..b8b0536f967 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size2-Regular.eot differ diff --git a/vendor/assets/fonts/KaTeX_Size2-Regular.ttf b/vendor/assets/fonts/KaTeX_Size2-Regular.ttf new file mode 100644 index 00000000000..b41b66a638f Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size2-Regular.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Size2-Regular.woff b/vendor/assets/fonts/KaTeX_Size2-Regular.woff new file mode 100644 index 00000000000..4a3055854ed Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size2-Regular.woff differ diff --git a/vendor/assets/fonts/KaTeX_Size2-Regular.woff2 b/vendor/assets/fonts/KaTeX_Size2-Regular.woff2 new file mode 100644 index 00000000000..3facec1ab89 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size2-Regular.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Size3-Regular.eot b/vendor/assets/fonts/KaTeX_Size3-Regular.eot new file mode 100644 index 00000000000..576b864fae6 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size3-Regular.eot differ diff --git a/vendor/assets/fonts/KaTeX_Size3-Regular.ttf b/vendor/assets/fonts/KaTeX_Size3-Regular.ttf new file mode 100644 index 00000000000..790ddbbc55f Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size3-Regular.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Size3-Regular.woff b/vendor/assets/fonts/KaTeX_Size3-Regular.woff new file mode 100644 index 00000000000..3a6d062e660 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size3-Regular.woff differ diff --git a/vendor/assets/fonts/KaTeX_Size3-Regular.woff2 b/vendor/assets/fonts/KaTeX_Size3-Regular.woff2 new file mode 100644 index 00000000000..2cffafe5018 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size3-Regular.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Size4-Regular.eot b/vendor/assets/fonts/KaTeX_Size4-Regular.eot new file mode 100644 index 00000000000..c2b045fc3db Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size4-Regular.eot differ diff --git a/vendor/assets/fonts/KaTeX_Size4-Regular.ttf b/vendor/assets/fonts/KaTeX_Size4-Regular.ttf new file mode 100644 index 00000000000..ce660aa7ff9 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size4-Regular.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Size4-Regular.woff b/vendor/assets/fonts/KaTeX_Size4-Regular.woff new file mode 100644 index 00000000000..7826c6c97a1 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size4-Regular.woff differ diff --git a/vendor/assets/fonts/KaTeX_Size4-Regular.woff2 b/vendor/assets/fonts/KaTeX_Size4-Regular.woff2 new file mode 100644 index 00000000000..c92189812d9 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size4-Regular.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Typewriter-Regular.eot b/vendor/assets/fonts/KaTeX_Typewriter-Regular.eot new file mode 100644 index 00000000000..4c178f484a8 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Typewriter-Regular.eot differ diff --git a/vendor/assets/fonts/KaTeX_Typewriter-Regular.ttf b/vendor/assets/fonts/KaTeX_Typewriter-Regular.ttf new file mode 100644 index 00000000000..b0427ad0a56 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Typewriter-Regular.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff b/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff new file mode 100644 index 00000000000..78e990488a9 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff differ diff --git a/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff2 b/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff2 new file mode 100644 index 00000000000..618de99d480 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff2 differ diff --git a/vendor/assets/javascripts/katex.js b/vendor/assets/javascripts/katex.js new file mode 100644 index 00000000000..9596b839832 --- /dev/null +++ b/vendor/assets/javascripts/katex.js @@ -0,0 +1,8642 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.katex = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ +/* eslint no-console:0 */ +/** + * This is the main entry point for KaTeX. Here, we expose functions for + * rendering expressions either to DOM nodes or to markup strings. + * + * We also expose the ParseError class to check if errors thrown from KaTeX are + * errors in the expression, or errors in javascript handling. + */ + +var ParseError = require("./src/ParseError"); +var Settings = require("./src/Settings"); + +var buildTree = require("./src/buildTree"); +var parseTree = require("./src/parseTree"); +var utils = require("./src/utils"); + +/** + * Parse and build an expression, and place that expression in the DOM node + * given. + */ +var render = function(expression, baseNode, options) { + utils.clearNode(baseNode); + + var settings = new Settings(options); + + var tree = parseTree(expression, settings); + var node = buildTree(tree, expression, settings).toNode(); + + baseNode.appendChild(node); +}; + +// KaTeX's styles don't work properly in quirks mode. Print out an error, and +// disable rendering. +if (typeof document !== "undefined") { + if (document.compatMode !== "CSS1Compat") { + typeof console !== "undefined" && console.warn( + "Warning: KaTeX doesn't work in quirks mode. Make sure your " + + "website has a suitable doctype."); + + render = function() { + throw new ParseError("KaTeX doesn't work in quirks mode."); + }; + } +} + +/** + * Parse and build an expression, and return the markup for that. + */ +var renderToString = function(expression, options) { + var settings = new Settings(options); + + var tree = parseTree(expression, settings); + return buildTree(tree, expression, settings).toMarkup(); +}; + +/** + * Parse an expression and return the parse tree. + */ +var generateParseTree = function(expression, options) { + var settings = new Settings(options); + return parseTree(expression, settings); +}; + +module.exports = { + render: render, + renderToString: renderToString, + /** + * NOTE: This method is not currently recommended for public use. + * The internal tree representation is unstable and is very likely + * to change. Use at your own risk. + */ + __parse: generateParseTree, + ParseError: ParseError, +}; + +},{"./src/ParseError":6,"./src/Settings":8,"./src/buildTree":13,"./src/parseTree":22,"./src/utils":25}],2:[function(require,module,exports){ +/** @flow */ + +"use strict"; + +function getRelocatable(re) { + // In the future, this could use a WeakMap instead of an expando. + if (!re.__matchAtRelocatable) { + // Disjunctions are the lowest-precedence operator, so we can make any + // pattern match the empty string by appending `|()` to it: + // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-patterns + var source = re.source + "|()"; + + // We always make the new regex global. + var flags = "g" + (re.ignoreCase ? "i" : "") + (re.multiline ? "m" : "") + (re.unicode ? "u" : "") + // sticky (/.../y) doesn't make sense in conjunction with our relocation + // logic, so we ignore it here. + ; + + re.__matchAtRelocatable = new RegExp(source, flags); + } + return re.__matchAtRelocatable; +} + +function matchAt(re, str, pos) { + if (re.global || re.sticky) { + throw new Error("matchAt(...): Only non-global regexes are supported"); + } + var reloc = getRelocatable(re); + reloc.lastIndex = pos; + var match = reloc.exec(str); + // Last capturing group is our sentinel that indicates whether the regex + // matched at the given location. + if (match[match.length - 1] == null) { + // Original regex matched. + match.length = match.length - 1; + return match; + } else { + return null; + } +} + +module.exports = matchAt; +},{}],3:[function(require,module,exports){ +/** + * The Lexer class handles tokenizing the input in various ways. Since our + * parser expects us to be able to backtrack, the lexer allows lexing from any + * given starting point. + * + * Its main exposed function is the `lex` function, which takes a position to + * lex from and a type of token to lex. It defers to the appropriate `_innerLex` + * function. + * + * The various `_innerLex` functions perform the actual lexing of different + * kinds. + */ + +var matchAt = require("match-at"); + +var ParseError = require("./ParseError"); + +// The main lexer class +function Lexer(input) { + this.input = input; + this.pos = 0; +} + +/** + * The resulting token returned from `lex`. + * + * It consists of the token text plus some position information. + * The position information is essentially a range in an input string, + * but instead of referencing the bare input string, we refer to the lexer. + * That way it is possible to attach extra metadata to the input string, + * like for example a file name or similar. + * + * The position information (all three parameters) is optional, + * so it is OK to construct synthetic tokens if appropriate. + * Not providing available position information may lead to + * degraded error reporting, though. + * + * @param {string} text the text of this token + * @param {number=} start the start offset, zero-based inclusive + * @param {number=} end the end offset, zero-based exclusive + * @param {Lexer=} lexer the lexer which in turn holds the input string + */ +function Token(text, start, end, lexer) { + this.text = text; + this.start = start; + this.end = end; + this.lexer = lexer; +} + +/** + * Given a pair of tokens (this and endToken), compute a “Token” encompassing + * the whole input range enclosed by these two. + * + * @param {Token} endToken last token of the range, inclusive + * @param {string} text the text of the newly constructed token + */ +Token.prototype.range = function(endToken, text) { + if (endToken.lexer !== this.lexer) { + return new Token(text); // sorry, no position information available + } + return new Token(text, this.start, endToken.end, this.lexer); +}; + +/* The following tokenRegex + * - matches typical whitespace (but not NBSP etc.) using its first group + * - does not match any control character \x00-\x1f except whitespace + * - does not match a bare backslash + * - matches any ASCII character except those just mentioned + * - does not match the BMP private use area \uE000-\uF8FF + * - does not match bare surrogate code units + * - matches any BMP character except for those just described + * - matches any valid Unicode surrogate pair + * - matches a backslash followed by one or more letters + * - matches a backslash followed by any BMP character, including newline + * Just because the Lexer matches something doesn't mean it's valid input: + * If there is no matching function or symbol definition, the Parser will + * still reject the input. + */ +var tokenRegex = new RegExp( + "([ \r\n\t]+)|" + // whitespace + "([!-\\[\\]-\u2027\u202A-\uD7FF\uF900-\uFFFF]" + // single codepoint + "|[\uD800-\uDBFF][\uDC00-\uDFFF]" + // surrogate pair + "|\\\\(?:[a-zA-Z]+|[^\uD800-\uDFFF])" + // function name + ")" +); + +/** + * This function lexes a single token. + */ +Lexer.prototype.lex = function() { + var input = this.input; + var pos = this.pos; + if (pos === input.length) { + return new Token("EOF", pos, pos, this); + } + var match = matchAt(tokenRegex, input, pos); + if (match === null) { + throw new ParseError( + "Unexpected character: '" + input[pos] + "'", + new Token(input[pos], pos, pos + 1, this)); + } + var text = match[2] || " "; + var start = this.pos; + this.pos += match[0].length; + var end = this.pos; + return new Token(text, start, end, this); +}; + +module.exports = Lexer; + +},{"./ParseError":6,"match-at":2}],4:[function(require,module,exports){ +/** + * This file contains the “gullet” where macros are expanded + * until only non-macro tokens remain. + */ + +var Lexer = require("./Lexer"); + +function MacroExpander(input, macros) { + this.lexer = new Lexer(input); + this.macros = macros; + this.stack = []; // contains tokens in REVERSE order + this.discardedWhiteSpace = []; +} + +/** + * Recursively expand first token, then return first non-expandable token. + */ +MacroExpander.prototype.nextToken = function() { + for (;;) { + if (this.stack.length === 0) { + this.stack.push(this.lexer.lex()); + } + var topToken = this.stack.pop(); + var name = topToken.text; + if (!(name.charAt(0) === "\\" && this.macros.hasOwnProperty(name))) { + return topToken; + } + var expansion = this.macros[name]; + if (typeof expansion === "string") { + var bodyLexer = new Lexer(expansion); + expansion = []; + var tok = bodyLexer.lex(); + while (tok.text !== "EOF") { + expansion.push(tok); + tok = bodyLexer.lex(); + } + expansion.reverse(); // to fit in with stack using push and pop + this.macros[name] = expansion; + } + this.stack = this.stack.concat(expansion); + } +}; + +MacroExpander.prototype.get = function(ignoreSpace) { + this.discardedWhiteSpace = []; + var token = this.nextToken(); + if (ignoreSpace) { + while (token.text === " ") { + this.discardedWhiteSpace.push(token); + token = this.nextToken(); + } + } + return token; +}; + +/** + * Undo the effect of the preceding call to the get method. + * A call to this method MUST be immediately preceded and immediately followed + * by a call to get. Only used during mode switching, i.e. after one token + * was got in the old mode but should get got again in a new mode + * with possibly different whitespace handling. + */ +MacroExpander.prototype.unget = function(token) { + this.stack.push(token); + while (this.discardedWhiteSpace.length !== 0) { + this.stack.push(this.discardedWhiteSpace.pop()); + } +}; + +module.exports = MacroExpander; + +},{"./Lexer":3}],5:[function(require,module,exports){ +/** + * This file contains information about the options that the Parser carries + * around with it while parsing. Data is held in an `Options` object, and when + * recursing, a new `Options` object can be created with the `.with*` and + * `.reset` functions. + */ + +/** + * This is the main options class. It contains the style, size, color, and font + * of the current parse level. It also contains the style and size of the parent + * parse level, so size changes can be handled efficiently. + * + * Each of the `.with*` and `.reset` functions passes its current style and size + * as the parentStyle and parentSize of the new options class, so parent + * handling is taken care of automatically. + */ +function Options(data) { + this.style = data.style; + this.color = data.color; + this.size = data.size; + this.phantom = data.phantom; + this.font = data.font; + + if (data.parentStyle === undefined) { + this.parentStyle = data.style; + } else { + this.parentStyle = data.parentStyle; + } + + if (data.parentSize === undefined) { + this.parentSize = data.size; + } else { + this.parentSize = data.parentSize; + } +} + +/** + * Returns a new options object with the same properties as "this". Properties + * from "extension" will be copied to the new options object. + */ +Options.prototype.extend = function(extension) { + var data = { + style: this.style, + size: this.size, + color: this.color, + parentStyle: this.style, + parentSize: this.size, + phantom: this.phantom, + font: this.font, + }; + + for (var key in extension) { + if (extension.hasOwnProperty(key)) { + data[key] = extension[key]; + } + } + + return new Options(data); +}; + +/** + * Create a new options object with the given style. + */ +Options.prototype.withStyle = function(style) { + return this.extend({ + style: style, + }); +}; + +/** + * Create a new options object with the given size. + */ +Options.prototype.withSize = function(size) { + return this.extend({ + size: size, + }); +}; + +/** + * Create a new options object with the given color. + */ +Options.prototype.withColor = function(color) { + return this.extend({ + color: color, + }); +}; + +/** + * Create a new options object with "phantom" set to true. + */ +Options.prototype.withPhantom = function() { + return this.extend({ + phantom: true, + }); +}; + +/** + * Create a new options objects with the give font. + */ +Options.prototype.withFont = function(font) { + return this.extend({ + font: font, + }); +}; + +/** + * Create a new options object with the same style, size, and color. This is + * used so that parent style and size changes are handled correctly. + */ +Options.prototype.reset = function() { + return this.extend({}); +}; + +/** + * A map of color names to CSS colors. + * TODO(emily): Remove this when we have real macros + */ +var colorMap = { + "katex-blue": "#6495ed", + "katex-orange": "#ffa500", + "katex-pink": "#ff00af", + "katex-red": "#df0030", + "katex-green": "#28ae7b", + "katex-gray": "gray", + "katex-purple": "#9d38bd", + "katex-blueA": "#ccfaff", + "katex-blueB": "#80f6ff", + "katex-blueC": "#63d9ea", + "katex-blueD": "#11accd", + "katex-blueE": "#0c7f99", + "katex-tealA": "#94fff5", + "katex-tealB": "#26edd5", + "katex-tealC": "#01d1c1", + "katex-tealD": "#01a995", + "katex-tealE": "#208170", + "katex-greenA": "#b6ffb0", + "katex-greenB": "#8af281", + "katex-greenC": "#74cf70", + "katex-greenD": "#1fab54", + "katex-greenE": "#0d923f", + "katex-goldA": "#ffd0a9", + "katex-goldB": "#ffbb71", + "katex-goldC": "#ff9c39", + "katex-goldD": "#e07d10", + "katex-goldE": "#a75a05", + "katex-redA": "#fca9a9", + "katex-redB": "#ff8482", + "katex-redC": "#f9685d", + "katex-redD": "#e84d39", + "katex-redE": "#bc2612", + "katex-maroonA": "#ffbde0", + "katex-maroonB": "#ff92c6", + "katex-maroonC": "#ed5fa6", + "katex-maroonD": "#ca337c", + "katex-maroonE": "#9e034e", + "katex-purpleA": "#ddd7ff", + "katex-purpleB": "#c6b9fc", + "katex-purpleC": "#aa87ff", + "katex-purpleD": "#7854ab", + "katex-purpleE": "#543b78", + "katex-mintA": "#f5f9e8", + "katex-mintB": "#edf2df", + "katex-mintC": "#e0e5cc", + "katex-grayA": "#f6f7f7", + "katex-grayB": "#f0f1f2", + "katex-grayC": "#e3e5e6", + "katex-grayD": "#d6d8da", + "katex-grayE": "#babec2", + "katex-grayF": "#888d93", + "katex-grayG": "#626569", + "katex-grayH": "#3b3e40", + "katex-grayI": "#21242c", + "katex-kaBlue": "#314453", + "katex-kaGreen": "#71B307", +}; + +/** + * Gets the CSS color of the current options object, accounting for the + * `colorMap`. + */ +Options.prototype.getColor = function() { + if (this.phantom) { + return "transparent"; + } else { + return colorMap[this.color] || this.color; + } +}; + +module.exports = Options; + +},{}],6:[function(require,module,exports){ +/** + * This is the ParseError class, which is the main error thrown by KaTeX + * functions when something has gone wrong. This is used to distinguish internal + * errors from errors in the expression that the user provided. + * + * If possible, a caller should provide a Token or ParseNode with information + * about where in the source string the problem occurred. + * + * @param {string} message The error message + * @param {(Token|ParseNode)=} token An object providing position information + */ +function ParseError(message, token) { + var error = "KaTeX parse error: " + message; + var start; + var end; + + if (token && token.lexer && token.start <= token.end) { + // If we have the input and a position, make the error a bit fancier + + // Get the input + var input = token.lexer.input; + + // Prepend some information + start = token.start; + end = token.end; + if (start === input.length) { + error += " at end of input: "; + } else { + error += " at position " + (start + 1) + ": "; + } + + // Underline token in question using combining underscores + var underlined = input.slice(start, end).replace(/[^]/g, "$&\u0332"); + + // Extract some context from the input and add it to the error + var left; + if (start > 15) { + left = "…" + input.slice(start - 15, start); + } else { + left = input.slice(0, start); + } + var right; + if (end + 15 < input.length) { + right = input.slice(end, end + 15) + "…"; + } else { + right = input.slice(end); + } + error += left + underlined + right; + } + + // Some hackery to make ParseError a prototype of Error + // See http://stackoverflow.com/a/8460753 + var self = new Error(error); + self.name = "ParseError"; + self.__proto__ = ParseError.prototype; + + self.position = start; + return self; +} + +// More hackery +ParseError.prototype.__proto__ = Error.prototype; + +module.exports = ParseError; + +},{}],7:[function(require,module,exports){ +/* eslint no-constant-condition:0 */ +var functions = require("./functions"); +var environments = require("./environments"); +var MacroExpander = require("./MacroExpander"); +var symbols = require("./symbols"); +var utils = require("./utils"); +var cjkRegex = require("./unicodeRegexes").cjkRegex; + +var parseData = require("./parseData"); +var ParseError = require("./ParseError"); + +/** + * This file contains the parser used to parse out a TeX expression from the + * input. Since TeX isn't context-free, standard parsers don't work particularly + * well. + * + * The strategy of this parser is as such: + * + * The main functions (the `.parse...` ones) take a position in the current + * parse string to parse tokens from. The lexer (found in Lexer.js, stored at + * this.lexer) also supports pulling out tokens at arbitrary places. When + * individual tokens are needed at a position, the lexer is called to pull out a + * token, which is then used. + * + * The parser has a property called "mode" indicating the mode that + * the parser is currently in. Currently it has to be one of "math" or + * "text", which denotes whether the current environment is a math-y + * one or a text-y one (e.g. inside \text). Currently, this serves to + * limit the functions which can be used in text mode. + * + * The main functions then return an object which contains the useful data that + * was parsed at its given point, and a new position at the end of the parsed + * data. The main functions can call each other and continue the parsing by + * using the returned position as a new starting point. + * + * There are also extra `.handle...` functions, which pull out some reused + * functionality into self-contained functions. + * + * The earlier functions return ParseNodes. + * The later functions (which are called deeper in the parse) sometimes return + * ParseFuncOrArgument, which contain a ParseNode as well as some data about + * whether the parsed object is a function which is missing some arguments, or a + * standalone object which can be used as an argument to another function. + */ + +/** + * Main Parser class + */ +function Parser(input, settings) { + // Create a new macro expander (gullet) and (indirectly via that) also a + // new lexer (mouth) for this parser (stomach, in the language of TeX) + this.gullet = new MacroExpander(input, settings.macros); + // Store the settings for use in parsing + this.settings = settings; +} + +var ParseNode = parseData.ParseNode; + +/** + * An initial function (without its arguments), or an argument to a function. + * The `result` argument should be a ParseNode. + */ +function ParseFuncOrArgument(result, isFunction, token) { + this.result = result; + // Is this a function (i.e. is it something defined in functions.js)? + this.isFunction = isFunction; + this.token = token; +} + +/** + * Checks a result to make sure it has the right type, and throws an + * appropriate error otherwise. + * + * @param {boolean=} consume whether to consume the expected token, + * defaults to true + */ +Parser.prototype.expect = function(text, consume) { + if (this.nextToken.text !== text) { + throw new ParseError( + "Expected '" + text + "', got '" + this.nextToken.text + "'", + this.nextToken + ); + } + if (consume !== false) { + this.consume(); + } +}; + +/** + * Considers the current look ahead token as consumed, + * and fetches the one after that as the new look ahead. + */ +Parser.prototype.consume = function() { + this.nextToken = this.gullet.get(this.mode === "math"); +}; + +Parser.prototype.switchMode = function(newMode) { + this.gullet.unget(this.nextToken); + this.mode = newMode; + this.consume(); +}; + +/** + * Main parsing function, which parses an entire input. + * + * @return {?Array.<ParseNode>} + */ +Parser.prototype.parse = function() { + // Try to parse the input + this.mode = "math"; + this.consume(); + var parse = this.parseInput(); + return parse; +}; + +/** + * Parses an entire input tree. + */ +Parser.prototype.parseInput = function() { + // Parse an expression + var expression = this.parseExpression(false); + // If we succeeded, make sure there's an EOF at the end + this.expect("EOF", false); + return expression; +}; + +var endOfExpression = ["}", "\\end", "\\right", "&", "\\\\", "\\cr"]; + +/** + * Parses an "expression", which is a list of atoms. + * + * @param {boolean} breakOnInfix Should the parsing stop when we hit infix + * nodes? This happens when functions have higher precendence + * than infix nodes in implicit parses. + * + * @param {?string} breakOnTokenText The text of the token that the expression + * should end with, or `null` if something else should end the + * expression. + * + * @return {ParseNode} + */ +Parser.prototype.parseExpression = function(breakOnInfix, breakOnTokenText) { + var body = []; + // Keep adding atoms to the body until we can't parse any more atoms (either + // we reached the end, a }, or a \right) + while (true) { + var lex = this.nextToken; + if (endOfExpression.indexOf(lex.text) !== -1) { + break; + } + if (breakOnTokenText && lex.text === breakOnTokenText) { + break; + } + if (breakOnInfix && functions[lex.text] && functions[lex.text].infix) { + break; + } + var atom = this.parseAtom(); + if (!atom) { + if (!this.settings.throwOnError && lex.text[0] === "\\") { + var errorNode = this.handleUnsupportedCmd(); + body.push(errorNode); + continue; + } + + break; + } + body.push(atom); + } + return this.handleInfixNodes(body); +}; + +/** + * Rewrites infix operators such as \over with corresponding commands such + * as \frac. + * + * There can only be one infix operator per group. If there's more than one + * then the expression is ambiguous. This can be resolved by adding {}. + * + * @returns {Array} + */ +Parser.prototype.handleInfixNodes = function(body) { + var overIndex = -1; + var funcName; + + for (var i = 0; i < body.length; i++) { + var node = body[i]; + if (node.type === "infix") { + if (overIndex !== -1) { + throw new ParseError( + "only one infix operator per group", + node.value.token); + } + overIndex = i; + funcName = node.value.replaceWith; + } + } + + if (overIndex !== -1) { + var numerNode; + var denomNode; + + var numerBody = body.slice(0, overIndex); + var denomBody = body.slice(overIndex + 1); + + if (numerBody.length === 1 && numerBody[0].type === "ordgroup") { + numerNode = numerBody[0]; + } else { + numerNode = new ParseNode("ordgroup", numerBody, this.mode); + } + + if (denomBody.length === 1 && denomBody[0].type === "ordgroup") { + denomNode = denomBody[0]; + } else { + denomNode = new ParseNode("ordgroup", denomBody, this.mode); + } + + var value = this.callFunction( + funcName, [numerNode, denomNode], null); + return [new ParseNode(value.type, value, this.mode)]; + } else { + return body; + } +}; + +// The greediness of a superscript or subscript +var SUPSUB_GREEDINESS = 1; + +/** + * Handle a subscript or superscript with nice errors. + */ +Parser.prototype.handleSupSubscript = function(name) { + var symbolToken = this.nextToken; + var symbol = symbolToken.text; + this.consume(); + var group = this.parseGroup(); + + if (!group) { + if (!this.settings.throwOnError && this.nextToken.text[0] === "\\") { + return this.handleUnsupportedCmd(); + } else { + throw new ParseError( + "Expected group after '" + symbol + "'", + symbolToken + ); + } + } else if (group.isFunction) { + // ^ and _ have a greediness, so handle interactions with functions' + // greediness + var funcGreediness = functions[group.result].greediness; + if (funcGreediness > SUPSUB_GREEDINESS) { + return this.parseFunction(group); + } else { + throw new ParseError( + "Got function '" + group.result + "' with no arguments " + + "as " + name, symbolToken); + } + } else { + return group.result; + } +}; + +/** + * Converts the textual input of an unsupported command into a text node + * contained within a color node whose color is determined by errorColor + */ +Parser.prototype.handleUnsupportedCmd = function() { + var text = this.nextToken.text; + var textordArray = []; + + for (var i = 0; i < text.length; i++) { + textordArray.push(new ParseNode("textord", text[i], "text")); + } + + var textNode = new ParseNode( + "text", + { + body: textordArray, + type: "text", + }, + this.mode); + + var colorNode = new ParseNode( + "color", + { + color: this.settings.errorColor, + value: [textNode], + type: "color", + }, + this.mode); + + this.consume(); + return colorNode; +}; + +/** + * Parses a group with optional super/subscripts. + * + * @return {?ParseNode} + */ +Parser.prototype.parseAtom = function() { + // The body of an atom is an implicit group, so that things like + // \left(x\right)^2 work correctly. + var base = this.parseImplicitGroup(); + + // In text mode, we don't have superscripts or subscripts + if (this.mode === "text") { + return base; + } + + // Note that base may be empty (i.e. null) at this point. + + var superscript; + var subscript; + while (true) { + // Lex the first token + var lex = this.nextToken; + + if (lex.text === "\\limits" || lex.text === "\\nolimits") { + // We got a limit control + if (!base || base.type !== "op") { + throw new ParseError( + "Limit controls must follow a math operator", + lex); + } else { + var limits = lex.text === "\\limits"; + base.value.limits = limits; + base.value.alwaysHandleSupSub = true; + } + this.consume(); + } else if (lex.text === "^") { + // We got a superscript start + if (superscript) { + throw new ParseError("Double superscript", lex); + } + superscript = this.handleSupSubscript("superscript"); + } else if (lex.text === "_") { + // We got a subscript start + if (subscript) { + throw new ParseError("Double subscript", lex); + } + subscript = this.handleSupSubscript("subscript"); + } else if (lex.text === "'") { + // We got a prime + var prime = new ParseNode("textord", "\\prime", this.mode); + + // Many primes can be grouped together, so we handle this here + var primes = [prime]; + this.consume(); + // Keep lexing tokens until we get something that's not a prime + while (this.nextToken.text === "'") { + // For each one, add another prime to the list + primes.push(prime); + this.consume(); + } + // Put them into an ordgroup as the superscript + superscript = new ParseNode("ordgroup", primes, this.mode); + } else { + // If it wasn't ^, _, or ', stop parsing super/subscripts + break; + } + } + + if (superscript || subscript) { + // If we got either a superscript or subscript, create a supsub + return new ParseNode("supsub", { + base: base, + sup: superscript, + sub: subscript, + }, this.mode); + } else { + // Otherwise return the original body + return base; + } +}; + +// A list of the size-changing functions, for use in parseImplicitGroup +var sizeFuncs = [ + "\\tiny", "\\scriptsize", "\\footnotesize", "\\small", "\\normalsize", + "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge", +]; + +// A list of the style-changing functions, for use in parseImplicitGroup +var styleFuncs = [ + "\\displaystyle", "\\textstyle", "\\scriptstyle", "\\scriptscriptstyle", +]; + +/** + * Parses an implicit group, which is a group that starts at the end of a + * specified, and ends right before a higher explicit group ends, or at EOL. It + * is used for functions that appear to affect the current style, like \Large or + * \textrm, where instead of keeping a style we just pretend that there is an + * implicit grouping after it until the end of the group. E.g. + * small text {\Large large text} small text again + * It is also used for \left and \right to get the correct grouping. + * + * @return {?ParseNode} + */ +Parser.prototype.parseImplicitGroup = function() { + var start = this.parseSymbol(); + + if (start == null) { + // If we didn't get anything we handle, fall back to parseFunction + return this.parseFunction(); + } + + var func = start.result; + var body; + + if (func === "\\left") { + // If we see a left: + // Parse the entire left function (including the delimiter) + var left = this.parseFunction(start); + // Parse out the implicit body + body = this.parseExpression(false); + // Check the next token + this.expect("\\right", false); + var right = this.parseFunction(); + return new ParseNode("leftright", { + body: body, + left: left.value.value, + right: right.value.value, + }, this.mode); + } else if (func === "\\begin") { + // begin...end is similar to left...right + var begin = this.parseFunction(start); + var envName = begin.value.name; + if (!environments.hasOwnProperty(envName)) { + throw new ParseError( + "No such environment: " + envName, begin.value.nameGroup); + } + // Build the environment object. Arguments and other information will + // be made available to the begin and end methods using properties. + var env = environments[envName]; + var args = this.parseArguments("\\begin{" + envName + "}", env); + var context = { + mode: this.mode, + envName: envName, + parser: this, + positions: args.pop(), + }; + var result = env.handler(context, args); + this.expect("\\end", false); + var endNameToken = this.nextToken; + var end = this.parseFunction(); + if (end.value.name !== envName) { + throw new ParseError( + "Mismatch: \\begin{" + envName + "} matched " + + "by \\end{" + end.value.name + "}", + endNameToken); + } + result.position = end.position; + return result; + } else if (utils.contains(sizeFuncs, func)) { + // If we see a sizing function, parse out the implict body + body = this.parseExpression(false); + return new ParseNode("sizing", { + // Figure out what size to use based on the list of functions above + size: "size" + (utils.indexOf(sizeFuncs, func) + 1), + value: body, + }, this.mode); + } else if (utils.contains(styleFuncs, func)) { + // If we see a styling function, parse out the implict body + body = this.parseExpression(true); + return new ParseNode("styling", { + // Figure out what style to use by pulling out the style from + // the function name + style: func.slice(1, func.length - 5), + value: body, + }, this.mode); + } else { + // Defer to parseFunction if it's not a function we handle + return this.parseFunction(start); + } +}; + +/** + * Parses an entire function, including its base and all of its arguments. + * The base might either have been parsed already, in which case + * it is provided as an argument, or it's the next group in the input. + * + * @param {ParseFuncOrArgument=} baseGroup optional as described above + * @return {?ParseNode} + */ +Parser.prototype.parseFunction = function(baseGroup) { + if (!baseGroup) { + baseGroup = this.parseGroup(); + } + + if (baseGroup) { + if (baseGroup.isFunction) { + var func = baseGroup.result; + var funcData = functions[func]; + if (this.mode === "text" && !funcData.allowedInText) { + throw new ParseError( + "Can't use function '" + func + "' in text mode", + baseGroup.token); + } + + var args = this.parseArguments(func, funcData); + var token = baseGroup.token; + var result = this.callFunction(func, args, args.pop(), token); + return new ParseNode(result.type, result, this.mode); + } else { + return baseGroup.result; + } + } else { + return null; + } +}; + +/** + * Call a function handler with a suitable context and arguments. + */ +Parser.prototype.callFunction = function(name, args, positions, token) { + var context = { + funcName: name, + parser: this, + positions: positions, + token: token, + }; + return functions[name].handler(context, args); +}; + +/** + * Parses the arguments of a function or environment + * + * @param {string} func "\name" or "\begin{name}" + * @param {{numArgs:number,numOptionalArgs:number|undefined}} funcData + * @return the array of arguments, with the list of positions as last element + */ +Parser.prototype.parseArguments = function(func, funcData) { + var totalArgs = funcData.numArgs + funcData.numOptionalArgs; + if (totalArgs === 0) { + return [[this.pos]]; + } + + var baseGreediness = funcData.greediness; + var positions = [this.pos]; + var args = []; + + for (var i = 0; i < totalArgs; i++) { + var nextToken = this.nextToken; + var argType = funcData.argTypes && funcData.argTypes[i]; + var arg; + if (i < funcData.numOptionalArgs) { + if (argType) { + arg = this.parseGroupOfType(argType, true); + } else { + arg = this.parseGroup(true); + } + if (!arg) { + args.push(null); + positions.push(this.pos); + continue; + } + } else { + if (argType) { + arg = this.parseGroupOfType(argType); + } else { + arg = this.parseGroup(); + } + if (!arg) { + if (!this.settings.throwOnError && + this.nextToken.text[0] === "\\") { + arg = new ParseFuncOrArgument( + this.handleUnsupportedCmd(this.nextToken.text), + false); + } else { + throw new ParseError( + "Expected group after '" + func + "'", nextToken); + } + } + } + var argNode; + if (arg.isFunction) { + var argGreediness = + functions[arg.result].greediness; + if (argGreediness > baseGreediness) { + argNode = this.parseFunction(arg); + } else { + throw new ParseError( + "Got function '" + arg.result + "' as " + + "argument to '" + func + "'", nextToken); + } + } else { + argNode = arg.result; + } + args.push(argNode); + positions.push(this.pos); + } + + args.push(positions); + + return args; +}; + + +/** + * Parses a group when the mode is changing. + * + * @return {?ParseFuncOrArgument} + */ +Parser.prototype.parseGroupOfType = function(innerMode, optional) { + var outerMode = this.mode; + // Handle `original` argTypes + if (innerMode === "original") { + innerMode = outerMode; + } + + if (innerMode === "color") { + return this.parseColorGroup(optional); + } + if (innerMode === "size") { + return this.parseSizeGroup(optional); + } + + this.switchMode(innerMode); + if (innerMode === "text") { + // text mode is special because it should ignore the whitespace before + // it + while (this.nextToken.text === " ") { + this.consume(); + } + } + // By the time we get here, innerMode is one of "text" or "math". + // We switch the mode of the parser, recurse, then restore the old mode. + var res = this.parseGroup(optional); + this.switchMode(outerMode); + return res; +}; + +/** + * Parses a group, essentially returning the string formed by the + * brace-enclosed tokens plus some position information. + * + * @param {string} modeName Used to describe the mode in error messages + * @param {boolean=} optional Whether the group is optional or required + */ +Parser.prototype.parseStringGroup = function(modeName, optional) { + if (optional && this.nextToken.text !== "[") { + return null; + } + var outerMode = this.mode; + this.mode = "text"; + this.expect(optional ? "[" : "{"); + var str = ""; + var firstToken = this.nextToken; + var lastToken = firstToken; + while (this.nextToken.text !== (optional ? "]" : "}")) { + if (this.nextToken.text === "EOF") { + throw new ParseError( + "Unexpected end of input in " + modeName, + firstToken.range(this.nextToken, str)); + } + lastToken = this.nextToken; + str += lastToken.text; + this.consume(); + } + this.mode = outerMode; + this.expect(optional ? "]" : "}"); + return firstToken.range(lastToken, str); +}; + +/** + * Parses a color description. + */ +Parser.prototype.parseColorGroup = function(optional) { + var res = this.parseStringGroup("color", optional); + if (!res) { + return null; + } + var match = (/^(#[a-z0-9]+|[a-z]+)$/i).exec(res.text); + if (!match) { + throw new ParseError("Invalid color: '" + res.text + "'", res); + } + return new ParseFuncOrArgument( + new ParseNode("color", match[0], this.mode), + false); +}; + +/** + * Parses a size specification, consisting of magnitude and unit. + */ +Parser.prototype.parseSizeGroup = function(optional) { + var res = this.parseStringGroup("size", optional); + if (!res) { + return null; + } + var match = (/(-?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/).exec(res.text); + if (!match) { + throw new ParseError("Invalid size: '" + res.text + "'", res); + } + var data = { + number: +(match[1] + match[2]), // sign + magnitude, cast to number + unit: match[3], + }; + if (data.unit !== "em" && data.unit !== "ex") { + throw new ParseError("Invalid unit: '" + data.unit + "'", res); + } + return new ParseFuncOrArgument( + new ParseNode("color", data, this.mode), + false); +}; + +/** + * If the argument is false or absent, this parses an ordinary group, + * which is either a single nucleus (like "x") or an expression + * in braces (like "{x+y}"). + * If the argument is true, it parses either a bracket-delimited expression + * (like "[x+y]") or returns null to indicate the absence of a + * bracket-enclosed group. + * + * @param {boolean=} optional Whether the group is optional or required + * @return {?ParseFuncOrArgument} + */ +Parser.prototype.parseGroup = function(optional) { + var firstToken = this.nextToken; + // Try to parse an open brace + if (this.nextToken.text === (optional ? "[" : "{")) { + // If we get a brace, parse an expression + this.consume(); + var expression = this.parseExpression(false, optional ? "]" : null); + var lastToken = this.nextToken; + // Make sure we get a close brace + this.expect(optional ? "]" : "}"); + if (this.mode === "text") { + this.formLigatures(expression); + } + return new ParseFuncOrArgument( + new ParseNode("ordgroup", expression, this.mode, + firstToken, lastToken), + false); + } else { + // Otherwise, just return a nucleus, or nothing for an optional group + return optional ? null : this.parseSymbol(); + } +}; + +/** + * Form ligature-like combinations of characters for text mode. + * This includes inputs like "--", "---", "``" and "''". + * The result will simply replace multiple textord nodes with a single + * character in each value by a single textord node having multiple + * characters in its value. The representation is still ASCII source. + * + * @param {Array.<ParseNode>} group the nodes of this group, + * list will be moified in place + */ +Parser.prototype.formLigatures = function(group) { + var i; + var n = group.length - 1; + for (i = 0; i < n; ++i) { + var a = group[i]; + var v = a.value; + if (v === "-" && group[i + 1].value === "-") { + if (i + 1 < n && group[i + 2].value === "-") { + group.splice(i, 3, new ParseNode( + "textord", "---", "text", a, group[i + 2])); + n -= 2; + } else { + group.splice(i, 2, new ParseNode( + "textord", "--", "text", a, group[i + 1])); + n -= 1; + } + } + if ((v === "'" || v === "`") && group[i + 1].value === v) { + group.splice(i, 2, new ParseNode( + "textord", v + v, "text", a, group[i + 1])); + n -= 1; + } + } +}; + +/** + * Parse a single symbol out of the string. Here, we handle both the functions + * we have defined, as well as the single character symbols + * + * @return {?ParseFuncOrArgument} + */ +Parser.prototype.parseSymbol = function() { + var nucleus = this.nextToken; + + if (functions[nucleus.text]) { + this.consume(); + // If there exists a function with this name, we return the function and + // say that it is a function. + return new ParseFuncOrArgument( + nucleus.text, + true, nucleus); + } else if (symbols[this.mode][nucleus.text]) { + this.consume(); + // Otherwise if this is a no-argument function, find the type it + // corresponds to in the symbols map + return new ParseFuncOrArgument( + new ParseNode(symbols[this.mode][nucleus.text].group, + nucleus.text, this.mode, nucleus), + false, nucleus); + } else if (this.mode === "text" && cjkRegex.test(nucleus.text)) { + this.consume(); + return new ParseFuncOrArgument( + new ParseNode("textord", nucleus.text, this.mode, nucleus), + false, nucleus); + } else { + return null; + } +}; + +Parser.prototype.ParseNode = ParseNode; + +module.exports = Parser; + +},{"./MacroExpander":4,"./ParseError":6,"./environments":16,"./functions":19,"./parseData":21,"./symbols":23,"./unicodeRegexes":24,"./utils":25}],8:[function(require,module,exports){ +/** + * This is a module for storing settings passed into KaTeX. It correctly handles + * default settings. + */ + +/** + * Helper function for getting a default value if the value is undefined + */ +function get(option, defaultValue) { + return option === undefined ? defaultValue : option; +} + +/** + * The main Settings object + * + * The current options stored are: + * - displayMode: Whether the expression should be typeset by default in + * textstyle or displaystyle (default false) + */ +function Settings(options) { + // allow null options + options = options || {}; + this.displayMode = get(options.displayMode, false); + this.throwOnError = get(options.throwOnError, true); + this.errorColor = get(options.errorColor, "#cc0000"); + this.macros = options.macros || {}; +} + +module.exports = Settings; + +},{}],9:[function(require,module,exports){ +/** + * This file contains information and classes for the various kinds of styles + * used in TeX. It provides a generic `Style` class, which holds information + * about a specific style. It then provides instances of all the different kinds + * of styles possible, and provides functions to move between them and get + * information about them. + */ + +/** + * The main style class. Contains a unique id for the style, a size (which is + * the same for cramped and uncramped version of a style), a cramped flag, and a + * size multiplier, which gives the size difference between a style and + * textstyle. + */ +function Style(id, size, multiplier, cramped) { + this.id = id; + this.size = size; + this.cramped = cramped; + this.sizeMultiplier = multiplier; +} + +/** + * Get the style of a superscript given a base in the current style. + */ +Style.prototype.sup = function() { + return styles[sup[this.id]]; +}; + +/** + * Get the style of a subscript given a base in the current style. + */ +Style.prototype.sub = function() { + return styles[sub[this.id]]; +}; + +/** + * Get the style of a fraction numerator given the fraction in the current + * style. + */ +Style.prototype.fracNum = function() { + return styles[fracNum[this.id]]; +}; + +/** + * Get the style of a fraction denominator given the fraction in the current + * style. + */ +Style.prototype.fracDen = function() { + return styles[fracDen[this.id]]; +}; + +/** + * Get the cramped version of a style (in particular, cramping a cramped style + * doesn't change the style). + */ +Style.prototype.cramp = function() { + return styles[cramp[this.id]]; +}; + +/** + * HTML class name, like "displaystyle cramped" + */ +Style.prototype.cls = function() { + return sizeNames[this.size] + (this.cramped ? " cramped" : " uncramped"); +}; + +/** + * HTML Reset class name, like "reset-textstyle" + */ +Style.prototype.reset = function() { + return resetNames[this.size]; +}; + +// IDs of the different styles +var D = 0; +var Dc = 1; +var T = 2; +var Tc = 3; +var S = 4; +var Sc = 5; +var SS = 6; +var SSc = 7; + +// String names for the different sizes +var sizeNames = [ + "displaystyle textstyle", + "textstyle", + "scriptstyle", + "scriptscriptstyle", +]; + +// Reset names for the different sizes +var resetNames = [ + "reset-textstyle", + "reset-textstyle", + "reset-scriptstyle", + "reset-scriptscriptstyle", +]; + +// Instances of the different styles +var styles = [ + new Style(D, 0, 1.0, false), + new Style(Dc, 0, 1.0, true), + new Style(T, 1, 1.0, false), + new Style(Tc, 1, 1.0, true), + new Style(S, 2, 0.7, false), + new Style(Sc, 2, 0.7, true), + new Style(SS, 3, 0.5, false), + new Style(SSc, 3, 0.5, true), +]; + +// Lookup tables for switching from one style to another +var sup = [S, Sc, S, Sc, SS, SSc, SS, SSc]; +var sub = [Sc, Sc, Sc, Sc, SSc, SSc, SSc, SSc]; +var fracNum = [T, Tc, S, Sc, SS, SSc, SS, SSc]; +var fracDen = [Tc, Tc, Sc, Sc, SSc, SSc, SSc, SSc]; +var cramp = [Dc, Dc, Tc, Tc, Sc, Sc, SSc, SSc]; + +// We only export some of the styles. Also, we don't export the `Style` class so +// no more styles can be generated. +module.exports = { + DISPLAY: styles[D], + TEXT: styles[T], + SCRIPT: styles[S], + SCRIPTSCRIPT: styles[SS], +}; + +},{}],10:[function(require,module,exports){ +/* eslint no-console:0 */ +/** + * This module contains general functions that can be used for building + * different kinds of domTree nodes in a consistent manner. + */ + +var domTree = require("./domTree"); +var fontMetrics = require("./fontMetrics"); +var symbols = require("./symbols"); +var utils = require("./utils"); + +var greekCapitals = [ + "\\Gamma", + "\\Delta", + "\\Theta", + "\\Lambda", + "\\Xi", + "\\Pi", + "\\Sigma", + "\\Upsilon", + "\\Phi", + "\\Psi", + "\\Omega", +]; + +// The following have to be loaded from Main-Italic font, using class mainit +var mainitLetters = [ + "\u0131", // dotless i, \imath + "\u0237", // dotless j, \jmath + "\u00a3", // \pounds +]; + +/** + * Makes a symbolNode after translation via the list of symbols in symbols.js. + * Correctly pulls out metrics for the character, and optionally takes a list of + * classes to be attached to the node. + */ +var makeSymbol = function(value, style, mode, color, classes) { + // Replace the value with its replaced value from symbol.js + if (symbols[mode][value] && symbols[mode][value].replace) { + value = symbols[mode][value].replace; + } + + var metrics = fontMetrics.getCharacterMetrics(value, style); + + var symbolNode; + if (metrics) { + symbolNode = new domTree.symbolNode( + value, metrics.height, metrics.depth, metrics.italic, metrics.skew, + classes); + } else { + // TODO(emily): Figure out a good way to only print this in development + typeof console !== "undefined" && console.warn( + "No character metrics for '" + value + "' in style '" + + style + "'"); + symbolNode = new domTree.symbolNode(value, 0, 0, 0, 0, classes); + } + + if (color) { + symbolNode.style.color = color; + } + + return symbolNode; +}; + +/** + * Makes a symbol in Main-Regular or AMS-Regular. + * Used for rel, bin, open, close, inner, and punct. + */ +var mathsym = function(value, mode, color, classes) { + // Decide what font to render the symbol in by its entry in the symbols + // table. + // Have a special case for when the value = \ because the \ is used as a + // textord in unsupported command errors but cannot be parsed as a regular + // text ordinal and is therefore not present as a symbol in the symbols + // table for text + if (value === "\\" || symbols[mode][value].font === "main") { + return makeSymbol(value, "Main-Regular", mode, color, classes); + } else { + return makeSymbol( + value, "AMS-Regular", mode, color, classes.concat(["amsrm"])); + } +}; + +/** + * Makes a symbol in the default font for mathords and textords. + */ +var mathDefault = function(value, mode, color, classes, type) { + if (type === "mathord") { + return mathit(value, mode, color, classes); + } else if (type === "textord") { + return makeSymbol( + value, "Main-Regular", mode, color, classes.concat(["mathrm"])); + } else { + throw new Error("unexpected type: " + type + " in mathDefault"); + } +}; + +/** + * Makes a symbol in the italic math font. + */ +var mathit = function(value, mode, color, classes) { + if (/[0-9]/.test(value.charAt(0)) || + // glyphs for \imath and \jmath do not exist in Math-Italic so we + // need to use Main-Italic instead + utils.contains(mainitLetters, value) || + utils.contains(greekCapitals, value)) { + return makeSymbol( + value, "Main-Italic", mode, color, classes.concat(["mainit"])); + } else { + return makeSymbol( + value, "Math-Italic", mode, color, classes.concat(["mathit"])); + } +}; + +/** + * Makes either a mathord or textord in the correct font and color. + */ +var makeOrd = function(group, options, type) { + var mode = group.mode; + var value = group.value; + if (symbols[mode][value] && symbols[mode][value].replace) { + value = symbols[mode][value].replace; + } + + var classes = ["mord"]; + var color = options.getColor(); + + var font = options.font; + if (font) { + if (font === "mathit" || utils.contains(mainitLetters, value)) { + return mathit(value, mode, color, classes); + } else { + var fontName = fontMap[font].fontName; + if (fontMetrics.getCharacterMetrics(value, fontName)) { + return makeSymbol( + value, fontName, mode, color, classes.concat([font])); + } else { + return mathDefault(value, mode, color, classes, type); + } + } + } else { + return mathDefault(value, mode, color, classes, type); + } +}; + +/** + * Calculate the height, depth, and maxFontSize of an element based on its + * children. + */ +var sizeElementFromChildren = function(elem) { + var height = 0; + var depth = 0; + var maxFontSize = 0; + + if (elem.children) { + for (var i = 0; i < elem.children.length; i++) { + if (elem.children[i].height > height) { + height = elem.children[i].height; + } + if (elem.children[i].depth > depth) { + depth = elem.children[i].depth; + } + if (elem.children[i].maxFontSize > maxFontSize) { + maxFontSize = elem.children[i].maxFontSize; + } + } + } + + elem.height = height; + elem.depth = depth; + elem.maxFontSize = maxFontSize; +}; + +/** + * Makes a span with the given list of classes, list of children, and color. + */ +var makeSpan = function(classes, children, color) { + var span = new domTree.span(classes, children); + + sizeElementFromChildren(span); + + if (color) { + span.style.color = color; + } + + return span; +}; + +/** + * Makes a document fragment with the given list of children. + */ +var makeFragment = function(children) { + var fragment = new domTree.documentFragment(children); + + sizeElementFromChildren(fragment); + + return fragment; +}; + +/** + * Makes an element placed in each of the vlist elements to ensure that each + * element has the same max font size. To do this, we create a zero-width space + * with the correct font size. + */ +var makeFontSizer = function(options, fontSize) { + var fontSizeInner = makeSpan([], [new domTree.symbolNode("\u200b")]); + fontSizeInner.style.fontSize = + (fontSize / options.style.sizeMultiplier) + "em"; + + var fontSizer = makeSpan( + ["fontsize-ensurer", "reset-" + options.size, "size5"], + [fontSizeInner]); + + return fontSizer; +}; + +/** + * Makes a vertical list by stacking elements and kerns on top of each other. + * Allows for many different ways of specifying the positioning method. + * + * Arguments: + * - children: A list of child or kern nodes to be stacked on top of each other + * (i.e. the first element will be at the bottom, and the last at + * the top). Element nodes are specified as + * {type: "elem", elem: node} + * while kern nodes are specified as + * {type: "kern", size: size} + * - positionType: The method by which the vlist should be positioned. Valid + * values are: + * - "individualShift": The children list only contains elem + * nodes, and each node contains an extra + * "shift" value of how much it should be + * shifted (note that shifting is always + * moving downwards). positionData is + * ignored. + * - "top": The positionData specifies the topmost point of + * the vlist (note this is expected to be a height, + * so positive values move up) + * - "bottom": The positionData specifies the bottommost point + * of the vlist (note this is expected to be a + * depth, so positive values move down + * - "shift": The vlist will be positioned such that its + * baseline is positionData away from the baseline + * of the first child. Positive values move + * downwards. + * - "firstBaseline": The vlist will be positioned such that + * its baseline is aligned with the + * baseline of the first child. + * positionData is ignored. (this is + * equivalent to "shift" with + * positionData=0) + * - positionData: Data used in different ways depending on positionType + * - options: An Options object + * + */ +var makeVList = function(children, positionType, positionData, options) { + var depth; + var currPos; + var i; + if (positionType === "individualShift") { + var oldChildren = children; + children = [oldChildren[0]]; + + // Add in kerns to the list of children to get each element to be + // shifted to the correct specified shift + depth = -oldChildren[0].shift - oldChildren[0].elem.depth; + currPos = depth; + for (i = 1; i < oldChildren.length; i++) { + var diff = -oldChildren[i].shift - currPos - + oldChildren[i].elem.depth; + var size = diff - + (oldChildren[i - 1].elem.height + + oldChildren[i - 1].elem.depth); + + currPos = currPos + diff; + + children.push({type: "kern", size: size}); + children.push(oldChildren[i]); + } + } else if (positionType === "top") { + // We always start at the bottom, so calculate the bottom by adding up + // all the sizes + var bottom = positionData; + for (i = 0; i < children.length; i++) { + if (children[i].type === "kern") { + bottom -= children[i].size; + } else { + bottom -= children[i].elem.height + children[i].elem.depth; + } + } + depth = bottom; + } else if (positionType === "bottom") { + depth = -positionData; + } else if (positionType === "shift") { + depth = -children[0].elem.depth - positionData; + } else if (positionType === "firstBaseline") { + depth = -children[0].elem.depth; + } else { + depth = 0; + } + + // Make the fontSizer + var maxFontSize = 0; + for (i = 0; i < children.length; i++) { + if (children[i].type === "elem") { + maxFontSize = Math.max(maxFontSize, children[i].elem.maxFontSize); + } + } + var fontSizer = makeFontSizer(options, maxFontSize); + + // Create a new list of actual children at the correct offsets + var realChildren = []; + currPos = depth; + for (i = 0; i < children.length; i++) { + if (children[i].type === "kern") { + currPos += children[i].size; + } else { + var child = children[i].elem; + + var shift = -child.depth - currPos; + currPos += child.height + child.depth; + + var childWrap = makeSpan([], [fontSizer, child]); + childWrap.height -= shift; + childWrap.depth += shift; + childWrap.style.top = shift + "em"; + + realChildren.push(childWrap); + } + } + + // Add in an element at the end with no offset to fix the calculation of + // baselines in some browsers (namely IE, sometimes safari) + var baselineFix = makeSpan( + ["baseline-fix"], [fontSizer, new domTree.symbolNode("\u200b")]); + realChildren.push(baselineFix); + + var vlist = makeSpan(["vlist"], realChildren); + // Fix the final height and depth, in case there were kerns at the ends + // since the makeSpan calculation won't take that in to account. + vlist.height = Math.max(currPos, vlist.height); + vlist.depth = Math.max(-depth, vlist.depth); + return vlist; +}; + +// A table of size -> font size for the different sizing functions +var sizingMultiplier = { + size1: 0.5, + size2: 0.7, + size3: 0.8, + size4: 0.9, + size5: 1.0, + size6: 1.2, + size7: 1.44, + size8: 1.73, + size9: 2.07, + size10: 2.49, +}; + +// A map of spacing functions to their attributes, like size and corresponding +// CSS class +var spacingFunctions = { + "\\qquad": { + size: "2em", + className: "qquad", + }, + "\\quad": { + size: "1em", + className: "quad", + }, + "\\enspace": { + size: "0.5em", + className: "enspace", + }, + "\\;": { + size: "0.277778em", + className: "thickspace", + }, + "\\:": { + size: "0.22222em", + className: "mediumspace", + }, + "\\,": { + size: "0.16667em", + className: "thinspace", + }, + "\\!": { + size: "-0.16667em", + className: "negativethinspace", + }, +}; + +/** + * Maps TeX font commands to objects containing: + * - variant: string used for "mathvariant" attribute in buildMathML.js + * - fontName: the "style" parameter to fontMetrics.getCharacterMetrics + */ +// A map between tex font commands an MathML mathvariant attribute values +var fontMap = { + // styles + "mathbf": { + variant: "bold", + fontName: "Main-Bold", + }, + "mathrm": { + variant: "normal", + fontName: "Main-Regular", + }, + + // "mathit" is missing because it requires the use of two fonts: Main-Italic + // and Math-Italic. This is handled by a special case in makeOrd which ends + // up calling mathit. + + // families + "mathbb": { + variant: "double-struck", + fontName: "AMS-Regular", + }, + "mathcal": { + variant: "script", + fontName: "Caligraphic-Regular", + }, + "mathfrak": { + variant: "fraktur", + fontName: "Fraktur-Regular", + }, + "mathscr": { + variant: "script", + fontName: "Script-Regular", + }, + "mathsf": { + variant: "sans-serif", + fontName: "SansSerif-Regular", + }, + "mathtt": { + variant: "monospace", + fontName: "Typewriter-Regular", + }, +}; + +module.exports = { + fontMap: fontMap, + makeSymbol: makeSymbol, + mathsym: mathsym, + makeSpan: makeSpan, + makeFragment: makeFragment, + makeVList: makeVList, + makeOrd: makeOrd, + sizingMultiplier: sizingMultiplier, + spacingFunctions: spacingFunctions, +}; + +},{"./domTree":15,"./fontMetrics":17,"./symbols":23,"./utils":25}],11:[function(require,module,exports){ +/* eslint no-console:0 */ +/** + * This file does the main work of building a domTree structure from a parse + * tree. The entry point is the `buildHTML` function, which takes a parse tree. + * Then, the buildExpression, buildGroup, and various groupTypes functions are + * called, to produce a final HTML tree. + */ + +var ParseError = require("./ParseError"); +var Style = require("./Style"); + +var buildCommon = require("./buildCommon"); +var delimiter = require("./delimiter"); +var domTree = require("./domTree"); +var fontMetrics = require("./fontMetrics"); +var utils = require("./utils"); + +var makeSpan = buildCommon.makeSpan; + +/** + * Take a list of nodes, build them in order, and return a list of the built + * nodes. This function handles the `prev` node correctly, and passes the + * previous element from the list as the prev of the next element. + */ +var buildExpression = function(expression, options, prev) { + var groups = []; + for (var i = 0; i < expression.length; i++) { + var group = expression[i]; + groups.push(buildGroup(group, options, prev)); + prev = group; + } + return groups; +}; + +// List of types used by getTypeOfGroup, +// see https://github.com/Khan/KaTeX/wiki/Examining-TeX#group-types +var groupToType = { + mathord: "mord", + textord: "mord", + bin: "mbin", + rel: "mrel", + text: "mord", + open: "mopen", + close: "mclose", + inner: "minner", + genfrac: "mord", + array: "mord", + spacing: "mord", + punct: "mpunct", + ordgroup: "mord", + op: "mop", + katex: "mord", + overline: "mord", + underline: "mord", + rule: "mord", + leftright: "minner", + sqrt: "mord", + accent: "mord", +}; + +/** + * Gets the final math type of an expression, given its group type. This type is + * used to determine spacing between elements, and affects bin elements by + * causing them to change depending on what types are around them. This type + * must be attached to the outermost node of an element as a CSS class so that + * spacing with its surrounding elements works correctly. + * + * Some elements can be mapped one-to-one from group type to math type, and + * those are listed in the `groupToType` table. + * + * Others (usually elements that wrap around other elements) often have + * recursive definitions, and thus call `getTypeOfGroup` on their inner + * elements. + */ +var getTypeOfGroup = function(group) { + if (group == null) { + // Like when typesetting $^3$ + return groupToType.mathord; + } else if (group.type === "supsub") { + return getTypeOfGroup(group.value.base); + } else if (group.type === "llap" || group.type === "rlap") { + return getTypeOfGroup(group.value); + } else if (group.type === "color") { + return getTypeOfGroup(group.value.value); + } else if (group.type === "sizing") { + return getTypeOfGroup(group.value.value); + } else if (group.type === "styling") { + return getTypeOfGroup(group.value.value); + } else if (group.type === "delimsizing") { + return groupToType[group.value.delimType]; + } else { + return groupToType[group.type]; + } +}; + +/** + * Sometimes, groups perform special rules when they have superscripts or + * subscripts attached to them. This function lets the `supsub` group know that + * its inner element should handle the superscripts and subscripts instead of + * handling them itself. + */ +var shouldHandleSupSub = function(group, options) { + if (!group) { + return false; + } else if (group.type === "op") { + // Operators handle supsubs differently when they have limits + // (e.g. `\displaystyle\sum_2^3`) + return group.value.limits && + (options.style.size === Style.DISPLAY.size || + group.value.alwaysHandleSupSub); + } else if (group.type === "accent") { + return isCharacterBox(group.value.base); + } else { + return null; + } +}; + +/** + * Sometimes we want to pull out the innermost element of a group. In most + * cases, this will just be the group itself, but when ordgroups and colors have + * a single element, we want to pull that out. + */ +var getBaseElem = function(group) { + if (!group) { + return false; + } else if (group.type === "ordgroup") { + if (group.value.length === 1) { + return getBaseElem(group.value[0]); + } else { + return group; + } + } else if (group.type === "color") { + if (group.value.value.length === 1) { + return getBaseElem(group.value.value[0]); + } else { + return group; + } + } else if (group.type === "font") { + return getBaseElem(group.value.body); + } else { + return group; + } +}; + +/** + * TeXbook algorithms often reference "character boxes", which are simply groups + * with a single character in them. To decide if something is a character box, + * we find its innermost group, and see if it is a single character. + */ +var isCharacterBox = function(group) { + var baseElem = getBaseElem(group); + + // These are all they types of groups which hold single characters + return baseElem.type === "mathord" || + baseElem.type === "textord" || + baseElem.type === "bin" || + baseElem.type === "rel" || + baseElem.type === "inner" || + baseElem.type === "open" || + baseElem.type === "close" || + baseElem.type === "punct"; +}; + +var makeNullDelimiter = function(options) { + return makeSpan([ + "sizing", "reset-" + options.size, "size5", + options.style.reset(), Style.TEXT.cls(), + "nulldelimiter", + ]); +}; + +/** + * This is a map of group types to the function used to handle that type. + * Simpler types come at the beginning, while complicated types come afterwards. + */ +var groupTypes = {}; + +groupTypes.mathord = function(group, options, prev) { + return buildCommon.makeOrd(group, options, "mathord"); +}; + +groupTypes.textord = function(group, options, prev) { + return buildCommon.makeOrd(group, options, "textord"); +}; + +groupTypes.bin = function(group, options, prev) { + var className = "mbin"; + // Pull out the most recent element. Do some special handling to find + // things at the end of a \color group. Note that we don't use the same + // logic for ordgroups (which count as ords). + var prevAtom = prev; + while (prevAtom && prevAtom.type === "color") { + var atoms = prevAtom.value.value; + prevAtom = atoms[atoms.length - 1]; + } + // See TeXbook pg. 442-446, Rules 5 and 6, and the text before Rule 19. + // Here, we determine whether the bin should turn into an ord. We + // currently only apply Rule 5. + if (!prev || utils.contains(["mbin", "mopen", "mrel", "mop", "mpunct"], + getTypeOfGroup(prevAtom))) { + group.type = "textord"; + className = "mord"; + } + + return buildCommon.mathsym( + group.value, group.mode, options.getColor(), [className]); +}; + +groupTypes.rel = function(group, options, prev) { + return buildCommon.mathsym( + group.value, group.mode, options.getColor(), ["mrel"]); +}; + +groupTypes.open = function(group, options, prev) { + return buildCommon.mathsym( + group.value, group.mode, options.getColor(), ["mopen"]); +}; + +groupTypes.close = function(group, options, prev) { + return buildCommon.mathsym( + group.value, group.mode, options.getColor(), ["mclose"]); +}; + +groupTypes.inner = function(group, options, prev) { + return buildCommon.mathsym( + group.value, group.mode, options.getColor(), ["minner"]); +}; + +groupTypes.punct = function(group, options, prev) { + return buildCommon.mathsym( + group.value, group.mode, options.getColor(), ["mpunct"]); +}; + +groupTypes.ordgroup = function(group, options, prev) { + return makeSpan( + ["mord", options.style.cls()], + buildExpression(group.value, options.reset()) + ); +}; + +groupTypes.text = function(group, options, prev) { + return makeSpan(["text", "mord", options.style.cls()], + buildExpression(group.value.body, options.reset())); +}; + +groupTypes.color = function(group, options, prev) { + var elements = buildExpression( + group.value.value, + options.withColor(group.value.color), + prev + ); + + // \color isn't supposed to affect the type of the elements it contains. + // To accomplish this, we wrap the results in a fragment, so the inner + // elements will be able to directly interact with their neighbors. For + // example, `\color{red}{2 +} 3` has the same spacing as `2 + 3` + return new buildCommon.makeFragment(elements); +}; + +groupTypes.supsub = function(group, options, prev) { + // Superscript and subscripts are handled in the TeXbook on page + // 445-446, rules 18(a-f). + + // Here is where we defer to the inner group if it should handle + // superscripts and subscripts itself. + if (shouldHandleSupSub(group.value.base, options)) { + return groupTypes[group.value.base.type](group, options, prev); + } + + var base = buildGroup(group.value.base, options.reset()); + var supmid; + var submid; + var sup; + var sub; + + if (group.value.sup) { + sup = buildGroup(group.value.sup, + options.withStyle(options.style.sup())); + supmid = makeSpan( + [options.style.reset(), options.style.sup().cls()], [sup]); + } + + if (group.value.sub) { + sub = buildGroup(group.value.sub, + options.withStyle(options.style.sub())); + submid = makeSpan( + [options.style.reset(), options.style.sub().cls()], [sub]); + } + + // Rule 18a + var supShift; + var subShift; + if (isCharacterBox(group.value.base)) { + supShift = 0; + subShift = 0; + } else { + supShift = base.height - fontMetrics.metrics.supDrop; + subShift = base.depth + fontMetrics.metrics.subDrop; + } + + // Rule 18c + var minSupShift; + if (options.style === Style.DISPLAY) { + minSupShift = fontMetrics.metrics.sup1; + } else if (options.style.cramped) { + minSupShift = fontMetrics.metrics.sup3; + } else { + minSupShift = fontMetrics.metrics.sup2; + } + + // scriptspace is a font-size-independent size, so scale it + // appropriately + var multiplier = Style.TEXT.sizeMultiplier * + options.style.sizeMultiplier; + var scriptspace = + (0.5 / fontMetrics.metrics.ptPerEm) / multiplier + "em"; + + var supsub; + if (!group.value.sup) { + // Rule 18b + subShift = Math.max( + subShift, fontMetrics.metrics.sub1, + sub.height - 0.8 * fontMetrics.metrics.xHeight); + + supsub = buildCommon.makeVList([ + {type: "elem", elem: submid}, + ], "shift", subShift, options); + + supsub.children[0].style.marginRight = scriptspace; + + // Subscripts shouldn't be shifted by the base's italic correction. + // Account for that by shifting the subscript back the appropriate + // amount. Note we only do this when the base is a single symbol. + if (base instanceof domTree.symbolNode) { + supsub.children[0].style.marginLeft = -base.italic + "em"; + } + } else if (!group.value.sub) { + // Rule 18c, d + supShift = Math.max(supShift, minSupShift, + sup.depth + 0.25 * fontMetrics.metrics.xHeight); + + supsub = buildCommon.makeVList([ + {type: "elem", elem: supmid}, + ], "shift", -supShift, options); + + supsub.children[0].style.marginRight = scriptspace; + } else { + supShift = Math.max( + supShift, minSupShift, + sup.depth + 0.25 * fontMetrics.metrics.xHeight); + subShift = Math.max(subShift, fontMetrics.metrics.sub2); + + var ruleWidth = fontMetrics.metrics.defaultRuleThickness; + + // Rule 18e + if ((supShift - sup.depth) - (sub.height - subShift) < + 4 * ruleWidth) { + subShift = 4 * ruleWidth - (supShift - sup.depth) + sub.height; + var psi = 0.8 * fontMetrics.metrics.xHeight - + (supShift - sup.depth); + if (psi > 0) { + supShift += psi; + subShift -= psi; + } + } + + supsub = buildCommon.makeVList([ + {type: "elem", elem: submid, shift: subShift}, + {type: "elem", elem: supmid, shift: -supShift}, + ], "individualShift", null, options); + + // See comment above about subscripts not being shifted + if (base instanceof domTree.symbolNode) { + supsub.children[0].style.marginLeft = -base.italic + "em"; + } + + supsub.children[0].style.marginRight = scriptspace; + supsub.children[1].style.marginRight = scriptspace; + } + + // We ensure to wrap the supsub vlist in a span.msupsub to reset text-align + return makeSpan([getTypeOfGroup(group.value.base)], + [base, makeSpan(["msupsub"], [supsub])]); +}; + +groupTypes.genfrac = function(group, options, prev) { + // Fractions are handled in the TeXbook on pages 444-445, rules 15(a-e). + // Figure out what style this fraction should be in based on the + // function used + var fstyle = options.style; + if (group.value.size === "display") { + fstyle = Style.DISPLAY; + } else if (group.value.size === "text") { + fstyle = Style.TEXT; + } + + var nstyle = fstyle.fracNum(); + var dstyle = fstyle.fracDen(); + + var numer = buildGroup(group.value.numer, options.withStyle(nstyle)); + var numerreset = makeSpan([fstyle.reset(), nstyle.cls()], [numer]); + + var denom = buildGroup(group.value.denom, options.withStyle(dstyle)); + var denomreset = makeSpan([fstyle.reset(), dstyle.cls()], [denom]); + + var ruleWidth; + if (group.value.hasBarLine) { + ruleWidth = fontMetrics.metrics.defaultRuleThickness / + options.style.sizeMultiplier; + } else { + ruleWidth = 0; + } + + // Rule 15b + var numShift; + var clearance; + var denomShift; + if (fstyle.size === Style.DISPLAY.size) { + numShift = fontMetrics.metrics.num1; + if (ruleWidth > 0) { + clearance = 3 * ruleWidth; + } else { + clearance = 7 * fontMetrics.metrics.defaultRuleThickness; + } + denomShift = fontMetrics.metrics.denom1; + } else { + if (ruleWidth > 0) { + numShift = fontMetrics.metrics.num2; + clearance = ruleWidth; + } else { + numShift = fontMetrics.metrics.num3; + clearance = 3 * fontMetrics.metrics.defaultRuleThickness; + } + denomShift = fontMetrics.metrics.denom2; + } + + var frac; + if (ruleWidth === 0) { + // Rule 15c + var candiateClearance = + (numShift - numer.depth) - (denom.height - denomShift); + if (candiateClearance < clearance) { + numShift += 0.5 * (clearance - candiateClearance); + denomShift += 0.5 * (clearance - candiateClearance); + } + + frac = buildCommon.makeVList([ + {type: "elem", elem: denomreset, shift: denomShift}, + {type: "elem", elem: numerreset, shift: -numShift}, + ], "individualShift", null, options); + } else { + // Rule 15d + var axisHeight = fontMetrics.metrics.axisHeight; + + if ((numShift - numer.depth) - (axisHeight + 0.5 * ruleWidth) < + clearance) { + numShift += + clearance - ((numShift - numer.depth) - + (axisHeight + 0.5 * ruleWidth)); + } + + if ((axisHeight - 0.5 * ruleWidth) - (denom.height - denomShift) < + clearance) { + denomShift += + clearance - ((axisHeight - 0.5 * ruleWidth) - + (denom.height - denomShift)); + } + + var mid = makeSpan( + [options.style.reset(), Style.TEXT.cls(), "frac-line"]); + // Manually set the height of the line because its height is + // created in CSS + mid.height = ruleWidth; + + var midShift = -(axisHeight - 0.5 * ruleWidth); + + frac = buildCommon.makeVList([ + {type: "elem", elem: denomreset, shift: denomShift}, + {type: "elem", elem: mid, shift: midShift}, + {type: "elem", elem: numerreset, shift: -numShift}, + ], "individualShift", null, options); + } + + // Since we manually change the style sometimes (with \dfrac or \tfrac), + // account for the possible size change here. + frac.height *= fstyle.sizeMultiplier / options.style.sizeMultiplier; + frac.depth *= fstyle.sizeMultiplier / options.style.sizeMultiplier; + + // Rule 15e + var delimSize; + if (fstyle.size === Style.DISPLAY.size) { + delimSize = fontMetrics.metrics.delim1; + } else { + delimSize = fontMetrics.metrics.getDelim2(fstyle); + } + + var leftDelim; + var rightDelim; + if (group.value.leftDelim == null) { + leftDelim = makeNullDelimiter(options); + } else { + leftDelim = delimiter.customSizedDelim( + group.value.leftDelim, delimSize, true, + options.withStyle(fstyle), group.mode); + } + if (group.value.rightDelim == null) { + rightDelim = makeNullDelimiter(options); + } else { + rightDelim = delimiter.customSizedDelim( + group.value.rightDelim, delimSize, true, + options.withStyle(fstyle), group.mode); + } + + return makeSpan( + ["mord", options.style.reset(), fstyle.cls()], + [leftDelim, makeSpan(["mfrac"], [frac]), rightDelim], + options.getColor()); +}; + +groupTypes.array = function(group, options, prev) { + var r; + var c; + var nr = group.value.body.length; + var nc = 0; + var body = new Array(nr); + + // Horizontal spacing + var pt = 1 / fontMetrics.metrics.ptPerEm; + var arraycolsep = 5 * pt; // \arraycolsep in article.cls + + // Vertical spacing + var baselineskip = 12 * pt; // see size10.clo + // Default \arraystretch from lttab.dtx + // TODO(gagern): may get redefined once we have user-defined macros + var arraystretch = utils.deflt(group.value.arraystretch, 1); + var arrayskip = arraystretch * baselineskip; + var arstrutHeight = 0.7 * arrayskip; // \strutbox in ltfsstrc.dtx and + var arstrutDepth = 0.3 * arrayskip; // \@arstrutbox in lttab.dtx + + var totalHeight = 0; + for (r = 0; r < group.value.body.length; ++r) { + var inrow = group.value.body[r]; + var height = arstrutHeight; // \@array adds an \@arstrut + var depth = arstrutDepth; // to each tow (via the template) + + if (nc < inrow.length) { + nc = inrow.length; + } + + var outrow = new Array(inrow.length); + for (c = 0; c < inrow.length; ++c) { + var elt = buildGroup(inrow[c], options); + if (depth < elt.depth) { + depth = elt.depth; + } + if (height < elt.height) { + height = elt.height; + } + outrow[c] = elt; + } + + var gap = 0; + if (group.value.rowGaps[r]) { + gap = group.value.rowGaps[r].value; + switch (gap.unit) { + case "em": + gap = gap.number; + break; + case "ex": + gap = gap.number * fontMetrics.metrics.emPerEx; + break; + default: + console.error("Can't handle unit " + gap.unit); + gap = 0; + } + if (gap > 0) { // \@argarraycr + gap += arstrutDepth; + if (depth < gap) { + depth = gap; // \@xargarraycr + } + gap = 0; + } + } + + outrow.height = height; + outrow.depth = depth; + totalHeight += height; + outrow.pos = totalHeight; + totalHeight += depth + gap; // \@yargarraycr + body[r] = outrow; + } + + var offset = totalHeight / 2 + fontMetrics.metrics.axisHeight; + var colDescriptions = group.value.cols || []; + var cols = []; + var colSep; + var colDescrNum; + for (c = 0, colDescrNum = 0; + // Continue while either there are more columns or more column + // descriptions, so trailing separators don't get lost. + c < nc || colDescrNum < colDescriptions.length; + ++c, ++colDescrNum) { + + var colDescr = colDescriptions[colDescrNum] || {}; + + var firstSeparator = true; + while (colDescr.type === "separator") { + // If there is more than one separator in a row, add a space + // between them. + if (!firstSeparator) { + colSep = makeSpan(["arraycolsep"], []); + colSep.style.width = + fontMetrics.metrics.doubleRuleSep + "em"; + cols.push(colSep); + } + + if (colDescr.separator === "|") { + var separator = makeSpan( + ["vertical-separator"], + []); + separator.style.height = totalHeight + "em"; + separator.style.verticalAlign = + -(totalHeight - offset) + "em"; + + cols.push(separator); + } else { + throw new ParseError( + "Invalid separator type: " + colDescr.separator); + } + + colDescrNum++; + colDescr = colDescriptions[colDescrNum] || {}; + firstSeparator = false; + } + + if (c >= nc) { + continue; + } + + var sepwidth; + if (c > 0 || group.value.hskipBeforeAndAfter) { + sepwidth = utils.deflt(colDescr.pregap, arraycolsep); + if (sepwidth !== 0) { + colSep = makeSpan(["arraycolsep"], []); + colSep.style.width = sepwidth + "em"; + cols.push(colSep); + } + } + + var col = []; + for (r = 0; r < nr; ++r) { + var row = body[r]; + var elem = row[c]; + if (!elem) { + continue; + } + var shift = row.pos - offset; + elem.depth = row.depth; + elem.height = row.height; + col.push({type: "elem", elem: elem, shift: shift}); + } + + col = buildCommon.makeVList(col, "individualShift", null, options); + col = makeSpan( + ["col-align-" + (colDescr.align || "c")], + [col]); + cols.push(col); + + if (c < nc - 1 || group.value.hskipBeforeAndAfter) { + sepwidth = utils.deflt(colDescr.postgap, arraycolsep); + if (sepwidth !== 0) { + colSep = makeSpan(["arraycolsep"], []); + colSep.style.width = sepwidth + "em"; + cols.push(colSep); + } + } + } + body = makeSpan(["mtable"], cols); + return makeSpan(["mord"], [body], options.getColor()); +}; + +groupTypes.spacing = function(group, options, prev) { + if (group.value === "\\ " || group.value === "\\space" || + group.value === " " || group.value === "~") { + // Spaces are generated by adding an actual space. Each of these + // things has an entry in the symbols table, so these will be turned + // into appropriate outputs. + return makeSpan( + ["mord", "mspace"], + [buildCommon.mathsym(group.value, group.mode)] + ); + } else { + // Other kinds of spaces are of arbitrary width. We use CSS to + // generate these. + return makeSpan( + ["mord", "mspace", + buildCommon.spacingFunctions[group.value].className]); + } +}; + +groupTypes.llap = function(group, options, prev) { + var inner = makeSpan( + ["inner"], [buildGroup(group.value.body, options.reset())]); + var fix = makeSpan(["fix"], []); + return makeSpan( + ["llap", options.style.cls()], [inner, fix]); +}; + +groupTypes.rlap = function(group, options, prev) { + var inner = makeSpan( + ["inner"], [buildGroup(group.value.body, options.reset())]); + var fix = makeSpan(["fix"], []); + return makeSpan( + ["rlap", options.style.cls()], [inner, fix]); +}; + +groupTypes.op = function(group, options, prev) { + // Operators are handled in the TeXbook pg. 443-444, rule 13(a). + var supGroup; + var subGroup; + var hasLimits = false; + if (group.type === "supsub" ) { + // If we have limits, supsub will pass us its group to handle. Pull + // out the superscript and subscript and set the group to the op in + // its base. + supGroup = group.value.sup; + subGroup = group.value.sub; + group = group.value.base; + hasLimits = true; + } + + // Most operators have a large successor symbol, but these don't. + var noSuccessor = [ + "\\smallint", + ]; + + var large = false; + if (options.style.size === Style.DISPLAY.size && + group.value.symbol && + !utils.contains(noSuccessor, group.value.body)) { + + // Most symbol operators get larger in displaystyle (rule 13) + large = true; + } + + var base; + var baseShift = 0; + var slant = 0; + if (group.value.symbol) { + // If this is a symbol, create the symbol. + var style = large ? "Size2-Regular" : "Size1-Regular"; + base = buildCommon.makeSymbol( + group.value.body, style, "math", options.getColor(), + ["op-symbol", large ? "large-op" : "small-op", "mop"]); + + // Shift the symbol so its center lies on the axis (rule 13). It + // appears that our fonts have the centers of the symbols already + // almost on the axis, so these numbers are very small. Note we + // don't actually apply this here, but instead it is used either in + // the vlist creation or separately when there are no limits. + baseShift = (base.height - base.depth) / 2 - + fontMetrics.metrics.axisHeight * + options.style.sizeMultiplier; + + // The slant of the symbol is just its italic correction. + slant = base.italic; + } else { + // Otherwise, this is a text operator. Build the text from the + // operator's name. + // TODO(emily): Add a space in the middle of some of these + // operators, like \limsup + var output = []; + for (var i = 1; i < group.value.body.length; i++) { + output.push(buildCommon.mathsym(group.value.body[i], group.mode)); + } + base = makeSpan(["mop"], output, options.getColor()); + } + + if (hasLimits) { + // IE 8 clips \int if it is in a display: inline-block. We wrap it + // in a new span so it is an inline, and works. + base = makeSpan([], [base]); + + var supmid; + var supKern; + var submid; + var subKern; + // We manually have to handle the superscripts and subscripts. This, + // aside from the kern calculations, is copied from supsub. + if (supGroup) { + var sup = buildGroup( + supGroup, options.withStyle(options.style.sup())); + supmid = makeSpan( + [options.style.reset(), options.style.sup().cls()], [sup]); + + supKern = Math.max( + fontMetrics.metrics.bigOpSpacing1, + fontMetrics.metrics.bigOpSpacing3 - sup.depth); + } + + if (subGroup) { + var sub = buildGroup( + subGroup, options.withStyle(options.style.sub())); + submid = makeSpan( + [options.style.reset(), options.style.sub().cls()], + [sub]); + + subKern = Math.max( + fontMetrics.metrics.bigOpSpacing2, + fontMetrics.metrics.bigOpSpacing4 - sub.height); + } + + // Build the final group as a vlist of the possible subscript, base, + // and possible superscript. + var finalGroup; + var top; + var bottom; + if (!supGroup) { + top = base.height - baseShift; + + finalGroup = buildCommon.makeVList([ + {type: "kern", size: fontMetrics.metrics.bigOpSpacing5}, + {type: "elem", elem: submid}, + {type: "kern", size: subKern}, + {type: "elem", elem: base}, + ], "top", top, options); + + // Here, we shift the limits by the slant of the symbol. Note + // that we are supposed to shift the limits by 1/2 of the slant, + // but since we are centering the limits adding a full slant of + // margin will shift by 1/2 that. + finalGroup.children[0].style.marginLeft = -slant + "em"; + } else if (!subGroup) { + bottom = base.depth + baseShift; + + finalGroup = buildCommon.makeVList([ + {type: "elem", elem: base}, + {type: "kern", size: supKern}, + {type: "elem", elem: supmid}, + {type: "kern", size: fontMetrics.metrics.bigOpSpacing5}, + ], "bottom", bottom, options); + + // See comment above about slants + finalGroup.children[1].style.marginLeft = slant + "em"; + } else if (!supGroup && !subGroup) { + // This case probably shouldn't occur (this would mean the + // supsub was sending us a group with no superscript or + // subscript) but be safe. + return base; + } else { + bottom = fontMetrics.metrics.bigOpSpacing5 + + submid.height + submid.depth + + subKern + + base.depth + baseShift; + + finalGroup = buildCommon.makeVList([ + {type: "kern", size: fontMetrics.metrics.bigOpSpacing5}, + {type: "elem", elem: submid}, + {type: "kern", size: subKern}, + {type: "elem", elem: base}, + {type: "kern", size: supKern}, + {type: "elem", elem: supmid}, + {type: "kern", size: fontMetrics.metrics.bigOpSpacing5}, + ], "bottom", bottom, options); + + // See comment above about slants + finalGroup.children[0].style.marginLeft = -slant + "em"; + finalGroup.children[2].style.marginLeft = slant + "em"; + } + + return makeSpan(["mop", "op-limits"], [finalGroup]); + } else { + if (group.value.symbol) { + base.style.top = baseShift + "em"; + } + + return base; + } +}; + +groupTypes.katex = function(group, options, prev) { + // The KaTeX logo. The offsets for the K and a were chosen to look + // good, but the offsets for the T, E, and X were taken from the + // definition of \TeX in TeX (see TeXbook pg. 356) + var k = makeSpan( + ["k"], [buildCommon.mathsym("K", group.mode)]); + var a = makeSpan( + ["a"], [buildCommon.mathsym("A", group.mode)]); + + a.height = (a.height + 0.2) * 0.75; + a.depth = (a.height - 0.2) * 0.75; + + var t = makeSpan( + ["t"], [buildCommon.mathsym("T", group.mode)]); + var e = makeSpan( + ["e"], [buildCommon.mathsym("E", group.mode)]); + + e.height = (e.height - 0.2155); + e.depth = (e.depth + 0.2155); + + var x = makeSpan( + ["x"], [buildCommon.mathsym("X", group.mode)]); + + return makeSpan( + ["katex-logo", "mord"], [k, a, t, e, x], options.getColor()); +}; + +groupTypes.overline = function(group, options, prev) { + // Overlines are handled in the TeXbook pg 443, Rule 9. + + // Build the inner group in the cramped style. + var innerGroup = buildGroup(group.value.body, + options.withStyle(options.style.cramp())); + + var ruleWidth = fontMetrics.metrics.defaultRuleThickness / + options.style.sizeMultiplier; + + // Create the line above the body + var line = makeSpan( + [options.style.reset(), Style.TEXT.cls(), "overline-line"]); + line.height = ruleWidth; + line.maxFontSize = 1.0; + + // Generate the vlist, with the appropriate kerns + var vlist = buildCommon.makeVList([ + {type: "elem", elem: innerGroup}, + {type: "kern", size: 3 * ruleWidth}, + {type: "elem", elem: line}, + {type: "kern", size: ruleWidth}, + ], "firstBaseline", null, options); + + return makeSpan(["overline", "mord"], [vlist], options.getColor()); +}; + +groupTypes.underline = function(group, options, prev) { + // Underlines are handled in the TeXbook pg 443, Rule 10. + + // Build the inner group. + var innerGroup = buildGroup(group.value.body, options); + + var ruleWidth = fontMetrics.metrics.defaultRuleThickness / + options.style.sizeMultiplier; + + // Create the line above the body + var line = makeSpan( + [options.style.reset(), Style.TEXT.cls(), "underline-line"]); + line.height = ruleWidth; + line.maxFontSize = 1.0; + + // Generate the vlist, with the appropriate kerns + var vlist = buildCommon.makeVList([ + {type: "kern", size: ruleWidth}, + {type: "elem", elem: line}, + {type: "kern", size: 3 * ruleWidth}, + {type: "elem", elem: innerGroup}, + ], "top", innerGroup.height, options); + + return makeSpan(["underline", "mord"], [vlist], options.getColor()); +}; + +groupTypes.sqrt = function(group, options, prev) { + // Square roots are handled in the TeXbook pg. 443, Rule 11. + + // First, we do the same steps as in overline to build the inner group + // and line + var inner = buildGroup(group.value.body, + options.withStyle(options.style.cramp())); + + var ruleWidth = fontMetrics.metrics.defaultRuleThickness / + options.style.sizeMultiplier; + + var line = makeSpan( + [options.style.reset(), Style.TEXT.cls(), "sqrt-line"], [], + options.getColor()); + line.height = ruleWidth; + line.maxFontSize = 1.0; + + var phi = ruleWidth; + if (options.style.id < Style.TEXT.id) { + phi = fontMetrics.metrics.xHeight; + } + + // Calculate the clearance between the body and line + var lineClearance = ruleWidth + phi / 4; + + var innerHeight = + (inner.height + inner.depth) * options.style.sizeMultiplier; + var minDelimiterHeight = innerHeight + lineClearance + ruleWidth; + + // Create a \surd delimiter of the required minimum size + var delim = makeSpan(["sqrt-sign"], [ + delimiter.customSizedDelim("\\surd", minDelimiterHeight, + false, options, group.mode)], + options.getColor()); + + var delimDepth = (delim.height + delim.depth) - ruleWidth; + + // Adjust the clearance based on the delimiter size + if (delimDepth > inner.height + inner.depth + lineClearance) { + lineClearance = + (lineClearance + delimDepth - inner.height - inner.depth) / 2; + } + + // Shift the delimiter so that its top lines up with the top of the line + var delimShift = -(inner.height + lineClearance + ruleWidth) + delim.height; + delim.style.top = delimShift + "em"; + delim.height -= delimShift; + delim.depth += delimShift; + + // We add a special case here, because even when `inner` is empty, we + // still get a line. So, we use a simple heuristic to decide if we + // should omit the body entirely. (note this doesn't work for something + // like `\sqrt{\rlap{x}}`, but if someone is doing that they deserve for + // it not to work. + var body; + if (inner.height === 0 && inner.depth === 0) { + body = makeSpan(); + } else { + body = buildCommon.makeVList([ + {type: "elem", elem: inner}, + {type: "kern", size: lineClearance}, + {type: "elem", elem: line}, + {type: "kern", size: ruleWidth}, + ], "firstBaseline", null, options); + } + + if (!group.value.index) { + return makeSpan(["sqrt", "mord"], [delim, body]); + } else { + // Handle the optional root index + + // The index is always in scriptscript style + var root = buildGroup( + group.value.index, + options.withStyle(Style.SCRIPTSCRIPT)); + var rootWrap = makeSpan( + [options.style.reset(), Style.SCRIPTSCRIPT.cls()], + [root]); + + // Figure out the height and depth of the inner part + var innerRootHeight = Math.max(delim.height, body.height); + var innerRootDepth = Math.max(delim.depth, body.depth); + + // The amount the index is shifted by. This is taken from the TeX + // source, in the definition of `\r@@t`. + var toShift = 0.6 * (innerRootHeight - innerRootDepth); + + // Build a VList with the superscript shifted up correctly + var rootVList = buildCommon.makeVList( + [{type: "elem", elem: rootWrap}], + "shift", -toShift, options); + // Add a class surrounding it so we can add on the appropriate + // kerning + var rootVListWrap = makeSpan(["root"], [rootVList]); + + return makeSpan(["sqrt", "mord"], [rootVListWrap, delim, body]); + } +}; + +groupTypes.sizing = function(group, options, prev) { + // Handle sizing operators like \Huge. Real TeX doesn't actually allow + // these functions inside of math expressions, so we do some special + // handling. + var inner = buildExpression(group.value.value, + options.withSize(group.value.size), prev); + + var span = makeSpan(["mord"], + [makeSpan(["sizing", "reset-" + options.size, group.value.size, + options.style.cls()], + inner)]); + + // Calculate the correct maxFontSize manually + var fontSize = buildCommon.sizingMultiplier[group.value.size]; + span.maxFontSize = fontSize * options.style.sizeMultiplier; + + return span; +}; + +groupTypes.styling = function(group, options, prev) { + // Style changes are handled in the TeXbook on pg. 442, Rule 3. + + // Figure out what style we're changing to. + var style = { + "display": Style.DISPLAY, + "text": Style.TEXT, + "script": Style.SCRIPT, + "scriptscript": Style.SCRIPTSCRIPT, + }; + + var newStyle = style[group.value.style]; + + // Build the inner expression in the new style. + var inner = buildExpression( + group.value.value, options.withStyle(newStyle), prev); + + return makeSpan([options.style.reset(), newStyle.cls()], inner); +}; + +groupTypes.font = function(group, options, prev) { + var font = group.value.font; + return buildGroup(group.value.body, options.withFont(font), prev); +}; + +groupTypes.delimsizing = function(group, options, prev) { + var delim = group.value.value; + + if (delim === ".") { + // Empty delimiters still count as elements, even though they don't + // show anything. + return makeSpan([groupToType[group.value.delimType]]); + } + + // Use delimiter.sizedDelim to generate the delimiter. + return makeSpan( + [groupToType[group.value.delimType]], + [delimiter.sizedDelim( + delim, group.value.size, options, group.mode)]); +}; + +groupTypes.leftright = function(group, options, prev) { + // Build the inner expression + var inner = buildExpression(group.value.body, options.reset()); + + var innerHeight = 0; + var innerDepth = 0; + + // Calculate its height and depth + for (var i = 0; i < inner.length; i++) { + innerHeight = Math.max(inner[i].height, innerHeight); + innerDepth = Math.max(inner[i].depth, innerDepth); + } + + // The size of delimiters is the same, regardless of what style we are + // in. Thus, to correctly calculate the size of delimiter we need around + // a group, we scale down the inner size based on the size. + innerHeight *= options.style.sizeMultiplier; + innerDepth *= options.style.sizeMultiplier; + + var leftDelim; + if (group.value.left === ".") { + // Empty delimiters in \left and \right make null delimiter spaces. + leftDelim = makeNullDelimiter(options); + } else { + // Otherwise, use leftRightDelim to generate the correct sized + // delimiter. + leftDelim = delimiter.leftRightDelim( + group.value.left, innerHeight, innerDepth, options, + group.mode); + } + // Add it to the beginning of the expression + inner.unshift(leftDelim); + + var rightDelim; + // Same for the right delimiter + if (group.value.right === ".") { + rightDelim = makeNullDelimiter(options); + } else { + rightDelim = delimiter.leftRightDelim( + group.value.right, innerHeight, innerDepth, options, + group.mode); + } + // Add it to the end of the expression. + inner.push(rightDelim); + + return makeSpan( + ["minner", options.style.cls()], inner, options.getColor()); +}; + +groupTypes.rule = function(group, options, prev) { + // Make an empty span for the rule + var rule = makeSpan(["mord", "rule"], [], options.getColor()); + + // Calculate the shift, width, and height of the rule, and account for units + var shift = 0; + if (group.value.shift) { + shift = group.value.shift.number; + if (group.value.shift.unit === "ex") { + shift *= fontMetrics.metrics.xHeight; + } + } + + var width = group.value.width.number; + if (group.value.width.unit === "ex") { + width *= fontMetrics.metrics.xHeight; + } + + var height = group.value.height.number; + if (group.value.height.unit === "ex") { + height *= fontMetrics.metrics.xHeight; + } + + // The sizes of rules are absolute, so make it larger if we are in a + // smaller style. + shift /= options.style.sizeMultiplier; + width /= options.style.sizeMultiplier; + height /= options.style.sizeMultiplier; + + // Style the rule to the right size + rule.style.borderRightWidth = width + "em"; + rule.style.borderTopWidth = height + "em"; + rule.style.bottom = shift + "em"; + + // Record the height and width + rule.width = width; + rule.height = height + shift; + rule.depth = -shift; + + return rule; +}; + +groupTypes.kern = function(group, options, prev) { + // Make an empty span for the rule + var rule = makeSpan(["mord", "rule"], [], options.getColor()); + + var dimension = 0; + if (group.value.dimension) { + dimension = group.value.dimension.number; + if (group.value.dimension.unit === "ex") { + dimension *= fontMetrics.metrics.xHeight; + } + } + + dimension /= options.style.sizeMultiplier; + + rule.style.marginLeft = dimension + "em"; + + return rule; +}; + +groupTypes.accent = function(group, options, prev) { + // Accents are handled in the TeXbook pg. 443, rule 12. + var base = group.value.base; + + var supsubGroup; + if (group.type === "supsub") { + // If our base is a character box, and we have superscripts and + // subscripts, the supsub will defer to us. In particular, we want + // to attach the superscripts and subscripts to the inner body (so + // that the position of the superscripts and subscripts won't be + // affected by the height of the accent). We accomplish this by + // sticking the base of the accent into the base of the supsub, and + // rendering that, while keeping track of where the accent is. + + // The supsub group is the group that was passed in + var supsub = group; + // The real accent group is the base of the supsub group + group = supsub.value.base; + // The character box is the base of the accent group + base = group.value.base; + // Stick the character box into the base of the supsub group + supsub.value.base = base; + + // Rerender the supsub group with its new base, and store that + // result. + supsubGroup = buildGroup( + supsub, options.reset(), prev); + } + + // Build the base group + var body = buildGroup( + base, options.withStyle(options.style.cramp())); + + // Calculate the skew of the accent. This is based on the line "If the + // nucleus is not a single character, let s = 0; otherwise set s to the + // kern amount for the nucleus followed by the \skewchar of its font." + // Note that our skew metrics are just the kern between each character + // and the skewchar. + var skew; + if (isCharacterBox(base)) { + // If the base is a character box, then we want the skew of the + // innermost character. To do that, we find the innermost character: + var baseChar = getBaseElem(base); + // Then, we render its group to get the symbol inside it + var baseGroup = buildGroup( + baseChar, options.withStyle(options.style.cramp())); + // Finally, we pull the skew off of the symbol. + skew = baseGroup.skew; + // Note that we now throw away baseGroup, because the layers we + // removed with getBaseElem might contain things like \color which + // we can't get rid of. + // TODO(emily): Find a better way to get the skew + } else { + skew = 0; + } + + // calculate the amount of space between the body and the accent + var clearance = Math.min(body.height, fontMetrics.metrics.xHeight); + + // Build the accent + var accent = buildCommon.makeSymbol( + group.value.accent, "Main-Regular", "math", options.getColor()); + // Remove the italic correction of the accent, because it only serves to + // shift the accent over to a place we don't want. + accent.italic = 0; + + // The \vec character that the fonts use is a combining character, and + // thus shows up much too far to the left. To account for this, we add a + // specific class which shifts the accent over to where we want it. + // TODO(emily): Fix this in a better way, like by changing the font + var vecClass = group.value.accent === "\\vec" ? "accent-vec" : null; + + var accentBody = makeSpan(["accent-body", vecClass], [ + makeSpan([], [accent])]); + + accentBody = buildCommon.makeVList([ + {type: "elem", elem: body}, + {type: "kern", size: -clearance}, + {type: "elem", elem: accentBody}, + ], "firstBaseline", null, options); + + // Shift the accent over by the skew. Note we shift by twice the skew + // because we are centering the accent, so by adding 2*skew to the left, + // we shift it to the right by 1*skew. + accentBody.children[1].style.marginLeft = 2 * skew + "em"; + + var accentWrap = makeSpan(["mord", "accent"], [accentBody]); + + if (supsubGroup) { + // Here, we replace the "base" child of the supsub with our newly + // generated accent. + supsubGroup.children[0] = accentWrap; + + // Since we don't rerun the height calculation after replacing the + // accent, we manually recalculate height. + supsubGroup.height = Math.max(accentWrap.height, supsubGroup.height); + + // Accents should always be ords, even when their innards are not. + supsubGroup.classes[0] = "mord"; + + return supsubGroup; + } else { + return accentWrap; + } +}; + +groupTypes.phantom = function(group, options, prev) { + var elements = buildExpression( + group.value.value, + options.withPhantom(), + prev + ); + + // \phantom isn't supposed to affect the elements it contains. + // See "color" for more details. + return new buildCommon.makeFragment(elements); +}; + +/** + * buildGroup is the function that takes a group and calls the correct groupType + * function for it. It also handles the interaction of size and style changes + * between parents and children. + */ +var buildGroup = function(group, options, prev) { + if (!group) { + return makeSpan(); + } + + if (groupTypes[group.type]) { + // Call the groupTypes function + var groupNode = groupTypes[group.type](group, options, prev); + var multiplier; + + // If the style changed between the parent and the current group, + // account for the size difference + if (options.style !== options.parentStyle) { + multiplier = options.style.sizeMultiplier / + options.parentStyle.sizeMultiplier; + + groupNode.height *= multiplier; + groupNode.depth *= multiplier; + } + + // If the size changed between the parent and the current group, account + // for that size difference. + if (options.size !== options.parentSize) { + multiplier = buildCommon.sizingMultiplier[options.size] / + buildCommon.sizingMultiplier[options.parentSize]; + + groupNode.height *= multiplier; + groupNode.depth *= multiplier; + } + + return groupNode; + } else { + throw new ParseError( + "Got group of unknown type: '" + group.type + "'"); + } +}; + +/** + * Take an entire parse tree, and build it into an appropriate set of HTML + * nodes. + */ +var buildHTML = function(tree, options) { + // buildExpression is destructive, so we need to make a clone + // of the incoming tree so that it isn't accidentally changed + tree = JSON.parse(JSON.stringify(tree)); + + // Build the expression contained in the tree + var expression = buildExpression(tree, options); + var body = makeSpan(["base", options.style.cls()], expression); + + // Add struts, which ensure that the top of the HTML element falls at the + // height of the expression, and the bottom of the HTML element falls at the + // depth of the expression. + var topStrut = makeSpan(["strut"]); + var bottomStrut = makeSpan(["strut", "bottom"]); + + topStrut.style.height = body.height + "em"; + bottomStrut.style.height = (body.height + body.depth) + "em"; + // We'd like to use `vertical-align: top` but in IE 9 this lowers the + // baseline of the box to the bottom of this strut (instead staying in the + // normal place) so we use an absolute value for vertical-align instead + bottomStrut.style.verticalAlign = -body.depth + "em"; + + // Wrap the struts and body together + var htmlNode = makeSpan(["katex-html"], [topStrut, bottomStrut, body]); + + htmlNode.setAttribute("aria-hidden", "true"); + + return htmlNode; +}; + +module.exports = buildHTML; + +},{"./ParseError":6,"./Style":9,"./buildCommon":10,"./delimiter":14,"./domTree":15,"./fontMetrics":17,"./utils":25}],12:[function(require,module,exports){ +/** + * This file converts a parse tree into a cooresponding MathML tree. The main + * entry point is the `buildMathML` function, which takes a parse tree from the + * parser. + */ + +var buildCommon = require("./buildCommon"); +var fontMetrics = require("./fontMetrics"); +var mathMLTree = require("./mathMLTree"); +var ParseError = require("./ParseError"); +var symbols = require("./symbols"); +var utils = require("./utils"); + +var makeSpan = buildCommon.makeSpan; +var fontMap = buildCommon.fontMap; + +/** + * Takes a symbol and converts it into a MathML text node after performing + * optional replacement from symbols.js. + */ +var makeText = function(text, mode) { + if (symbols[mode][text] && symbols[mode][text].replace) { + text = symbols[mode][text].replace; + } + + return new mathMLTree.TextNode(text); +}; + +/** + * Returns the math variant as a string or null if none is required. + */ +var getVariant = function(group, options) { + var font = options.font; + if (!font) { + return null; + } + + var mode = group.mode; + if (font === "mathit") { + return "italic"; + } + + var value = group.value; + if (utils.contains(["\\imath", "\\jmath"], value)) { + return null; + } + + if (symbols[mode][value] && symbols[mode][value].replace) { + value = symbols[mode][value].replace; + } + + var fontName = fontMap[font].fontName; + if (fontMetrics.getCharacterMetrics(value, fontName)) { + return fontMap[options.font].variant; + } + + return null; +}; + +/** + * Functions for handling the different types of groups found in the parse + * tree. Each function should take a parse group and return a MathML node. + */ +var groupTypes = {}; + +groupTypes.mathord = function(group, options) { + var node = new mathMLTree.MathNode( + "mi", + [makeText(group.value, group.mode)]); + + var variant = getVariant(group, options); + if (variant) { + node.setAttribute("mathvariant", variant); + } + return node; +}; + +groupTypes.textord = function(group, options) { + var text = makeText(group.value, group.mode); + + var variant = getVariant(group, options) || "normal"; + + var node; + if (/[0-9]/.test(group.value)) { + // TODO(kevinb) merge adjacent <mn> nodes + // do it as a post processing step + node = new mathMLTree.MathNode("mn", [text]); + if (options.font) { + node.setAttribute("mathvariant", variant); + } + } else { + node = new mathMLTree.MathNode("mi", [text]); + node.setAttribute("mathvariant", variant); + } + + return node; +}; + +groupTypes.bin = function(group) { + var node = new mathMLTree.MathNode( + "mo", [makeText(group.value, group.mode)]); + + return node; +}; + +groupTypes.rel = function(group) { + var node = new mathMLTree.MathNode( + "mo", [makeText(group.value, group.mode)]); + + return node; +}; + +groupTypes.open = function(group) { + var node = new mathMLTree.MathNode( + "mo", [makeText(group.value, group.mode)]); + + return node; +}; + +groupTypes.close = function(group) { + var node = new mathMLTree.MathNode( + "mo", [makeText(group.value, group.mode)]); + + return node; +}; + +groupTypes.inner = function(group) { + var node = new mathMLTree.MathNode( + "mo", [makeText(group.value, group.mode)]); + + return node; +}; + +groupTypes.punct = function(group) { + var node = new mathMLTree.MathNode( + "mo", [makeText(group.value, group.mode)]); + + node.setAttribute("separator", "true"); + + return node; +}; + +groupTypes.ordgroup = function(group, options) { + var inner = buildExpression(group.value, options); + + var node = new mathMLTree.MathNode("mrow", inner); + + return node; +}; + +groupTypes.text = function(group, options) { + var inner = buildExpression(group.value.body, options); + + var node = new mathMLTree.MathNode("mtext", inner); + + return node; +}; + +groupTypes.color = function(group, options) { + var inner = buildExpression(group.value.value, options); + + var node = new mathMLTree.MathNode("mstyle", inner); + + node.setAttribute("mathcolor", group.value.color); + + return node; +}; + +groupTypes.supsub = function(group, options) { + var children = [buildGroup(group.value.base, options)]; + + if (group.value.sub) { + children.push(buildGroup(group.value.sub, options)); + } + + if (group.value.sup) { + children.push(buildGroup(group.value.sup, options)); + } + + var nodeType; + if (!group.value.sub) { + nodeType = "msup"; + } else if (!group.value.sup) { + nodeType = "msub"; + } else { + nodeType = "msubsup"; + } + + var node = new mathMLTree.MathNode(nodeType, children); + + return node; +}; + +groupTypes.genfrac = function(group, options) { + var node = new mathMLTree.MathNode( + "mfrac", + [buildGroup(group.value.numer, options), + buildGroup(group.value.denom, options)]); + + if (!group.value.hasBarLine) { + node.setAttribute("linethickness", "0px"); + } + + if (group.value.leftDelim != null || group.value.rightDelim != null) { + var withDelims = []; + + if (group.value.leftDelim != null) { + var leftOp = new mathMLTree.MathNode( + "mo", [new mathMLTree.TextNode(group.value.leftDelim)]); + + leftOp.setAttribute("fence", "true"); + + withDelims.push(leftOp); + } + + withDelims.push(node); + + if (group.value.rightDelim != null) { + var rightOp = new mathMLTree.MathNode( + "mo", [new mathMLTree.TextNode(group.value.rightDelim)]); + + rightOp.setAttribute("fence", "true"); + + withDelims.push(rightOp); + } + + var outerNode = new mathMLTree.MathNode("mrow", withDelims); + + return outerNode; + } + + return node; +}; + +groupTypes.array = function(group, options) { + return new mathMLTree.MathNode( + "mtable", group.value.body.map(function(row) { + return new mathMLTree.MathNode( + "mtr", row.map(function(cell) { + return new mathMLTree.MathNode( + "mtd", [buildGroup(cell, options)]); + })); + })); +}; + +groupTypes.sqrt = function(group, options) { + var node; + if (group.value.index) { + node = new mathMLTree.MathNode( + "mroot", [ + buildGroup(group.value.body, options), + buildGroup(group.value.index, options), + ]); + } else { + node = new mathMLTree.MathNode( + "msqrt", [buildGroup(group.value.body, options)]); + } + + return node; +}; + +groupTypes.leftright = function(group, options) { + var inner = buildExpression(group.value.body, options); + + if (group.value.left !== ".") { + var leftNode = new mathMLTree.MathNode( + "mo", [makeText(group.value.left, group.mode)]); + + leftNode.setAttribute("fence", "true"); + + inner.unshift(leftNode); + } + + if (group.value.right !== ".") { + var rightNode = new mathMLTree.MathNode( + "mo", [makeText(group.value.right, group.mode)]); + + rightNode.setAttribute("fence", "true"); + + inner.push(rightNode); + } + + var outerNode = new mathMLTree.MathNode("mrow", inner); + + return outerNode; +}; + +groupTypes.accent = function(group, options) { + var accentNode = new mathMLTree.MathNode( + "mo", [makeText(group.value.accent, group.mode)]); + + var node = new mathMLTree.MathNode( + "mover", + [buildGroup(group.value.base, options), + accentNode]); + + node.setAttribute("accent", "true"); + + return node; +}; + +groupTypes.spacing = function(group) { + var node; + + if (group.value === "\\ " || group.value === "\\space" || + group.value === " " || group.value === "~") { + node = new mathMLTree.MathNode( + "mtext", [new mathMLTree.TextNode("\u00a0")]); + } else { + node = new mathMLTree.MathNode("mspace"); + + node.setAttribute( + "width", buildCommon.spacingFunctions[group.value].size); + } + + return node; +}; + +groupTypes.op = function(group) { + var node; + + // TODO(emily): handle big operators using the `largeop` attribute + + if (group.value.symbol) { + // This is a symbol. Just add the symbol. + node = new mathMLTree.MathNode( + "mo", [makeText(group.value.body, group.mode)]); + } else { + // This is a text operator. Add all of the characters from the + // operator's name. + // TODO(emily): Add a space in the middle of some of these + // operators, like \limsup. + node = new mathMLTree.MathNode( + "mi", [new mathMLTree.TextNode(group.value.body.slice(1))]); + } + + return node; +}; + +groupTypes.katex = function(group) { + var node = new mathMLTree.MathNode( + "mtext", [new mathMLTree.TextNode("KaTeX")]); + + return node; +}; + +groupTypes.font = function(group, options) { + var font = group.value.font; + return buildGroup(group.value.body, options.withFont(font)); +}; + +groupTypes.delimsizing = function(group) { + var children = []; + + if (group.value.value !== ".") { + children.push(makeText(group.value.value, group.mode)); + } + + var node = new mathMLTree.MathNode("mo", children); + + if (group.value.delimType === "open" || + group.value.delimType === "close") { + // Only some of the delimsizing functions act as fences, and they + // return "open" or "close" delimTypes. + node.setAttribute("fence", "true"); + } else { + // Explicitly disable fencing if it's not a fence, to override the + // defaults. + node.setAttribute("fence", "false"); + } + + return node; +}; + +groupTypes.styling = function(group, options) { + var inner = buildExpression(group.value.value, options); + + var node = new mathMLTree.MathNode("mstyle", inner); + + var styleAttributes = { + "display": ["0", "true"], + "text": ["0", "false"], + "script": ["1", "false"], + "scriptscript": ["2", "false"], + }; + + var attr = styleAttributes[group.value.style]; + + node.setAttribute("scriptlevel", attr[0]); + node.setAttribute("displaystyle", attr[1]); + + return node; +}; + +groupTypes.sizing = function(group, options) { + var inner = buildExpression(group.value.value, options); + + var node = new mathMLTree.MathNode("mstyle", inner); + + // TODO(emily): This doesn't produce the correct size for nested size + // changes, because we don't keep state of what style we're currently + // in, so we can't reset the size to normal before changing it. Now + // that we're passing an options parameter we should be able to fix + // this. + node.setAttribute( + "mathsize", buildCommon.sizingMultiplier[group.value.size] + "em"); + + return node; +}; + +groupTypes.overline = function(group, options) { + var operator = new mathMLTree.MathNode( + "mo", [new mathMLTree.TextNode("\u203e")]); + operator.setAttribute("stretchy", "true"); + + var node = new mathMLTree.MathNode( + "mover", + [buildGroup(group.value.body, options), + operator]); + node.setAttribute("accent", "true"); + + return node; +}; + +groupTypes.underline = function(group, options) { + var operator = new mathMLTree.MathNode( + "mo", [new mathMLTree.TextNode("\u203e")]); + operator.setAttribute("stretchy", "true"); + + var node = new mathMLTree.MathNode( + "munder", + [buildGroup(group.value.body, options), + operator]); + node.setAttribute("accentunder", "true"); + + return node; +}; + +groupTypes.rule = function(group) { + // TODO(emily): Figure out if there's an actual way to draw black boxes + // in MathML. + var node = new mathMLTree.MathNode("mrow"); + + return node; +}; + +groupTypes.kern = function(group) { + // TODO(kevin): Figure out if there's a way to add space in MathML + var node = new mathMLTree.MathNode("mrow"); + + return node; +}; + +groupTypes.llap = function(group, options) { + var node = new mathMLTree.MathNode( + "mpadded", [buildGroup(group.value.body, options)]); + + node.setAttribute("lspace", "-1width"); + node.setAttribute("width", "0px"); + + return node; +}; + +groupTypes.rlap = function(group, options) { + var node = new mathMLTree.MathNode( + "mpadded", [buildGroup(group.value.body, options)]); + + node.setAttribute("width", "0px"); + + return node; +}; + +groupTypes.phantom = function(group, options, prev) { + var inner = buildExpression(group.value.value, options); + return new mathMLTree.MathNode("mphantom", inner); +}; + +/** + * Takes a list of nodes, builds them, and returns a list of the generated + * MathML nodes. A little simpler than the HTML version because we don't do any + * previous-node handling. + */ +var buildExpression = function(expression, options) { + var groups = []; + for (var i = 0; i < expression.length; i++) { + var group = expression[i]; + groups.push(buildGroup(group, options)); + } + return groups; +}; + +/** + * Takes a group from the parser and calls the appropriate groupTypes function + * on it to produce a MathML node. + */ +var buildGroup = function(group, options) { + if (!group) { + return new mathMLTree.MathNode("mrow"); + } + + if (groupTypes[group.type]) { + // Call the groupTypes function + return groupTypes[group.type](group, options); + } else { + throw new ParseError( + "Got group of unknown type: '" + group.type + "'"); + } +}; + +/** + * Takes a full parse tree and settings and builds a MathML representation of + * it. In particular, we put the elements from building the parse tree into a + * <semantics> tag so we can also include that TeX source as an annotation. + * + * Note that we actually return a domTree element with a `<math>` inside it so + * we can do appropriate styling. + */ +var buildMathML = function(tree, texExpression, options) { + var expression = buildExpression(tree, options); + + // Wrap up the expression in an mrow so it is presented in the semantics + // tag correctly. + var wrapper = new mathMLTree.MathNode("mrow", expression); + + // Build a TeX annotation of the source + var annotation = new mathMLTree.MathNode( + "annotation", [new mathMLTree.TextNode(texExpression)]); + + annotation.setAttribute("encoding", "application/x-tex"); + + var semantics = new mathMLTree.MathNode( + "semantics", [wrapper, annotation]); + + var math = new mathMLTree.MathNode("math", [semantics]); + + // You can't style <math> nodes, so we wrap the node in a span. + return makeSpan(["katex-mathml"], [math]); +}; + +module.exports = buildMathML; + +},{"./ParseError":6,"./buildCommon":10,"./fontMetrics":17,"./mathMLTree":20,"./symbols":23,"./utils":25}],13:[function(require,module,exports){ +var buildHTML = require("./buildHTML"); +var buildMathML = require("./buildMathML"); +var buildCommon = require("./buildCommon"); +var Options = require("./Options"); +var Settings = require("./Settings"); +var Style = require("./Style"); + +var makeSpan = buildCommon.makeSpan; + +var buildTree = function(tree, expression, settings) { + settings = settings || new Settings({}); + + var startStyle = Style.TEXT; + if (settings.displayMode) { + startStyle = Style.DISPLAY; + } + + // Setup the default options + var options = new Options({ + style: startStyle, + size: "size5", + }); + + // `buildHTML` sometimes messes with the parse tree (like turning bins -> + // ords), so we build the MathML version first. + var mathMLNode = buildMathML(tree, expression, options); + var htmlNode = buildHTML(tree, options); + + var katexNode = makeSpan(["katex"], [ + mathMLNode, htmlNode, + ]); + + if (settings.displayMode) { + return makeSpan(["katex-display"], [katexNode]); + } else { + return katexNode; + } +}; + +module.exports = buildTree; + +},{"./Options":5,"./Settings":8,"./Style":9,"./buildCommon":10,"./buildHTML":11,"./buildMathML":12}],14:[function(require,module,exports){ +/** + * This file deals with creating delimiters of various sizes. The TeXbook + * discusses these routines on page 441-442, in the "Another subroutine sets box + * x to a specified variable delimiter" paragraph. + * + * There are three main routines here. `makeSmallDelim` makes a delimiter in the + * normal font, but in either text, script, or scriptscript style. + * `makeLargeDelim` makes a delimiter in textstyle, but in one of the Size1, + * Size2, Size3, or Size4 fonts. `makeStackedDelim` makes a delimiter out of + * smaller pieces that are stacked on top of one another. + * + * The functions take a parameter `center`, which determines if the delimiter + * should be centered around the axis. + * + * Then, there are three exposed functions. `sizedDelim` makes a delimiter in + * one of the given sizes. This is used for things like `\bigl`. + * `customSizedDelim` makes a delimiter with a given total height+depth. It is + * called in places like `\sqrt`. `leftRightDelim` makes an appropriate + * delimiter which surrounds an expression of a given height an depth. It is + * used in `\left` and `\right`. + */ + +var ParseError = require("./ParseError"); +var Style = require("./Style"); + +var buildCommon = require("./buildCommon"); +var fontMetrics = require("./fontMetrics"); +var symbols = require("./symbols"); +var utils = require("./utils"); + +var makeSpan = buildCommon.makeSpan; + +/** + * Get the metrics for a given symbol and font, after transformation (i.e. + * after following replacement from symbols.js) + */ +var getMetrics = function(symbol, font) { + if (symbols.math[symbol] && symbols.math[symbol].replace) { + return fontMetrics.getCharacterMetrics( + symbols.math[symbol].replace, font); + } else { + return fontMetrics.getCharacterMetrics( + symbol, font); + } +}; + +/** + * Builds a symbol in the given font size (note size is an integer) + */ +var mathrmSize = function(value, size, mode) { + return buildCommon.makeSymbol(value, "Size" + size + "-Regular", mode); +}; + +/** + * Puts a delimiter span in a given style, and adds appropriate height, depth, + * and maxFontSizes. + */ +var styleWrap = function(delim, toStyle, options) { + var span = makeSpan( + ["style-wrap", options.style.reset(), toStyle.cls()], [delim]); + + var multiplier = toStyle.sizeMultiplier / options.style.sizeMultiplier; + + span.height *= multiplier; + span.depth *= multiplier; + span.maxFontSize = toStyle.sizeMultiplier; + + return span; +}; + +/** + * Makes a small delimiter. This is a delimiter that comes in the Main-Regular + * font, but is restyled to either be in textstyle, scriptstyle, or + * scriptscriptstyle. + */ +var makeSmallDelim = function(delim, style, center, options, mode) { + var text = buildCommon.makeSymbol(delim, "Main-Regular", mode); + + var span = styleWrap(text, style, options); + + if (center) { + var shift = + (1 - options.style.sizeMultiplier / style.sizeMultiplier) * + fontMetrics.metrics.axisHeight; + + span.style.top = shift + "em"; + span.height -= shift; + span.depth += shift; + } + + return span; +}; + +/** + * Makes a large delimiter. This is a delimiter that comes in the Size1, Size2, + * Size3, or Size4 fonts. It is always rendered in textstyle. + */ +var makeLargeDelim = function(delim, size, center, options, mode) { + var inner = mathrmSize(delim, size, mode); + + var span = styleWrap( + makeSpan(["delimsizing", "size" + size], + [inner], options.getColor()), + Style.TEXT, options); + + if (center) { + var shift = (1 - options.style.sizeMultiplier) * + fontMetrics.metrics.axisHeight; + + span.style.top = shift + "em"; + span.height -= shift; + span.depth += shift; + } + + return span; +}; + +/** + * Make an inner span with the given offset and in the given font. This is used + * in `makeStackedDelim` to make the stacking pieces for the delimiter. + */ +var makeInner = function(symbol, font, mode) { + var sizeClass; + // Apply the correct CSS class to choose the right font. + if (font === "Size1-Regular") { + sizeClass = "delim-size1"; + } else if (font === "Size4-Regular") { + sizeClass = "delim-size4"; + } + + var inner = makeSpan( + ["delimsizinginner", sizeClass], + [makeSpan([], [buildCommon.makeSymbol(symbol, font, mode)])]); + + // Since this will be passed into `makeVList` in the end, wrap the element + // in the appropriate tag that VList uses. + return {type: "elem", elem: inner}; +}; + +/** + * Make a stacked delimiter out of a given delimiter, with the total height at + * least `heightTotal`. This routine is mentioned on page 442 of the TeXbook. + */ +var makeStackedDelim = function(delim, heightTotal, center, options, mode) { + // There are four parts, the top, an optional middle, a repeated part, and a + // bottom. + var top; + var middle; + var repeat; + var bottom; + top = repeat = bottom = delim; + middle = null; + // Also keep track of what font the delimiters are in + var font = "Size1-Regular"; + + // We set the parts and font based on the symbol. Note that we use + // '\u23d0' instead of '|' and '\u2016' instead of '\\|' for the + // repeats of the arrows + if (delim === "\\uparrow") { + repeat = bottom = "\u23d0"; + } else if (delim === "\\Uparrow") { + repeat = bottom = "\u2016"; + } else if (delim === "\\downarrow") { + top = repeat = "\u23d0"; + } else if (delim === "\\Downarrow") { + top = repeat = "\u2016"; + } else if (delim === "\\updownarrow") { + top = "\\uparrow"; + repeat = "\u23d0"; + bottom = "\\downarrow"; + } else if (delim === "\\Updownarrow") { + top = "\\Uparrow"; + repeat = "\u2016"; + bottom = "\\Downarrow"; + } else if (delim === "[" || delim === "\\lbrack") { + top = "\u23a1"; + repeat = "\u23a2"; + bottom = "\u23a3"; + font = "Size4-Regular"; + } else if (delim === "]" || delim === "\\rbrack") { + top = "\u23a4"; + repeat = "\u23a5"; + bottom = "\u23a6"; + font = "Size4-Regular"; + } else if (delim === "\\lfloor") { + repeat = top = "\u23a2"; + bottom = "\u23a3"; + font = "Size4-Regular"; + } else if (delim === "\\lceil") { + top = "\u23a1"; + repeat = bottom = "\u23a2"; + font = "Size4-Regular"; + } else if (delim === "\\rfloor") { + repeat = top = "\u23a5"; + bottom = "\u23a6"; + font = "Size4-Regular"; + } else if (delim === "\\rceil") { + top = "\u23a4"; + repeat = bottom = "\u23a5"; + font = "Size4-Regular"; + } else if (delim === "(") { + top = "\u239b"; + repeat = "\u239c"; + bottom = "\u239d"; + font = "Size4-Regular"; + } else if (delim === ")") { + top = "\u239e"; + repeat = "\u239f"; + bottom = "\u23a0"; + font = "Size4-Regular"; + } else if (delim === "\\{" || delim === "\\lbrace") { + top = "\u23a7"; + middle = "\u23a8"; + bottom = "\u23a9"; + repeat = "\u23aa"; + font = "Size4-Regular"; + } else if (delim === "\\}" || delim === "\\rbrace") { + top = "\u23ab"; + middle = "\u23ac"; + bottom = "\u23ad"; + repeat = "\u23aa"; + font = "Size4-Regular"; + } else if (delim === "\\lgroup") { + top = "\u23a7"; + bottom = "\u23a9"; + repeat = "\u23aa"; + font = "Size4-Regular"; + } else if (delim === "\\rgroup") { + top = "\u23ab"; + bottom = "\u23ad"; + repeat = "\u23aa"; + font = "Size4-Regular"; + } else if (delim === "\\lmoustache") { + top = "\u23a7"; + bottom = "\u23ad"; + repeat = "\u23aa"; + font = "Size4-Regular"; + } else if (delim === "\\rmoustache") { + top = "\u23ab"; + bottom = "\u23a9"; + repeat = "\u23aa"; + font = "Size4-Regular"; + } else if (delim === "\\surd") { + top = "\ue001"; + bottom = "\u23b7"; + repeat = "\ue000"; + font = "Size4-Regular"; + } + + // Get the metrics of the four sections + var topMetrics = getMetrics(top, font); + var topHeightTotal = topMetrics.height + topMetrics.depth; + var repeatMetrics = getMetrics(repeat, font); + var repeatHeightTotal = repeatMetrics.height + repeatMetrics.depth; + var bottomMetrics = getMetrics(bottom, font); + var bottomHeightTotal = bottomMetrics.height + bottomMetrics.depth; + var middleHeightTotal = 0; + var middleFactor = 1; + if (middle !== null) { + var middleMetrics = getMetrics(middle, font); + middleHeightTotal = middleMetrics.height + middleMetrics.depth; + middleFactor = 2; // repeat symmetrically above and below middle + } + + // Calcuate the minimal height that the delimiter can have. + // It is at least the size of the top, bottom, and optional middle combined. + var minHeight = topHeightTotal + bottomHeightTotal + middleHeightTotal; + + // Compute the number of copies of the repeat symbol we will need + var repeatCount = Math.ceil( + (heightTotal - minHeight) / (middleFactor * repeatHeightTotal)); + + // Compute the total height of the delimiter including all the symbols + var realHeightTotal = + minHeight + repeatCount * middleFactor * repeatHeightTotal; + + // The center of the delimiter is placed at the center of the axis. Note + // that in this context, "center" means that the delimiter should be + // centered around the axis in the current style, while normally it is + // centered around the axis in textstyle. + var axisHeight = fontMetrics.metrics.axisHeight; + if (center) { + axisHeight *= options.style.sizeMultiplier; + } + // Calculate the depth + var depth = realHeightTotal / 2 - axisHeight; + + // Now, we start building the pieces that will go into the vlist + + // Keep a list of the inner pieces + var inners = []; + + // Add the bottom symbol + inners.push(makeInner(bottom, font, mode)); + + var i; + if (middle === null) { + // Add that many symbols + for (i = 0; i < repeatCount; i++) { + inners.push(makeInner(repeat, font, mode)); + } + } else { + // When there is a middle bit, we need the middle part and two repeated + // sections + for (i = 0; i < repeatCount; i++) { + inners.push(makeInner(repeat, font, mode)); + } + inners.push(makeInner(middle, font, mode)); + for (i = 0; i < repeatCount; i++) { + inners.push(makeInner(repeat, font, mode)); + } + } + + // Add the top symbol + inners.push(makeInner(top, font, mode)); + + // Finally, build the vlist + var inner = buildCommon.makeVList(inners, "bottom", depth, options); + + return styleWrap( + makeSpan(["delimsizing", "mult"], [inner], options.getColor()), + Style.TEXT, options); +}; + +// There are three kinds of delimiters, delimiters that stack when they become +// too large +var stackLargeDelimiters = [ + "(", ")", "[", "\\lbrack", "]", "\\rbrack", + "\\{", "\\lbrace", "\\}", "\\rbrace", + "\\lfloor", "\\rfloor", "\\lceil", "\\rceil", + "\\surd", +]; + +// delimiters that always stack +var stackAlwaysDelimiters = [ + "\\uparrow", "\\downarrow", "\\updownarrow", + "\\Uparrow", "\\Downarrow", "\\Updownarrow", + "|", "\\|", "\\vert", "\\Vert", + "\\lvert", "\\rvert", "\\lVert", "\\rVert", + "\\lgroup", "\\rgroup", "\\lmoustache", "\\rmoustache", +]; + +// and delimiters that never stack +var stackNeverDelimiters = [ + "<", ">", "\\langle", "\\rangle", "/", "\\backslash", "\\lt", "\\gt", +]; + +// Metrics of the different sizes. Found by looking at TeX's output of +// $\bigl| // \Bigl| \biggl| \Biggl| \showlists$ +// Used to create stacked delimiters of appropriate sizes in makeSizedDelim. +var sizeToMaxHeight = [0, 1.2, 1.8, 2.4, 3.0]; + +/** + * Used to create a delimiter of a specific size, where `size` is 1, 2, 3, or 4. + */ +var makeSizedDelim = function(delim, size, options, mode) { + // < and > turn into \langle and \rangle in delimiters + if (delim === "<" || delim === "\\lt") { + delim = "\\langle"; + } else if (delim === ">" || delim === "\\gt") { + delim = "\\rangle"; + } + + // Sized delimiters are never centered. + if (utils.contains(stackLargeDelimiters, delim) || + utils.contains(stackNeverDelimiters, delim)) { + return makeLargeDelim(delim, size, false, options, mode); + } else if (utils.contains(stackAlwaysDelimiters, delim)) { + return makeStackedDelim( + delim, sizeToMaxHeight[size], false, options, mode); + } else { + throw new ParseError("Illegal delimiter: '" + delim + "'"); + } +}; + +/** + * There are three different sequences of delimiter sizes that the delimiters + * follow depending on the kind of delimiter. This is used when creating custom + * sized delimiters to decide whether to create a small, large, or stacked + * delimiter. + * + * In real TeX, these sequences aren't explicitly defined, but are instead + * defined inside the font metrics. Since there are only three sequences that + * are possible for the delimiters that TeX defines, it is easier to just encode + * them explicitly here. + */ + +// Delimiters that never stack try small delimiters and large delimiters only +var stackNeverDelimiterSequence = [ + {type: "small", style: Style.SCRIPTSCRIPT}, + {type: "small", style: Style.SCRIPT}, + {type: "small", style: Style.TEXT}, + {type: "large", size: 1}, + {type: "large", size: 2}, + {type: "large", size: 3}, + {type: "large", size: 4}, +]; + +// Delimiters that always stack try the small delimiters first, then stack +var stackAlwaysDelimiterSequence = [ + {type: "small", style: Style.SCRIPTSCRIPT}, + {type: "small", style: Style.SCRIPT}, + {type: "small", style: Style.TEXT}, + {type: "stack"}, +]; + +// Delimiters that stack when large try the small and then large delimiters, and +// stack afterwards +var stackLargeDelimiterSequence = [ + {type: "small", style: Style.SCRIPTSCRIPT}, + {type: "small", style: Style.SCRIPT}, + {type: "small", style: Style.TEXT}, + {type: "large", size: 1}, + {type: "large", size: 2}, + {type: "large", size: 3}, + {type: "large", size: 4}, + {type: "stack"}, +]; + +/** + * Get the font used in a delimiter based on what kind of delimiter it is. + */ +var delimTypeToFont = function(type) { + if (type.type === "small") { + return "Main-Regular"; + } else if (type.type === "large") { + return "Size" + type.size + "-Regular"; + } else if (type.type === "stack") { + return "Size4-Regular"; + } +}; + +/** + * Traverse a sequence of types of delimiters to decide what kind of delimiter + * should be used to create a delimiter of the given height+depth. + */ +var traverseSequence = function(delim, height, sequence, options) { + // Here, we choose the index we should start at in the sequences. In smaller + // sizes (which correspond to larger numbers in style.size) we start earlier + // in the sequence. Thus, scriptscript starts at index 3-3=0, script starts + // at index 3-2=1, text starts at 3-1=2, and display starts at min(2,3-0)=2 + var start = Math.min(2, 3 - options.style.size); + for (var i = start; i < sequence.length; i++) { + if (sequence[i].type === "stack") { + // This is always the last delimiter, so we just break the loop now. + break; + } + + var metrics = getMetrics(delim, delimTypeToFont(sequence[i])); + var heightDepth = metrics.height + metrics.depth; + + // Small delimiters are scaled down versions of the same font, so we + // account for the style change size. + + if (sequence[i].type === "small") { + heightDepth *= sequence[i].style.sizeMultiplier; + } + + // Check if the delimiter at this size works for the given height. + if (heightDepth > height) { + return sequence[i]; + } + } + + // If we reached the end of the sequence, return the last sequence element. + return sequence[sequence.length - 1]; +}; + +/** + * Make a delimiter of a given height+depth, with optional centering. Here, we + * traverse the sequences, and create a delimiter that the sequence tells us to. + */ +var makeCustomSizedDelim = function(delim, height, center, options, mode) { + if (delim === "<" || delim === "\\lt") { + delim = "\\langle"; + } else if (delim === ">" || delim === "\\gt") { + delim = "\\rangle"; + } + + // Decide what sequence to use + var sequence; + if (utils.contains(stackNeverDelimiters, delim)) { + sequence = stackNeverDelimiterSequence; + } else if (utils.contains(stackLargeDelimiters, delim)) { + sequence = stackLargeDelimiterSequence; + } else { + sequence = stackAlwaysDelimiterSequence; + } + + // Look through the sequence + var delimType = traverseSequence(delim, height, sequence, options); + + // Depending on the sequence element we decided on, call the appropriate + // function. + if (delimType.type === "small") { + return makeSmallDelim(delim, delimType.style, center, options, mode); + } else if (delimType.type === "large") { + return makeLargeDelim(delim, delimType.size, center, options, mode); + } else if (delimType.type === "stack") { + return makeStackedDelim(delim, height, center, options, mode); + } +}; + +/** + * Make a delimiter for use with `\left` and `\right`, given a height and depth + * of an expression that the delimiters surround. + */ +var makeLeftRightDelim = function(delim, height, depth, options, mode) { + // We always center \left/\right delimiters, so the axis is always shifted + var axisHeight = + fontMetrics.metrics.axisHeight * options.style.sizeMultiplier; + + // Taken from TeX source, tex.web, function make_left_right + var delimiterFactor = 901; + var delimiterExtend = 5.0 / fontMetrics.metrics.ptPerEm; + + var maxDistFromAxis = Math.max( + height - axisHeight, depth + axisHeight); + + var totalHeight = Math.max( + // In real TeX, calculations are done using integral values which are + // 65536 per pt, or 655360 per em. So, the division here truncates in + // TeX but doesn't here, producing different results. If we wanted to + // exactly match TeX's calculation, we could do + // Math.floor(655360 * maxDistFromAxis / 500) * + // delimiterFactor / 655360 + // (To see the difference, compare + // x^{x^{\left(\rule{0.1em}{0.68em}\right)}} + // in TeX and KaTeX) + maxDistFromAxis / 500 * delimiterFactor, + 2 * maxDistFromAxis - delimiterExtend); + + // Finally, we defer to `makeCustomSizedDelim` with our calculated total + // height + return makeCustomSizedDelim(delim, totalHeight, true, options, mode); +}; + +module.exports = { + sizedDelim: makeSizedDelim, + customSizedDelim: makeCustomSizedDelim, + leftRightDelim: makeLeftRightDelim, +}; + +},{"./ParseError":6,"./Style":9,"./buildCommon":10,"./fontMetrics":17,"./symbols":23,"./utils":25}],15:[function(require,module,exports){ +/** + * These objects store the data about the DOM nodes we create, as well as some + * extra data. They can then be transformed into real DOM nodes with the + * `toNode` function or HTML markup using `toMarkup`. They are useful for both + * storing extra properties on the nodes, as well as providing a way to easily + * work with the DOM. + * + * Similar functions for working with MathML nodes exist in mathMLTree.js. + */ +var unicodeRegexes = require("./unicodeRegexes"); +var utils = require("./utils"); + +/** + * Create an HTML className based on a list of classes. In addition to joining + * with spaces, we also remove null or empty classes. + */ +var createClass = function(classes) { + classes = classes.slice(); + for (var i = classes.length - 1; i >= 0; i--) { + if (!classes[i]) { + classes.splice(i, 1); + } + } + + return classes.join(" "); +}; + +/** + * This node represents a span node, with a className, a list of children, and + * an inline style. It also contains information about its height, depth, and + * maxFontSize. + */ +function span(classes, children, height, depth, maxFontSize, style) { + this.classes = classes || []; + this.children = children || []; + this.height = height || 0; + this.depth = depth || 0; + this.maxFontSize = maxFontSize || 0; + this.style = style || {}; + this.attributes = {}; +} + +/** + * Sets an arbitrary attribute on the span. Warning: use this wisely. Not all + * browsers support attributes the same, and having too many custom attributes + * is probably bad. + */ +span.prototype.setAttribute = function(attribute, value) { + this.attributes[attribute] = value; +}; + +/** + * Convert the span into an HTML node + */ +span.prototype.toNode = function() { + var span = document.createElement("span"); + + // Apply the class + span.className = createClass(this.classes); + + // Apply inline styles + for (var style in this.style) { + if (Object.prototype.hasOwnProperty.call(this.style, style)) { + span.style[style] = this.style[style]; + } + } + + // Apply attributes + for (var attr in this.attributes) { + if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { + span.setAttribute(attr, this.attributes[attr]); + } + } + + // Append the children, also as HTML nodes + for (var i = 0; i < this.children.length; i++) { + span.appendChild(this.children[i].toNode()); + } + + return span; +}; + +/** + * Convert the span into an HTML markup string + */ +span.prototype.toMarkup = function() { + var markup = "<span"; + + // Add the class + if (this.classes.length) { + markup += " class=\""; + markup += utils.escape(createClass(this.classes)); + markup += "\""; + } + + var styles = ""; + + // Add the styles, after hyphenation + for (var style in this.style) { + if (this.style.hasOwnProperty(style)) { + styles += utils.hyphenate(style) + ":" + this.style[style] + ";"; + } + } + + if (styles) { + markup += " style=\"" + utils.escape(styles) + "\""; + } + + // Add the attributes + for (var attr in this.attributes) { + if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { + markup += " " + attr + "=\""; + markup += utils.escape(this.attributes[attr]); + markup += "\""; + } + } + + markup += ">"; + + // Add the markup of the children, also as markup + for (var i = 0; i < this.children.length; i++) { + markup += this.children[i].toMarkup(); + } + + markup += "</span>"; + + return markup; +}; + +/** + * This node represents a document fragment, which contains elements, but when + * placed into the DOM doesn't have any representation itself. Thus, it only + * contains children and doesn't have any HTML properties. It also keeps track + * of a height, depth, and maxFontSize. + */ +function documentFragment(children, height, depth, maxFontSize) { + this.children = children || []; + this.height = height || 0; + this.depth = depth || 0; + this.maxFontSize = maxFontSize || 0; +} + +/** + * Convert the fragment into a node + */ +documentFragment.prototype.toNode = function() { + // Create a fragment + var frag = document.createDocumentFragment(); + + // Append the children + for (var i = 0; i < this.children.length; i++) { + frag.appendChild(this.children[i].toNode()); + } + + return frag; +}; + +/** + * Convert the fragment into HTML markup + */ +documentFragment.prototype.toMarkup = function() { + var markup = ""; + + // Simply concatenate the markup for the children together + for (var i = 0; i < this.children.length; i++) { + markup += this.children[i].toMarkup(); + } + + return markup; +}; + +var iCombinations = { + 'î': '\u0131\u0302', + 'ï': '\u0131\u0308', + 'í': '\u0131\u0301', + // 'ī': '\u0131\u0304', // enable when we add Extended Latin + 'ì': '\u0131\u0300', +}; + +/** + * A symbol node contains information about a single symbol. It either renders + * to a single text node, or a span with a single text node in it, depending on + * whether it has CSS classes, styles, or needs italic correction. + */ +function symbolNode(value, height, depth, italic, skew, classes, style) { + this.value = value || ""; + this.height = height || 0; + this.depth = depth || 0; + this.italic = italic || 0; + this.skew = skew || 0; + this.classes = classes || []; + this.style = style || {}; + this.maxFontSize = 0; + + // Mark CJK characters with specific classes so that we can specify which + // fonts to use. This allows us to render these characters with a serif + // font in situations where the browser would either default to a sans serif + // or render a placeholder character. + if (unicodeRegexes.cjkRegex.test(value)) { + // I couldn't find any fonts that contained Hangul as well as all of + // the other characters we wanted to test there for it gets its own + // CSS class. + if (unicodeRegexes.hangulRegex.test(value)) { + this.classes.push('hangul_fallback'); + } else { + this.classes.push('cjk_fallback'); + } + } + + if (/[îïíì]/.test(this.value)) { // add ī when we add Extended Latin + this.value = iCombinations[this.value]; + } +} + +/** + * Creates a text node or span from a symbol node. Note that a span is only + * created if it is needed. + */ +symbolNode.prototype.toNode = function() { + var node = document.createTextNode(this.value); + var span = null; + + if (this.italic > 0) { + span = document.createElement("span"); + span.style.marginRight = this.italic + "em"; + } + + if (this.classes.length > 0) { + span = span || document.createElement("span"); + span.className = createClass(this.classes); + } + + for (var style in this.style) { + if (this.style.hasOwnProperty(style)) { + span = span || document.createElement("span"); + span.style[style] = this.style[style]; + } + } + + if (span) { + span.appendChild(node); + return span; + } else { + return node; + } +}; + +/** + * Creates markup for a symbol node. + */ +symbolNode.prototype.toMarkup = function() { + // TODO(alpert): More duplication than I'd like from + // span.prototype.toMarkup and symbolNode.prototype.toNode... + var needsSpan = false; + + var markup = "<span"; + + if (this.classes.length) { + needsSpan = true; + markup += " class=\""; + markup += utils.escape(createClass(this.classes)); + markup += "\""; + } + + var styles = ""; + + if (this.italic > 0) { + styles += "margin-right:" + this.italic + "em;"; + } + for (var style in this.style) { + if (this.style.hasOwnProperty(style)) { + styles += utils.hyphenate(style) + ":" + this.style[style] + ";"; + } + } + + if (styles) { + needsSpan = true; + markup += " style=\"" + utils.escape(styles) + "\""; + } + + var escaped = utils.escape(this.value); + if (needsSpan) { + markup += ">"; + markup += escaped; + markup += "</span>"; + return markup; + } else { + return escaped; + } +}; + +module.exports = { + span: span, + documentFragment: documentFragment, + symbolNode: symbolNode, +}; + +},{"./unicodeRegexes":24,"./utils":25}],16:[function(require,module,exports){ +/* eslint no-constant-condition:0 */ +var fontMetrics = require("./fontMetrics"); +var parseData = require("./parseData"); +var ParseError = require("./ParseError"); + +var ParseNode = parseData.ParseNode; + +/** + * Parse the body of the environment, with rows delimited by \\ and + * columns delimited by &, and create a nested list in row-major order + * with one group per cell. + */ +function parseArray(parser, result) { + var row = []; + var body = [row]; + var rowGaps = []; + while (true) { + var cell = parser.parseExpression(false, null); + row.push(new ParseNode("ordgroup", cell, parser.mode)); + var next = parser.nextToken.text; + if (next === "&") { + parser.consume(); + } else if (next === "\\end") { + break; + } else if (next === "\\\\" || next === "\\cr") { + var cr = parser.parseFunction(); + rowGaps.push(cr.value.size); + row = []; + body.push(row); + } else { + throw new ParseError("Expected & or \\\\ or \\end", + parser.nextToken); + } + } + result.body = body; + result.rowGaps = rowGaps; + return new ParseNode(result.type, result, parser.mode); +} + +/* + * An environment definition is very similar to a function definition: + * it is declared with a name or a list of names, a set of properties + * and a handler containing the actual implementation. + * + * The properties include: + * - numArgs: The number of arguments after the \begin{name} function. + * - argTypes: (optional) Just like for a function + * - allowedInText: (optional) Whether or not the environment is allowed inside + * text mode (default false) (not enforced yet) + * - numOptionalArgs: (optional) Just like for a function + * A bare number instead of that object indicates the numArgs value. + * + * The handler function will receive two arguments + * - context: information and references provided by the parser + * - args: an array of arguments passed to \begin{name} + * The context contains the following properties: + * - envName: the name of the environment, one of the listed names. + * - parser: the parser object + * - lexer: the lexer object + * - positions: the positions associated with these arguments from args. + * The handler must return a ParseResult. + */ + +function defineEnvironment(names, props, handler) { + if (typeof names === "string") { + names = [names]; + } + if (typeof props === "number") { + props = { numArgs: props }; + } + // Set default values of environments + var data = { + numArgs: props.numArgs || 0, + argTypes: props.argTypes, + greediness: 1, + allowedInText: !!props.allowedInText, + numOptionalArgs: props.numOptionalArgs || 0, + handler: handler, + }; + for (var i = 0; i < names.length; ++i) { + module.exports[names[i]] = data; + } +} + +// Arrays are part of LaTeX, defined in lttab.dtx so its documentation +// is part of the source2e.pdf file of LaTeX2e source documentation. +defineEnvironment("array", { + numArgs: 1, +}, function(context, args) { + var colalign = args[0]; + colalign = colalign.value.map ? colalign.value : [colalign]; + var cols = colalign.map(function(node) { + var ca = node.value; + if ("lcr".indexOf(ca) !== -1) { + return { + type: "align", + align: ca, + }; + } else if (ca === "|") { + return { + type: "separator", + separator: "|", + }; + } + throw new ParseError( + "Unknown column alignment: " + node.value, + node); + }); + var res = { + type: "array", + cols: cols, + hskipBeforeAndAfter: true, // \@preamble in lttab.dtx + }; + res = parseArray(context.parser, res); + return res; +}); + +// The matrix environments of amsmath builds on the array environment +// of LaTeX, which is discussed above. +defineEnvironment([ + "matrix", + "pmatrix", + "bmatrix", + "Bmatrix", + "vmatrix", + "Vmatrix", +], { +}, function(context) { + var delimiters = { + "matrix": null, + "pmatrix": ["(", ")"], + "bmatrix": ["[", "]"], + "Bmatrix": ["\\{", "\\}"], + "vmatrix": ["|", "|"], + "Vmatrix": ["\\Vert", "\\Vert"], + }[context.envName]; + var res = { + type: "array", + hskipBeforeAndAfter: false, // \hskip -\arraycolsep in amsmath + }; + res = parseArray(context.parser, res); + if (delimiters) { + res = new ParseNode("leftright", { + body: [res], + left: delimiters[0], + right: delimiters[1], + }, context.mode); + } + return res; +}); + +// A cases environment (in amsmath.sty) is almost equivalent to +// \def\arraystretch{1.2}% +// \left\{\begin{array}{@{}l@{\quad}l@{}} … \end{array}\right. +defineEnvironment("cases", { +}, function(context) { + var res = { + type: "array", + arraystretch: 1.2, + cols: [{ + type: "align", + align: "l", + pregap: 0, + postgap: fontMetrics.metrics.quad, + }, { + type: "align", + align: "l", + pregap: 0, + postgap: 0, + }], + }; + res = parseArray(context.parser, res); + res = new ParseNode("leftright", { + body: [res], + left: "\\{", + right: ".", + }, context.mode); + return res; +}); + +// An aligned environment is like the align* environment +// except it operates within math mode. +// Note that we assume \nomallineskiplimit to be zero, +// so that \strut@ is the same as \strut. +defineEnvironment("aligned", { +}, function(context) { + var res = { + type: "array", + cols: [], + }; + res = parseArray(context.parser, res); + var emptyGroup = new ParseNode("ordgroup", [], context.mode); + var numCols = 0; + res.value.body.forEach(function(row) { + var i; + for (i = 1; i < row.length; i += 2) { + row[i].value.unshift(emptyGroup); + } + if (numCols < row.length) { + numCols = row.length; + } + }); + for (var i = 0; i < numCols; ++i) { + var align = "r"; + var pregap = 0; + if (i % 2 === 1) { + align = "l"; + } else if (i > 0) { + pregap = 2; // one \qquad between columns + } + res.value.cols[i] = { + type: "align", + align: align, + pregap: pregap, + postgap: 0, + }; + } + return res; +}); + +},{"./ParseError":6,"./fontMetrics":17,"./parseData":21}],17:[function(require,module,exports){ +/* eslint no-unused-vars:0 */ + +var Style = require("./Style"); +var cjkRegex = require("./unicodeRegexes").cjkRegex; + +/** + * This file contains metrics regarding fonts and individual symbols. The sigma + * and xi variables, as well as the metricMap map contain data extracted from + * TeX, TeX font metrics, and the TTF files. These data are then exposed via the + * `metrics` variable and the getCharacterMetrics function. + */ + +// These font metrics are extracted from TeX by using +// \font\a=cmmi10 +// \showthe\fontdimenX\a +// where X is the corresponding variable number. These correspond to the font +// parameters of the symbol fonts. In TeX, there are actually three sets of +// dimensions, one for each of textstyle, scriptstyle, and scriptscriptstyle, +// but we only use the textstyle ones, and scale certain dimensions accordingly. +// See the TeXbook, page 441. +var sigma1 = 0.025; +var sigma2 = 0; +var sigma3 = 0; +var sigma4 = 0; +var sigma5 = 0.431; +var sigma6 = 1; +var sigma7 = 0; +var sigma8 = 0.677; +var sigma9 = 0.394; +var sigma10 = 0.444; +var sigma11 = 0.686; +var sigma12 = 0.345; +var sigma13 = 0.413; +var sigma14 = 0.363; +var sigma15 = 0.289; +var sigma16 = 0.150; +var sigma17 = 0.247; +var sigma18 = 0.386; +var sigma19 = 0.050; +var sigma20 = 2.390; +var sigma21 = 1.01; +var sigma21Script = 0.81; +var sigma21ScriptScript = 0.71; +var sigma22 = 0.250; + +// These font metrics are extracted from TeX by using +// \font\a=cmex10 +// \showthe\fontdimenX\a +// where X is the corresponding variable number. These correspond to the font +// parameters of the extension fonts (family 3). See the TeXbook, page 441. +var xi1 = 0; +var xi2 = 0; +var xi3 = 0; +var xi4 = 0; +var xi5 = 0.431; +var xi6 = 1; +var xi7 = 0; +var xi8 = 0.04; +var xi9 = 0.111; +var xi10 = 0.166; +var xi11 = 0.2; +var xi12 = 0.6; +var xi13 = 0.1; + +// This value determines how large a pt is, for metrics which are defined in +// terms of pts. +// This value is also used in katex.less; if you change it make sure the values +// match. +var ptPerEm = 10.0; + +// The space between adjacent `|` columns in an array definition. From +// `\showthe\doublerulesep` in LaTeX. +var doubleRuleSep = 2.0 / ptPerEm; + +/** + * This is just a mapping from common names to real metrics + */ +var metrics = { + xHeight: sigma5, + quad: sigma6, + num1: sigma8, + num2: sigma9, + num3: sigma10, + denom1: sigma11, + denom2: sigma12, + sup1: sigma13, + sup2: sigma14, + sup3: sigma15, + sub1: sigma16, + sub2: sigma17, + supDrop: sigma18, + subDrop: sigma19, + axisHeight: sigma22, + defaultRuleThickness: xi8, + bigOpSpacing1: xi9, + bigOpSpacing2: xi10, + bigOpSpacing3: xi11, + bigOpSpacing4: xi12, + bigOpSpacing5: xi13, + ptPerEm: ptPerEm, + emPerEx: sigma5 / sigma6, + doubleRuleSep: doubleRuleSep, + + // TODO(alpert): Missing parallel structure here. We should probably add + // style-specific metrics for all of these. + delim1: sigma20, + getDelim2: function(style) { + if (style.size === Style.TEXT.size) { + return sigma21; + } else if (style.size === Style.SCRIPT.size) { + return sigma21Script; + } else if (style.size === Style.SCRIPTSCRIPT.size) { + return sigma21ScriptScript; + } + throw new Error("Unexpected style size: " + style.size); + }, +}; + +// This map contains a mapping from font name and character code to character +// metrics, including height, depth, italic correction, and skew (kern from the +// character to the corresponding \skewchar) +// This map is generated via `make metrics`. It should not be changed manually. +var metricMap = require("./fontMetricsData"); + +// These are very rough approximations. We default to Times New Roman which +// should have Latin-1 and Cyrillic characters, but may not depending on the +// operating system. The metrics do not account for extra height from the +// accents. In the case of Cyrillic characters which have both ascenders and +// descenders we prefer approximations with ascenders, primarily to prevent +// the fraction bar or root line from intersecting the glyph. +// TODO(kevinb) allow union of multiple glyph metrics for better accuracy. +var extraCharacterMap = { + // Latin-1 + 'À': 'A', + 'Á': 'A', + 'Â': 'A', + 'Ã': 'A', + 'Ä': 'A', + 'Å': 'A', + 'Æ': 'A', + 'Ç': 'C', + 'È': 'E', + 'É': 'E', + 'Ê': 'E', + 'Ë': 'E', + 'Ì': 'I', + 'Í': 'I', + 'Î': 'I', + 'Ï': 'I', + 'Ð': 'D', + 'Ñ': 'N', + 'Ò': 'O', + 'Ó': 'O', + 'Ô': 'O', + 'Õ': 'O', + 'Ö': 'O', + 'Ø': 'O', + 'Ù': 'U', + 'Ú': 'U', + 'Û': 'U', + 'Ü': 'U', + 'Ý': 'Y', + 'Þ': 'o', + 'ß': 'B', + 'à': 'a', + 'á': 'a', + 'â': 'a', + 'ã': 'a', + 'ä': 'a', + 'å': 'a', + 'æ': 'a', + 'ç': 'c', + 'è': 'e', + 'é': 'e', + 'ê': 'e', + 'ë': 'e', + 'ì': 'i', + 'í': 'i', + 'î': 'i', + 'ï': 'i', + 'ð': 'd', + 'ñ': 'n', + 'ò': 'o', + 'ó': 'o', + 'ô': 'o', + 'õ': 'o', + 'ö': 'o', + 'ø': 'o', + 'ù': 'u', + 'ú': 'u', + 'û': 'u', + 'ü': 'u', + 'ý': 'y', + 'þ': 'o', + 'ÿ': 'y', + + // Cyrillic + 'А': 'A', + 'Б': 'B', + 'В': 'B', + 'Г': 'F', + 'Д': 'A', + 'Е': 'E', + 'Ж': 'K', + 'З': '3', + 'И': 'N', + 'Й': 'N', + 'К': 'K', + 'Л': 'N', + 'М': 'M', + 'Н': 'H', + 'О': 'O', + 'П': 'N', + 'Р': 'P', + 'С': 'C', + 'Т': 'T', + 'У': 'y', + 'Ф': 'O', + 'Х': 'X', + 'Ц': 'U', + 'Ч': 'h', + 'Ш': 'W', + 'Щ': 'W', + 'Ъ': 'B', + 'Ы': 'X', + 'Ь': 'B', + 'Э': '3', + 'Ю': 'X', + 'Я': 'R', + 'а': 'a', + 'б': 'b', + 'в': 'a', + 'г': 'r', + 'д': 'y', + 'е': 'e', + 'ж': 'm', + 'з': 'e', + 'и': 'n', + 'й': 'n', + 'к': 'n', + 'л': 'n', + 'м': 'm', + 'н': 'n', + 'о': 'o', + 'п': 'n', + 'р': 'p', + 'с': 'c', + 'т': 'o', + 'у': 'y', + 'ф': 'b', + 'х': 'x', + 'ц': 'n', + 'ч': 'n', + 'ш': 'w', + 'щ': 'w', + 'ъ': 'a', + 'ы': 'm', + 'ь': 'a', + 'э': 'e', + 'ю': 'm', + 'я': 'r', +}; + +/** + * This function is a convenience function for looking up information in the + * metricMap table. It takes a character as a string, and a style. + * + * Note: the `width` property may be undefined if fontMetricsData.js wasn't + * built using `Make extended_metrics`. + */ +var getCharacterMetrics = function(character, style) { + var ch = character.charCodeAt(0); + if (character[0] in extraCharacterMap) { + ch = extraCharacterMap[character[0]].charCodeAt(0); + } else if (cjkRegex.test(character[0])) { + ch = 'M'.charCodeAt(0); + } + var metrics = metricMap[style][ch]; + if (metrics) { + return { + depth: metrics[0], + height: metrics[1], + italic: metrics[2], + skew: metrics[3], + width: metrics[4], + }; + } +}; + +module.exports = { + metrics: metrics, + getCharacterMetrics: getCharacterMetrics, +}; + +},{"./Style":9,"./fontMetricsData":18,"./unicodeRegexes":24}],18:[function(require,module,exports){ +module.exports = { + "AMS-Regular": { + "65": [0, 0.68889, 0, 0], + "66": [0, 0.68889, 0, 0], + "67": [0, 0.68889, 0, 0], + "68": [0, 0.68889, 0, 0], + "69": [0, 0.68889, 0, 0], + "70": [0, 0.68889, 0, 0], + "71": [0, 0.68889, 0, 0], + "72": [0, 0.68889, 0, 0], + "73": [0, 0.68889, 0, 0], + "74": [0.16667, 0.68889, 0, 0], + "75": [0, 0.68889, 0, 0], + "76": [0, 0.68889, 0, 0], + "77": [0, 0.68889, 0, 0], + "78": [0, 0.68889, 0, 0], + "79": [0.16667, 0.68889, 0, 0], + "80": [0, 0.68889, 0, 0], + "81": [0.16667, 0.68889, 0, 0], + "82": [0, 0.68889, 0, 0], + "83": [0, 0.68889, 0, 0], + "84": [0, 0.68889, 0, 0], + "85": [0, 0.68889, 0, 0], + "86": [0, 0.68889, 0, 0], + "87": [0, 0.68889, 0, 0], + "88": [0, 0.68889, 0, 0], + "89": [0, 0.68889, 0, 0], + "90": [0, 0.68889, 0, 0], + "107": [0, 0.68889, 0, 0], + "165": [0, 0.675, 0.025, 0], + "174": [0.15559, 0.69224, 0, 0], + "240": [0, 0.68889, 0, 0], + "295": [0, 0.68889, 0, 0], + "710": [0, 0.825, 0, 0], + "732": [0, 0.9, 0, 0], + "770": [0, 0.825, 0, 0], + "771": [0, 0.9, 0, 0], + "989": [0.08167, 0.58167, 0, 0], + "1008": [0, 0.43056, 0.04028, 0], + "8245": [0, 0.54986, 0, 0], + "8463": [0, 0.68889, 0, 0], + "8487": [0, 0.68889, 0, 0], + "8498": [0, 0.68889, 0, 0], + "8502": [0, 0.68889, 0, 0], + "8503": [0, 0.68889, 0, 0], + "8504": [0, 0.68889, 0, 0], + "8513": [0, 0.68889, 0, 0], + "8592": [-0.03598, 0.46402, 0, 0], + "8594": [-0.03598, 0.46402, 0, 0], + "8602": [-0.13313, 0.36687, 0, 0], + "8603": [-0.13313, 0.36687, 0, 0], + "8606": [0.01354, 0.52239, 0, 0], + "8608": [0.01354, 0.52239, 0, 0], + "8610": [0.01354, 0.52239, 0, 0], + "8611": [0.01354, 0.52239, 0, 0], + "8619": [0, 0.54986, 0, 0], + "8620": [0, 0.54986, 0, 0], + "8621": [-0.13313, 0.37788, 0, 0], + "8622": [-0.13313, 0.36687, 0, 0], + "8624": [0, 0.69224, 0, 0], + "8625": [0, 0.69224, 0, 0], + "8630": [0, 0.43056, 0, 0], + "8631": [0, 0.43056, 0, 0], + "8634": [0.08198, 0.58198, 0, 0], + "8635": [0.08198, 0.58198, 0, 0], + "8638": [0.19444, 0.69224, 0, 0], + "8639": [0.19444, 0.69224, 0, 0], + "8642": [0.19444, 0.69224, 0, 0], + "8643": [0.19444, 0.69224, 0, 0], + "8644": [0.1808, 0.675, 0, 0], + "8646": [0.1808, 0.675, 0, 0], + "8647": [0.1808, 0.675, 0, 0], + "8648": [0.19444, 0.69224, 0, 0], + "8649": [0.1808, 0.675, 0, 0], + "8650": [0.19444, 0.69224, 0, 0], + "8651": [0.01354, 0.52239, 0, 0], + "8652": [0.01354, 0.52239, 0, 0], + "8653": [-0.13313, 0.36687, 0, 0], + "8654": [-0.13313, 0.36687, 0, 0], + "8655": [-0.13313, 0.36687, 0, 0], + "8666": [0.13667, 0.63667, 0, 0], + "8667": [0.13667, 0.63667, 0, 0], + "8669": [-0.13313, 0.37788, 0, 0], + "8672": [-0.064, 0.437, 0, 0], + "8674": [-0.064, 0.437, 0, 0], + "8705": [0, 0.825, 0, 0], + "8708": [0, 0.68889, 0, 0], + "8709": [0.08167, 0.58167, 0, 0], + "8717": [0, 0.43056, 0, 0], + "8722": [-0.03598, 0.46402, 0, 0], + "8724": [0.08198, 0.69224, 0, 0], + "8726": [0.08167, 0.58167, 0, 0], + "8733": [0, 0.69224, 0, 0], + "8736": [0, 0.69224, 0, 0], + "8737": [0, 0.69224, 0, 0], + "8738": [0.03517, 0.52239, 0, 0], + "8739": [0.08167, 0.58167, 0, 0], + "8740": [0.25142, 0.74111, 0, 0], + "8741": [0.08167, 0.58167, 0, 0], + "8742": [0.25142, 0.74111, 0, 0], + "8756": [0, 0.69224, 0, 0], + "8757": [0, 0.69224, 0, 0], + "8764": [-0.13313, 0.36687, 0, 0], + "8765": [-0.13313, 0.37788, 0, 0], + "8769": [-0.13313, 0.36687, 0, 0], + "8770": [-0.03625, 0.46375, 0, 0], + "8774": [0.30274, 0.79383, 0, 0], + "8776": [-0.01688, 0.48312, 0, 0], + "8778": [0.08167, 0.58167, 0, 0], + "8782": [0.06062, 0.54986, 0, 0], + "8783": [0.06062, 0.54986, 0, 0], + "8785": [0.08198, 0.58198, 0, 0], + "8786": [0.08198, 0.58198, 0, 0], + "8787": [0.08198, 0.58198, 0, 0], + "8790": [0, 0.69224, 0, 0], + "8791": [0.22958, 0.72958, 0, 0], + "8796": [0.08198, 0.91667, 0, 0], + "8806": [0.25583, 0.75583, 0, 0], + "8807": [0.25583, 0.75583, 0, 0], + "8808": [0.25142, 0.75726, 0, 0], + "8809": [0.25142, 0.75726, 0, 0], + "8812": [0.25583, 0.75583, 0, 0], + "8814": [0.20576, 0.70576, 0, 0], + "8815": [0.20576, 0.70576, 0, 0], + "8816": [0.30274, 0.79383, 0, 0], + "8817": [0.30274, 0.79383, 0, 0], + "8818": [0.22958, 0.72958, 0, 0], + "8819": [0.22958, 0.72958, 0, 0], + "8822": [0.1808, 0.675, 0, 0], + "8823": [0.1808, 0.675, 0, 0], + "8828": [0.13667, 0.63667, 0, 0], + "8829": [0.13667, 0.63667, 0, 0], + "8830": [0.22958, 0.72958, 0, 0], + "8831": [0.22958, 0.72958, 0, 0], + "8832": [0.20576, 0.70576, 0, 0], + "8833": [0.20576, 0.70576, 0, 0], + "8840": [0.30274, 0.79383, 0, 0], + "8841": [0.30274, 0.79383, 0, 0], + "8842": [0.13597, 0.63597, 0, 0], + "8843": [0.13597, 0.63597, 0, 0], + "8847": [0.03517, 0.54986, 0, 0], + "8848": [0.03517, 0.54986, 0, 0], + "8858": [0.08198, 0.58198, 0, 0], + "8859": [0.08198, 0.58198, 0, 0], + "8861": [0.08198, 0.58198, 0, 0], + "8862": [0, 0.675, 0, 0], + "8863": [0, 0.675, 0, 0], + "8864": [0, 0.675, 0, 0], + "8865": [0, 0.675, 0, 0], + "8872": [0, 0.69224, 0, 0], + "8873": [0, 0.69224, 0, 0], + "8874": [0, 0.69224, 0, 0], + "8876": [0, 0.68889, 0, 0], + "8877": [0, 0.68889, 0, 0], + "8878": [0, 0.68889, 0, 0], + "8879": [0, 0.68889, 0, 0], + "8882": [0.03517, 0.54986, 0, 0], + "8883": [0.03517, 0.54986, 0, 0], + "8884": [0.13667, 0.63667, 0, 0], + "8885": [0.13667, 0.63667, 0, 0], + "8888": [0, 0.54986, 0, 0], + "8890": [0.19444, 0.43056, 0, 0], + "8891": [0.19444, 0.69224, 0, 0], + "8892": [0.19444, 0.69224, 0, 0], + "8901": [0, 0.54986, 0, 0], + "8903": [0.08167, 0.58167, 0, 0], + "8905": [0.08167, 0.58167, 0, 0], + "8906": [0.08167, 0.58167, 0, 0], + "8907": [0, 0.69224, 0, 0], + "8908": [0, 0.69224, 0, 0], + "8909": [-0.03598, 0.46402, 0, 0], + "8910": [0, 0.54986, 0, 0], + "8911": [0, 0.54986, 0, 0], + "8912": [0.03517, 0.54986, 0, 0], + "8913": [0.03517, 0.54986, 0, 0], + "8914": [0, 0.54986, 0, 0], + "8915": [0, 0.54986, 0, 0], + "8916": [0, 0.69224, 0, 0], + "8918": [0.0391, 0.5391, 0, 0], + "8919": [0.0391, 0.5391, 0, 0], + "8920": [0.03517, 0.54986, 0, 0], + "8921": [0.03517, 0.54986, 0, 0], + "8922": [0.38569, 0.88569, 0, 0], + "8923": [0.38569, 0.88569, 0, 0], + "8926": [0.13667, 0.63667, 0, 0], + "8927": [0.13667, 0.63667, 0, 0], + "8928": [0.30274, 0.79383, 0, 0], + "8929": [0.30274, 0.79383, 0, 0], + "8934": [0.23222, 0.74111, 0, 0], + "8935": [0.23222, 0.74111, 0, 0], + "8936": [0.23222, 0.74111, 0, 0], + "8937": [0.23222, 0.74111, 0, 0], + "8938": [0.20576, 0.70576, 0, 0], + "8939": [0.20576, 0.70576, 0, 0], + "8940": [0.30274, 0.79383, 0, 0], + "8941": [0.30274, 0.79383, 0, 0], + "8994": [0.19444, 0.69224, 0, 0], + "8995": [0.19444, 0.69224, 0, 0], + "9416": [0.15559, 0.69224, 0, 0], + "9484": [0, 0.69224, 0, 0], + "9488": [0, 0.69224, 0, 0], + "9492": [0, 0.37788, 0, 0], + "9496": [0, 0.37788, 0, 0], + "9585": [0.19444, 0.68889, 0, 0], + "9586": [0.19444, 0.74111, 0, 0], + "9632": [0, 0.675, 0, 0], + "9633": [0, 0.675, 0, 0], + "9650": [0, 0.54986, 0, 0], + "9651": [0, 0.54986, 0, 0], + "9654": [0.03517, 0.54986, 0, 0], + "9660": [0, 0.54986, 0, 0], + "9661": [0, 0.54986, 0, 0], + "9664": [0.03517, 0.54986, 0, 0], + "9674": [0.11111, 0.69224, 0, 0], + "9733": [0.19444, 0.69224, 0, 0], + "10003": [0, 0.69224, 0, 0], + "10016": [0, 0.69224, 0, 0], + "10731": [0.11111, 0.69224, 0, 0], + "10846": [0.19444, 0.75583, 0, 0], + "10877": [0.13667, 0.63667, 0, 0], + "10878": [0.13667, 0.63667, 0, 0], + "10885": [0.25583, 0.75583, 0, 0], + "10886": [0.25583, 0.75583, 0, 0], + "10887": [0.13597, 0.63597, 0, 0], + "10888": [0.13597, 0.63597, 0, 0], + "10889": [0.26167, 0.75726, 0, 0], + "10890": [0.26167, 0.75726, 0, 0], + "10891": [0.48256, 0.98256, 0, 0], + "10892": [0.48256, 0.98256, 0, 0], + "10901": [0.13667, 0.63667, 0, 0], + "10902": [0.13667, 0.63667, 0, 0], + "10933": [0.25142, 0.75726, 0, 0], + "10934": [0.25142, 0.75726, 0, 0], + "10935": [0.26167, 0.75726, 0, 0], + "10936": [0.26167, 0.75726, 0, 0], + "10937": [0.26167, 0.75726, 0, 0], + "10938": [0.26167, 0.75726, 0, 0], + "10949": [0.25583, 0.75583, 0, 0], + "10950": [0.25583, 0.75583, 0, 0], + "10955": [0.28481, 0.79383, 0, 0], + "10956": [0.28481, 0.79383, 0, 0], + "57350": [0.08167, 0.58167, 0, 0], + "57351": [0.08167, 0.58167, 0, 0], + "57352": [0.08167, 0.58167, 0, 0], + "57353": [0, 0.43056, 0.04028, 0], + "57356": [0.25142, 0.75726, 0, 0], + "57357": [0.25142, 0.75726, 0, 0], + "57358": [0.41951, 0.91951, 0, 0], + "57359": [0.30274, 0.79383, 0, 0], + "57360": [0.30274, 0.79383, 0, 0], + "57361": [0.41951, 0.91951, 0, 0], + "57366": [0.25142, 0.75726, 0, 0], + "57367": [0.25142, 0.75726, 0, 0], + "57368": [0.25142, 0.75726, 0, 0], + "57369": [0.25142, 0.75726, 0, 0], + "57370": [0.13597, 0.63597, 0, 0], + "57371": [0.13597, 0.63597, 0, 0], + }, + "Caligraphic-Regular": { + "48": [0, 0.43056, 0, 0], + "49": [0, 0.43056, 0, 0], + "50": [0, 0.43056, 0, 0], + "51": [0.19444, 0.43056, 0, 0], + "52": [0.19444, 0.43056, 0, 0], + "53": [0.19444, 0.43056, 0, 0], + "54": [0, 0.64444, 0, 0], + "55": [0.19444, 0.43056, 0, 0], + "56": [0, 0.64444, 0, 0], + "57": [0.19444, 0.43056, 0, 0], + "65": [0, 0.68333, 0, 0.19445], + "66": [0, 0.68333, 0.03041, 0.13889], + "67": [0, 0.68333, 0.05834, 0.13889], + "68": [0, 0.68333, 0.02778, 0.08334], + "69": [0, 0.68333, 0.08944, 0.11111], + "70": [0, 0.68333, 0.09931, 0.11111], + "71": [0.09722, 0.68333, 0.0593, 0.11111], + "72": [0, 0.68333, 0.00965, 0.11111], + "73": [0, 0.68333, 0.07382, 0], + "74": [0.09722, 0.68333, 0.18472, 0.16667], + "75": [0, 0.68333, 0.01445, 0.05556], + "76": [0, 0.68333, 0, 0.13889], + "77": [0, 0.68333, 0, 0.13889], + "78": [0, 0.68333, 0.14736, 0.08334], + "79": [0, 0.68333, 0.02778, 0.11111], + "80": [0, 0.68333, 0.08222, 0.08334], + "81": [0.09722, 0.68333, 0, 0.11111], + "82": [0, 0.68333, 0, 0.08334], + "83": [0, 0.68333, 0.075, 0.13889], + "84": [0, 0.68333, 0.25417, 0], + "85": [0, 0.68333, 0.09931, 0.08334], + "86": [0, 0.68333, 0.08222, 0], + "87": [0, 0.68333, 0.08222, 0.08334], + "88": [0, 0.68333, 0.14643, 0.13889], + "89": [0.09722, 0.68333, 0.08222, 0.08334], + "90": [0, 0.68333, 0.07944, 0.13889], + }, + "Fraktur-Regular": { + "33": [0, 0.69141, 0, 0], + "34": [0, 0.69141, 0, 0], + "38": [0, 0.69141, 0, 0], + "39": [0, 0.69141, 0, 0], + "40": [0.24982, 0.74947, 0, 0], + "41": [0.24982, 0.74947, 0, 0], + "42": [0, 0.62119, 0, 0], + "43": [0.08319, 0.58283, 0, 0], + "44": [0, 0.10803, 0, 0], + "45": [0.08319, 0.58283, 0, 0], + "46": [0, 0.10803, 0, 0], + "47": [0.24982, 0.74947, 0, 0], + "48": [0, 0.47534, 0, 0], + "49": [0, 0.47534, 0, 0], + "50": [0, 0.47534, 0, 0], + "51": [0.18906, 0.47534, 0, 0], + "52": [0.18906, 0.47534, 0, 0], + "53": [0.18906, 0.47534, 0, 0], + "54": [0, 0.69141, 0, 0], + "55": [0.18906, 0.47534, 0, 0], + "56": [0, 0.69141, 0, 0], + "57": [0.18906, 0.47534, 0, 0], + "58": [0, 0.47534, 0, 0], + "59": [0.12604, 0.47534, 0, 0], + "61": [-0.13099, 0.36866, 0, 0], + "63": [0, 0.69141, 0, 0], + "65": [0, 0.69141, 0, 0], + "66": [0, 0.69141, 0, 0], + "67": [0, 0.69141, 0, 0], + "68": [0, 0.69141, 0, 0], + "69": [0, 0.69141, 0, 0], + "70": [0.12604, 0.69141, 0, 0], + "71": [0, 0.69141, 0, 0], + "72": [0.06302, 0.69141, 0, 0], + "73": [0, 0.69141, 0, 0], + "74": [0.12604, 0.69141, 0, 0], + "75": [0, 0.69141, 0, 0], + "76": [0, 0.69141, 0, 0], + "77": [0, 0.69141, 0, 0], + "78": [0, 0.69141, 0, 0], + "79": [0, 0.69141, 0, 0], + "80": [0.18906, 0.69141, 0, 0], + "81": [0.03781, 0.69141, 0, 0], + "82": [0, 0.69141, 0, 0], + "83": [0, 0.69141, 0, 0], + "84": [0, 0.69141, 0, 0], + "85": [0, 0.69141, 0, 0], + "86": [0, 0.69141, 0, 0], + "87": [0, 0.69141, 0, 0], + "88": [0, 0.69141, 0, 0], + "89": [0.18906, 0.69141, 0, 0], + "90": [0.12604, 0.69141, 0, 0], + "91": [0.24982, 0.74947, 0, 0], + "93": [0.24982, 0.74947, 0, 0], + "94": [0, 0.69141, 0, 0], + "97": [0, 0.47534, 0, 0], + "98": [0, 0.69141, 0, 0], + "99": [0, 0.47534, 0, 0], + "100": [0, 0.62119, 0, 0], + "101": [0, 0.47534, 0, 0], + "102": [0.18906, 0.69141, 0, 0], + "103": [0.18906, 0.47534, 0, 0], + "104": [0.18906, 0.69141, 0, 0], + "105": [0, 0.69141, 0, 0], + "106": [0, 0.69141, 0, 0], + "107": [0, 0.69141, 0, 0], + "108": [0, 0.69141, 0, 0], + "109": [0, 0.47534, 0, 0], + "110": [0, 0.47534, 0, 0], + "111": [0, 0.47534, 0, 0], + "112": [0.18906, 0.52396, 0, 0], + "113": [0.18906, 0.47534, 0, 0], + "114": [0, 0.47534, 0, 0], + "115": [0, 0.47534, 0, 0], + "116": [0, 0.62119, 0, 0], + "117": [0, 0.47534, 0, 0], + "118": [0, 0.52396, 0, 0], + "119": [0, 0.52396, 0, 0], + "120": [0.18906, 0.47534, 0, 0], + "121": [0.18906, 0.47534, 0, 0], + "122": [0.18906, 0.47534, 0, 0], + "8216": [0, 0.69141, 0, 0], + "8217": [0, 0.69141, 0, 0], + "58112": [0, 0.62119, 0, 0], + "58113": [0, 0.62119, 0, 0], + "58114": [0.18906, 0.69141, 0, 0], + "58115": [0.18906, 0.69141, 0, 0], + "58116": [0.18906, 0.47534, 0, 0], + "58117": [0, 0.69141, 0, 0], + "58118": [0, 0.62119, 0, 0], + "58119": [0, 0.47534, 0, 0], + }, + "Main-Bold": { + "33": [0, 0.69444, 0, 0], + "34": [0, 0.69444, 0, 0], + "35": [0.19444, 0.69444, 0, 0], + "36": [0.05556, 0.75, 0, 0], + "37": [0.05556, 0.75, 0, 0], + "38": [0, 0.69444, 0, 0], + "39": [0, 0.69444, 0, 0], + "40": [0.25, 0.75, 0, 0], + "41": [0.25, 0.75, 0, 0], + "42": [0, 0.75, 0, 0], + "43": [0.13333, 0.63333, 0, 0], + "44": [0.19444, 0.15556, 0, 0], + "45": [0, 0.44444, 0, 0], + "46": [0, 0.15556, 0, 0], + "47": [0.25, 0.75, 0, 0], + "48": [0, 0.64444, 0, 0], + "49": [0, 0.64444, 0, 0], + "50": [0, 0.64444, 0, 0], + "51": [0, 0.64444, 0, 0], + "52": [0, 0.64444, 0, 0], + "53": [0, 0.64444, 0, 0], + "54": [0, 0.64444, 0, 0], + "55": [0, 0.64444, 0, 0], + "56": [0, 0.64444, 0, 0], + "57": [0, 0.64444, 0, 0], + "58": [0, 0.44444, 0, 0], + "59": [0.19444, 0.44444, 0, 0], + "60": [0.08556, 0.58556, 0, 0], + "61": [-0.10889, 0.39111, 0, 0], + "62": [0.08556, 0.58556, 0, 0], + "63": [0, 0.69444, 0, 0], + "64": [0, 0.69444, 0, 0], + "65": [0, 0.68611, 0, 0], + "66": [0, 0.68611, 0, 0], + "67": [0, 0.68611, 0, 0], + "68": [0, 0.68611, 0, 0], + "69": [0, 0.68611, 0, 0], + "70": [0, 0.68611, 0, 0], + "71": [0, 0.68611, 0, 0], + "72": [0, 0.68611, 0, 0], + "73": [0, 0.68611, 0, 0], + "74": [0, 0.68611, 0, 0], + "75": [0, 0.68611, 0, 0], + "76": [0, 0.68611, 0, 0], + "77": [0, 0.68611, 0, 0], + "78": [0, 0.68611, 0, 0], + "79": [0, 0.68611, 0, 0], + "80": [0, 0.68611, 0, 0], + "81": [0.19444, 0.68611, 0, 0], + "82": [0, 0.68611, 0, 0], + "83": [0, 0.68611, 0, 0], + "84": [0, 0.68611, 0, 0], + "85": [0, 0.68611, 0, 0], + "86": [0, 0.68611, 0.01597, 0], + "87": [0, 0.68611, 0.01597, 0], + "88": [0, 0.68611, 0, 0], + "89": [0, 0.68611, 0.02875, 0], + "90": [0, 0.68611, 0, 0], + "91": [0.25, 0.75, 0, 0], + "92": [0.25, 0.75, 0, 0], + "93": [0.25, 0.75, 0, 0], + "94": [0, 0.69444, 0, 0], + "95": [0.31, 0.13444, 0.03194, 0], + "96": [0, 0.69444, 0, 0], + "97": [0, 0.44444, 0, 0], + "98": [0, 0.69444, 0, 0], + "99": [0, 0.44444, 0, 0], + "100": [0, 0.69444, 0, 0], + "101": [0, 0.44444, 0, 0], + "102": [0, 0.69444, 0.10903, 0], + "103": [0.19444, 0.44444, 0.01597, 0], + "104": [0, 0.69444, 0, 0], + "105": [0, 0.69444, 0, 0], + "106": [0.19444, 0.69444, 0, 0], + "107": [0, 0.69444, 0, 0], + "108": [0, 0.69444, 0, 0], + "109": [0, 0.44444, 0, 0], + "110": [0, 0.44444, 0, 0], + "111": [0, 0.44444, 0, 0], + "112": [0.19444, 0.44444, 0, 0], + "113": [0.19444, 0.44444, 0, 0], + "114": [0, 0.44444, 0, 0], + "115": [0, 0.44444, 0, 0], + "116": [0, 0.63492, 0, 0], + "117": [0, 0.44444, 0, 0], + "118": [0, 0.44444, 0.01597, 0], + "119": [0, 0.44444, 0.01597, 0], + "120": [0, 0.44444, 0, 0], + "121": [0.19444, 0.44444, 0.01597, 0], + "122": [0, 0.44444, 0, 0], + "123": [0.25, 0.75, 0, 0], + "124": [0.25, 0.75, 0, 0], + "125": [0.25, 0.75, 0, 0], + "126": [0.35, 0.34444, 0, 0], + "168": [0, 0.69444, 0, 0], + "172": [0, 0.44444, 0, 0], + "175": [0, 0.59611, 0, 0], + "176": [0, 0.69444, 0, 0], + "177": [0.13333, 0.63333, 0, 0], + "180": [0, 0.69444, 0, 0], + "215": [0.13333, 0.63333, 0, 0], + "247": [0.13333, 0.63333, 0, 0], + "305": [0, 0.44444, 0, 0], + "567": [0.19444, 0.44444, 0, 0], + "710": [0, 0.69444, 0, 0], + "711": [0, 0.63194, 0, 0], + "713": [0, 0.59611, 0, 0], + "714": [0, 0.69444, 0, 0], + "715": [0, 0.69444, 0, 0], + "728": [0, 0.69444, 0, 0], + "729": [0, 0.69444, 0, 0], + "730": [0, 0.69444, 0, 0], + "732": [0, 0.69444, 0, 0], + "768": [0, 0.69444, 0, 0], + "769": [0, 0.69444, 0, 0], + "770": [0, 0.69444, 0, 0], + "771": [0, 0.69444, 0, 0], + "772": [0, 0.59611, 0, 0], + "774": [0, 0.69444, 0, 0], + "775": [0, 0.69444, 0, 0], + "776": [0, 0.69444, 0, 0], + "778": [0, 0.69444, 0, 0], + "779": [0, 0.69444, 0, 0], + "780": [0, 0.63194, 0, 0], + "824": [0.19444, 0.69444, 0, 0], + "915": [0, 0.68611, 0, 0], + "916": [0, 0.68611, 0, 0], + "920": [0, 0.68611, 0, 0], + "923": [0, 0.68611, 0, 0], + "926": [0, 0.68611, 0, 0], + "928": [0, 0.68611, 0, 0], + "931": [0, 0.68611, 0, 0], + "933": [0, 0.68611, 0, 0], + "934": [0, 0.68611, 0, 0], + "936": [0, 0.68611, 0, 0], + "937": [0, 0.68611, 0, 0], + "8211": [0, 0.44444, 0.03194, 0], + "8212": [0, 0.44444, 0.03194, 0], + "8216": [0, 0.69444, 0, 0], + "8217": [0, 0.69444, 0, 0], + "8220": [0, 0.69444, 0, 0], + "8221": [0, 0.69444, 0, 0], + "8224": [0.19444, 0.69444, 0, 0], + "8225": [0.19444, 0.69444, 0, 0], + "8242": [0, 0.55556, 0, 0], + "8407": [0, 0.72444, 0.15486, 0], + "8463": [0, 0.69444, 0, 0], + "8465": [0, 0.69444, 0, 0], + "8467": [0, 0.69444, 0, 0], + "8472": [0.19444, 0.44444, 0, 0], + "8476": [0, 0.69444, 0, 0], + "8501": [0, 0.69444, 0, 0], + "8592": [-0.10889, 0.39111, 0, 0], + "8593": [0.19444, 0.69444, 0, 0], + "8594": [-0.10889, 0.39111, 0, 0], + "8595": [0.19444, 0.69444, 0, 0], + "8596": [-0.10889, 0.39111, 0, 0], + "8597": [0.25, 0.75, 0, 0], + "8598": [0.19444, 0.69444, 0, 0], + "8599": [0.19444, 0.69444, 0, 0], + "8600": [0.19444, 0.69444, 0, 0], + "8601": [0.19444, 0.69444, 0, 0], + "8636": [-0.10889, 0.39111, 0, 0], + "8637": [-0.10889, 0.39111, 0, 0], + "8640": [-0.10889, 0.39111, 0, 0], + "8641": [-0.10889, 0.39111, 0, 0], + "8656": [-0.10889, 0.39111, 0, 0], + "8657": [0.19444, 0.69444, 0, 0], + "8658": [-0.10889, 0.39111, 0, 0], + "8659": [0.19444, 0.69444, 0, 0], + "8660": [-0.10889, 0.39111, 0, 0], + "8661": [0.25, 0.75, 0, 0], + "8704": [0, 0.69444, 0, 0], + "8706": [0, 0.69444, 0.06389, 0], + "8707": [0, 0.69444, 0, 0], + "8709": [0.05556, 0.75, 0, 0], + "8711": [0, 0.68611, 0, 0], + "8712": [0.08556, 0.58556, 0, 0], + "8715": [0.08556, 0.58556, 0, 0], + "8722": [0.13333, 0.63333, 0, 0], + "8723": [0.13333, 0.63333, 0, 0], + "8725": [0.25, 0.75, 0, 0], + "8726": [0.25, 0.75, 0, 0], + "8727": [-0.02778, 0.47222, 0, 0], + "8728": [-0.02639, 0.47361, 0, 0], + "8729": [-0.02639, 0.47361, 0, 0], + "8730": [0.18, 0.82, 0, 0], + "8733": [0, 0.44444, 0, 0], + "8734": [0, 0.44444, 0, 0], + "8736": [0, 0.69224, 0, 0], + "8739": [0.25, 0.75, 0, 0], + "8741": [0.25, 0.75, 0, 0], + "8743": [0, 0.55556, 0, 0], + "8744": [0, 0.55556, 0, 0], + "8745": [0, 0.55556, 0, 0], + "8746": [0, 0.55556, 0, 0], + "8747": [0.19444, 0.69444, 0.12778, 0], + "8764": [-0.10889, 0.39111, 0, 0], + "8768": [0.19444, 0.69444, 0, 0], + "8771": [0.00222, 0.50222, 0, 0], + "8776": [0.02444, 0.52444, 0, 0], + "8781": [0.00222, 0.50222, 0, 0], + "8801": [0.00222, 0.50222, 0, 0], + "8804": [0.19667, 0.69667, 0, 0], + "8805": [0.19667, 0.69667, 0, 0], + "8810": [0.08556, 0.58556, 0, 0], + "8811": [0.08556, 0.58556, 0, 0], + "8826": [0.08556, 0.58556, 0, 0], + "8827": [0.08556, 0.58556, 0, 0], + "8834": [0.08556, 0.58556, 0, 0], + "8835": [0.08556, 0.58556, 0, 0], + "8838": [0.19667, 0.69667, 0, 0], + "8839": [0.19667, 0.69667, 0, 0], + "8846": [0, 0.55556, 0, 0], + "8849": [0.19667, 0.69667, 0, 0], + "8850": [0.19667, 0.69667, 0, 0], + "8851": [0, 0.55556, 0, 0], + "8852": [0, 0.55556, 0, 0], + "8853": [0.13333, 0.63333, 0, 0], + "8854": [0.13333, 0.63333, 0, 0], + "8855": [0.13333, 0.63333, 0, 0], + "8856": [0.13333, 0.63333, 0, 0], + "8857": [0.13333, 0.63333, 0, 0], + "8866": [0, 0.69444, 0, 0], + "8867": [0, 0.69444, 0, 0], + "8868": [0, 0.69444, 0, 0], + "8869": [0, 0.69444, 0, 0], + "8900": [-0.02639, 0.47361, 0, 0], + "8901": [-0.02639, 0.47361, 0, 0], + "8902": [-0.02778, 0.47222, 0, 0], + "8968": [0.25, 0.75, 0, 0], + "8969": [0.25, 0.75, 0, 0], + "8970": [0.25, 0.75, 0, 0], + "8971": [0.25, 0.75, 0, 0], + "8994": [-0.13889, 0.36111, 0, 0], + "8995": [-0.13889, 0.36111, 0, 0], + "9651": [0.19444, 0.69444, 0, 0], + "9657": [-0.02778, 0.47222, 0, 0], + "9661": [0.19444, 0.69444, 0, 0], + "9667": [-0.02778, 0.47222, 0, 0], + "9711": [0.19444, 0.69444, 0, 0], + "9824": [0.12963, 0.69444, 0, 0], + "9825": [0.12963, 0.69444, 0, 0], + "9826": [0.12963, 0.69444, 0, 0], + "9827": [0.12963, 0.69444, 0, 0], + "9837": [0, 0.75, 0, 0], + "9838": [0.19444, 0.69444, 0, 0], + "9839": [0.19444, 0.69444, 0, 0], + "10216": [0.25, 0.75, 0, 0], + "10217": [0.25, 0.75, 0, 0], + "10815": [0, 0.68611, 0, 0], + "10927": [0.19667, 0.69667, 0, 0], + "10928": [0.19667, 0.69667, 0, 0], + }, + "Main-Italic": { + "33": [0, 0.69444, 0.12417, 0], + "34": [0, 0.69444, 0.06961, 0], + "35": [0.19444, 0.69444, 0.06616, 0], + "37": [0.05556, 0.75, 0.13639, 0], + "38": [0, 0.69444, 0.09694, 0], + "39": [0, 0.69444, 0.12417, 0], + "40": [0.25, 0.75, 0.16194, 0], + "41": [0.25, 0.75, 0.03694, 0], + "42": [0, 0.75, 0.14917, 0], + "43": [0.05667, 0.56167, 0.03694, 0], + "44": [0.19444, 0.10556, 0, 0], + "45": [0, 0.43056, 0.02826, 0], + "46": [0, 0.10556, 0, 0], + "47": [0.25, 0.75, 0.16194, 0], + "48": [0, 0.64444, 0.13556, 0], + "49": [0, 0.64444, 0.13556, 0], + "50": [0, 0.64444, 0.13556, 0], + "51": [0, 0.64444, 0.13556, 0], + "52": [0.19444, 0.64444, 0.13556, 0], + "53": [0, 0.64444, 0.13556, 0], + "54": [0, 0.64444, 0.13556, 0], + "55": [0.19444, 0.64444, 0.13556, 0], + "56": [0, 0.64444, 0.13556, 0], + "57": [0, 0.64444, 0.13556, 0], + "58": [0, 0.43056, 0.0582, 0], + "59": [0.19444, 0.43056, 0.0582, 0], + "61": [-0.13313, 0.36687, 0.06616, 0], + "63": [0, 0.69444, 0.1225, 0], + "64": [0, 0.69444, 0.09597, 0], + "65": [0, 0.68333, 0, 0], + "66": [0, 0.68333, 0.10257, 0], + "67": [0, 0.68333, 0.14528, 0], + "68": [0, 0.68333, 0.09403, 0], + "69": [0, 0.68333, 0.12028, 0], + "70": [0, 0.68333, 0.13305, 0], + "71": [0, 0.68333, 0.08722, 0], + "72": [0, 0.68333, 0.16389, 0], + "73": [0, 0.68333, 0.15806, 0], + "74": [0, 0.68333, 0.14028, 0], + "75": [0, 0.68333, 0.14528, 0], + "76": [0, 0.68333, 0, 0], + "77": [0, 0.68333, 0.16389, 0], + "78": [0, 0.68333, 0.16389, 0], + "79": [0, 0.68333, 0.09403, 0], + "80": [0, 0.68333, 0.10257, 0], + "81": [0.19444, 0.68333, 0.09403, 0], + "82": [0, 0.68333, 0.03868, 0], + "83": [0, 0.68333, 0.11972, 0], + "84": [0, 0.68333, 0.13305, 0], + "85": [0, 0.68333, 0.16389, 0], + "86": [0, 0.68333, 0.18361, 0], + "87": [0, 0.68333, 0.18361, 0], + "88": [0, 0.68333, 0.15806, 0], + "89": [0, 0.68333, 0.19383, 0], + "90": [0, 0.68333, 0.14528, 0], + "91": [0.25, 0.75, 0.1875, 0], + "93": [0.25, 0.75, 0.10528, 0], + "94": [0, 0.69444, 0.06646, 0], + "95": [0.31, 0.12056, 0.09208, 0], + "97": [0, 0.43056, 0.07671, 0], + "98": [0, 0.69444, 0.06312, 0], + "99": [0, 0.43056, 0.05653, 0], + "100": [0, 0.69444, 0.10333, 0], + "101": [0, 0.43056, 0.07514, 0], + "102": [0.19444, 0.69444, 0.21194, 0], + "103": [0.19444, 0.43056, 0.08847, 0], + "104": [0, 0.69444, 0.07671, 0], + "105": [0, 0.65536, 0.1019, 0], + "106": [0.19444, 0.65536, 0.14467, 0], + "107": [0, 0.69444, 0.10764, 0], + "108": [0, 0.69444, 0.10333, 0], + "109": [0, 0.43056, 0.07671, 0], + "110": [0, 0.43056, 0.07671, 0], + "111": [0, 0.43056, 0.06312, 0], + "112": [0.19444, 0.43056, 0.06312, 0], + "113": [0.19444, 0.43056, 0.08847, 0], + "114": [0, 0.43056, 0.10764, 0], + "115": [0, 0.43056, 0.08208, 0], + "116": [0, 0.61508, 0.09486, 0], + "117": [0, 0.43056, 0.07671, 0], + "118": [0, 0.43056, 0.10764, 0], + "119": [0, 0.43056, 0.10764, 0], + "120": [0, 0.43056, 0.12042, 0], + "121": [0.19444, 0.43056, 0.08847, 0], + "122": [0, 0.43056, 0.12292, 0], + "126": [0.35, 0.31786, 0.11585, 0], + "163": [0, 0.69444, 0, 0], + "305": [0, 0.43056, 0, 0.02778], + "567": [0.19444, 0.43056, 0, 0.08334], + "768": [0, 0.69444, 0, 0], + "769": [0, 0.69444, 0.09694, 0], + "770": [0, 0.69444, 0.06646, 0], + "771": [0, 0.66786, 0.11585, 0], + "772": [0, 0.56167, 0.10333, 0], + "774": [0, 0.69444, 0.10806, 0], + "775": [0, 0.66786, 0.11752, 0], + "776": [0, 0.66786, 0.10474, 0], + "778": [0, 0.69444, 0, 0], + "779": [0, 0.69444, 0.1225, 0], + "780": [0, 0.62847, 0.08295, 0], + "915": [0, 0.68333, 0.13305, 0], + "916": [0, 0.68333, 0, 0], + "920": [0, 0.68333, 0.09403, 0], + "923": [0, 0.68333, 0, 0], + "926": [0, 0.68333, 0.15294, 0], + "928": [0, 0.68333, 0.16389, 0], + "931": [0, 0.68333, 0.12028, 0], + "933": [0, 0.68333, 0.11111, 0], + "934": [0, 0.68333, 0.05986, 0], + "936": [0, 0.68333, 0.11111, 0], + "937": [0, 0.68333, 0.10257, 0], + "8211": [0, 0.43056, 0.09208, 0], + "8212": [0, 0.43056, 0.09208, 0], + "8216": [0, 0.69444, 0.12417, 0], + "8217": [0, 0.69444, 0.12417, 0], + "8220": [0, 0.69444, 0.1685, 0], + "8221": [0, 0.69444, 0.06961, 0], + "8463": [0, 0.68889, 0, 0], + }, + "Main-Regular": { + "32": [0, 0, 0, 0], + "33": [0, 0.69444, 0, 0], + "34": [0, 0.69444, 0, 0], + "35": [0.19444, 0.69444, 0, 0], + "36": [0.05556, 0.75, 0, 0], + "37": [0.05556, 0.75, 0, 0], + "38": [0, 0.69444, 0, 0], + "39": [0, 0.69444, 0, 0], + "40": [0.25, 0.75, 0, 0], + "41": [0.25, 0.75, 0, 0], + "42": [0, 0.75, 0, 0], + "43": [0.08333, 0.58333, 0, 0], + "44": [0.19444, 0.10556, 0, 0], + "45": [0, 0.43056, 0, 0], + "46": [0, 0.10556, 0, 0], + "47": [0.25, 0.75, 0, 0], + "48": [0, 0.64444, 0, 0], + "49": [0, 0.64444, 0, 0], + "50": [0, 0.64444, 0, 0], + "51": [0, 0.64444, 0, 0], + "52": [0, 0.64444, 0, 0], + "53": [0, 0.64444, 0, 0], + "54": [0, 0.64444, 0, 0], + "55": [0, 0.64444, 0, 0], + "56": [0, 0.64444, 0, 0], + "57": [0, 0.64444, 0, 0], + "58": [0, 0.43056, 0, 0], + "59": [0.19444, 0.43056, 0, 0], + "60": [0.0391, 0.5391, 0, 0], + "61": [-0.13313, 0.36687, 0, 0], + "62": [0.0391, 0.5391, 0, 0], + "63": [0, 0.69444, 0, 0], + "64": [0, 0.69444, 0, 0], + "65": [0, 0.68333, 0, 0], + "66": [0, 0.68333, 0, 0], + "67": [0, 0.68333, 0, 0], + "68": [0, 0.68333, 0, 0], + "69": [0, 0.68333, 0, 0], + "70": [0, 0.68333, 0, 0], + "71": [0, 0.68333, 0, 0], + "72": [0, 0.68333, 0, 0], + "73": [0, 0.68333, 0, 0], + "74": [0, 0.68333, 0, 0], + "75": [0, 0.68333, 0, 0], + "76": [0, 0.68333, 0, 0], + "77": [0, 0.68333, 0, 0], + "78": [0, 0.68333, 0, 0], + "79": [0, 0.68333, 0, 0], + "80": [0, 0.68333, 0, 0], + "81": [0.19444, 0.68333, 0, 0], + "82": [0, 0.68333, 0, 0], + "83": [0, 0.68333, 0, 0], + "84": [0, 0.68333, 0, 0], + "85": [0, 0.68333, 0, 0], + "86": [0, 0.68333, 0.01389, 0], + "87": [0, 0.68333, 0.01389, 0], + "88": [0, 0.68333, 0, 0], + "89": [0, 0.68333, 0.025, 0], + "90": [0, 0.68333, 0, 0], + "91": [0.25, 0.75, 0, 0], + "92": [0.25, 0.75, 0, 0], + "93": [0.25, 0.75, 0, 0], + "94": [0, 0.69444, 0, 0], + "95": [0.31, 0.12056, 0.02778, 0], + "96": [0, 0.69444, 0, 0], + "97": [0, 0.43056, 0, 0], + "98": [0, 0.69444, 0, 0], + "99": [0, 0.43056, 0, 0], + "100": [0, 0.69444, 0, 0], + "101": [0, 0.43056, 0, 0], + "102": [0, 0.69444, 0.07778, 0], + "103": [0.19444, 0.43056, 0.01389, 0], + "104": [0, 0.69444, 0, 0], + "105": [0, 0.66786, 0, 0], + "106": [0.19444, 0.66786, 0, 0], + "107": [0, 0.69444, 0, 0], + "108": [0, 0.69444, 0, 0], + "109": [0, 0.43056, 0, 0], + "110": [0, 0.43056, 0, 0], + "111": [0, 0.43056, 0, 0], + "112": [0.19444, 0.43056, 0, 0], + "113": [0.19444, 0.43056, 0, 0], + "114": [0, 0.43056, 0, 0], + "115": [0, 0.43056, 0, 0], + "116": [0, 0.61508, 0, 0], + "117": [0, 0.43056, 0, 0], + "118": [0, 0.43056, 0.01389, 0], + "119": [0, 0.43056, 0.01389, 0], + "120": [0, 0.43056, 0, 0], + "121": [0.19444, 0.43056, 0.01389, 0], + "122": [0, 0.43056, 0, 0], + "123": [0.25, 0.75, 0, 0], + "124": [0.25, 0.75, 0, 0], + "125": [0.25, 0.75, 0, 0], + "126": [0.35, 0.31786, 0, 0], + "160": [0, 0, 0, 0], + "168": [0, 0.66786, 0, 0], + "172": [0, 0.43056, 0, 0], + "175": [0, 0.56778, 0, 0], + "176": [0, 0.69444, 0, 0], + "177": [0.08333, 0.58333, 0, 0], + "180": [0, 0.69444, 0, 0], + "215": [0.08333, 0.58333, 0, 0], + "247": [0.08333, 0.58333, 0, 0], + "305": [0, 0.43056, 0, 0], + "567": [0.19444, 0.43056, 0, 0], + "710": [0, 0.69444, 0, 0], + "711": [0, 0.62847, 0, 0], + "713": [0, 0.56778, 0, 0], + "714": [0, 0.69444, 0, 0], + "715": [0, 0.69444, 0, 0], + "728": [0, 0.69444, 0, 0], + "729": [0, 0.66786, 0, 0], + "730": [0, 0.69444, 0, 0], + "732": [0, 0.66786, 0, 0], + "768": [0, 0.69444, 0, 0], + "769": [0, 0.69444, 0, 0], + "770": [0, 0.69444, 0, 0], + "771": [0, 0.66786, 0, 0], + "772": [0, 0.56778, 0, 0], + "774": [0, 0.69444, 0, 0], + "775": [0, 0.66786, 0, 0], + "776": [0, 0.66786, 0, 0], + "778": [0, 0.69444, 0, 0], + "779": [0, 0.69444, 0, 0], + "780": [0, 0.62847, 0, 0], + "824": [0.19444, 0.69444, 0, 0], + "915": [0, 0.68333, 0, 0], + "916": [0, 0.68333, 0, 0], + "920": [0, 0.68333, 0, 0], + "923": [0, 0.68333, 0, 0], + "926": [0, 0.68333, 0, 0], + "928": [0, 0.68333, 0, 0], + "931": [0, 0.68333, 0, 0], + "933": [0, 0.68333, 0, 0], + "934": [0, 0.68333, 0, 0], + "936": [0, 0.68333, 0, 0], + "937": [0, 0.68333, 0, 0], + "8211": [0, 0.43056, 0.02778, 0], + "8212": [0, 0.43056, 0.02778, 0], + "8216": [0, 0.69444, 0, 0], + "8217": [0, 0.69444, 0, 0], + "8220": [0, 0.69444, 0, 0], + "8221": [0, 0.69444, 0, 0], + "8224": [0.19444, 0.69444, 0, 0], + "8225": [0.19444, 0.69444, 0, 0], + "8230": [0, 0.12, 0, 0], + "8242": [0, 0.55556, 0, 0], + "8407": [0, 0.71444, 0.15382, 0], + "8463": [0, 0.68889, 0, 0], + "8465": [0, 0.69444, 0, 0], + "8467": [0, 0.69444, 0, 0.11111], + "8472": [0.19444, 0.43056, 0, 0.11111], + "8476": [0, 0.69444, 0, 0], + "8501": [0, 0.69444, 0, 0], + "8592": [-0.13313, 0.36687, 0, 0], + "8593": [0.19444, 0.69444, 0, 0], + "8594": [-0.13313, 0.36687, 0, 0], + "8595": [0.19444, 0.69444, 0, 0], + "8596": [-0.13313, 0.36687, 0, 0], + "8597": [0.25, 0.75, 0, 0], + "8598": [0.19444, 0.69444, 0, 0], + "8599": [0.19444, 0.69444, 0, 0], + "8600": [0.19444, 0.69444, 0, 0], + "8601": [0.19444, 0.69444, 0, 0], + "8614": [0.011, 0.511, 0, 0], + "8617": [0.011, 0.511, 0, 0], + "8618": [0.011, 0.511, 0, 0], + "8636": [-0.13313, 0.36687, 0, 0], + "8637": [-0.13313, 0.36687, 0, 0], + "8640": [-0.13313, 0.36687, 0, 0], + "8641": [-0.13313, 0.36687, 0, 0], + "8652": [0.011, 0.671, 0, 0], + "8656": [-0.13313, 0.36687, 0, 0], + "8657": [0.19444, 0.69444, 0, 0], + "8658": [-0.13313, 0.36687, 0, 0], + "8659": [0.19444, 0.69444, 0, 0], + "8660": [-0.13313, 0.36687, 0, 0], + "8661": [0.25, 0.75, 0, 0], + "8704": [0, 0.69444, 0, 0], + "8706": [0, 0.69444, 0.05556, 0.08334], + "8707": [0, 0.69444, 0, 0], + "8709": [0.05556, 0.75, 0, 0], + "8711": [0, 0.68333, 0, 0], + "8712": [0.0391, 0.5391, 0, 0], + "8715": [0.0391, 0.5391, 0, 0], + "8722": [0.08333, 0.58333, 0, 0], + "8723": [0.08333, 0.58333, 0, 0], + "8725": [0.25, 0.75, 0, 0], + "8726": [0.25, 0.75, 0, 0], + "8727": [-0.03472, 0.46528, 0, 0], + "8728": [-0.05555, 0.44445, 0, 0], + "8729": [-0.05555, 0.44445, 0, 0], + "8730": [0.2, 0.8, 0, 0], + "8733": [0, 0.43056, 0, 0], + "8734": [0, 0.43056, 0, 0], + "8736": [0, 0.69224, 0, 0], + "8739": [0.25, 0.75, 0, 0], + "8741": [0.25, 0.75, 0, 0], + "8743": [0, 0.55556, 0, 0], + "8744": [0, 0.55556, 0, 0], + "8745": [0, 0.55556, 0, 0], + "8746": [0, 0.55556, 0, 0], + "8747": [0.19444, 0.69444, 0.11111, 0], + "8764": [-0.13313, 0.36687, 0, 0], + "8768": [0.19444, 0.69444, 0, 0], + "8771": [-0.03625, 0.46375, 0, 0], + "8773": [-0.022, 0.589, 0, 0], + "8776": [-0.01688, 0.48312, 0, 0], + "8781": [-0.03625, 0.46375, 0, 0], + "8784": [-0.133, 0.67, 0, 0], + "8800": [0.215, 0.716, 0, 0], + "8801": [-0.03625, 0.46375, 0, 0], + "8804": [0.13597, 0.63597, 0, 0], + "8805": [0.13597, 0.63597, 0, 0], + "8810": [0.0391, 0.5391, 0, 0], + "8811": [0.0391, 0.5391, 0, 0], + "8826": [0.0391, 0.5391, 0, 0], + "8827": [0.0391, 0.5391, 0, 0], + "8834": [0.0391, 0.5391, 0, 0], + "8835": [0.0391, 0.5391, 0, 0], + "8838": [0.13597, 0.63597, 0, 0], + "8839": [0.13597, 0.63597, 0, 0], + "8846": [0, 0.55556, 0, 0], + "8849": [0.13597, 0.63597, 0, 0], + "8850": [0.13597, 0.63597, 0, 0], + "8851": [0, 0.55556, 0, 0], + "8852": [0, 0.55556, 0, 0], + "8853": [0.08333, 0.58333, 0, 0], + "8854": [0.08333, 0.58333, 0, 0], + "8855": [0.08333, 0.58333, 0, 0], + "8856": [0.08333, 0.58333, 0, 0], + "8857": [0.08333, 0.58333, 0, 0], + "8866": [0, 0.69444, 0, 0], + "8867": [0, 0.69444, 0, 0], + "8868": [0, 0.69444, 0, 0], + "8869": [0, 0.69444, 0, 0], + "8872": [0.249, 0.75, 0, 0], + "8900": [-0.05555, 0.44445, 0, 0], + "8901": [-0.05555, 0.44445, 0, 0], + "8902": [-0.03472, 0.46528, 0, 0], + "8904": [0.005, 0.505, 0, 0], + "8942": [0.03, 0.9, 0, 0], + "8943": [-0.19, 0.31, 0, 0], + "8945": [-0.1, 0.82, 0, 0], + "8968": [0.25, 0.75, 0, 0], + "8969": [0.25, 0.75, 0, 0], + "8970": [0.25, 0.75, 0, 0], + "8971": [0.25, 0.75, 0, 0], + "8994": [-0.14236, 0.35764, 0, 0], + "8995": [-0.14236, 0.35764, 0, 0], + "9136": [0.244, 0.744, 0, 0], + "9137": [0.244, 0.744, 0, 0], + "9651": [0.19444, 0.69444, 0, 0], + "9657": [-0.03472, 0.46528, 0, 0], + "9661": [0.19444, 0.69444, 0, 0], + "9667": [-0.03472, 0.46528, 0, 0], + "9711": [0.19444, 0.69444, 0, 0], + "9824": [0.12963, 0.69444, 0, 0], + "9825": [0.12963, 0.69444, 0, 0], + "9826": [0.12963, 0.69444, 0, 0], + "9827": [0.12963, 0.69444, 0, 0], + "9837": [0, 0.75, 0, 0], + "9838": [0.19444, 0.69444, 0, 0], + "9839": [0.19444, 0.69444, 0, 0], + "10216": [0.25, 0.75, 0, 0], + "10217": [0.25, 0.75, 0, 0], + "10222": [0.244, 0.744, 0, 0], + "10223": [0.244, 0.744, 0, 0], + "10229": [0.011, 0.511, 0, 0], + "10230": [0.011, 0.511, 0, 0], + "10231": [0.011, 0.511, 0, 0], + "10232": [0.024, 0.525, 0, 0], + "10233": [0.024, 0.525, 0, 0], + "10234": [0.024, 0.525, 0, 0], + "10236": [0.011, 0.511, 0, 0], + "10815": [0, 0.68333, 0, 0], + "10927": [0.13597, 0.63597, 0, 0], + "10928": [0.13597, 0.63597, 0, 0], + }, + "Math-BoldItalic": { + "47": [0.19444, 0.69444, 0, 0], + "65": [0, 0.68611, 0, 0], + "66": [0, 0.68611, 0.04835, 0], + "67": [0, 0.68611, 0.06979, 0], + "68": [0, 0.68611, 0.03194, 0], + "69": [0, 0.68611, 0.05451, 0], + "70": [0, 0.68611, 0.15972, 0], + "71": [0, 0.68611, 0, 0], + "72": [0, 0.68611, 0.08229, 0], + "73": [0, 0.68611, 0.07778, 0], + "74": [0, 0.68611, 0.10069, 0], + "75": [0, 0.68611, 0.06979, 0], + "76": [0, 0.68611, 0, 0], + "77": [0, 0.68611, 0.11424, 0], + "78": [0, 0.68611, 0.11424, 0], + "79": [0, 0.68611, 0.03194, 0], + "80": [0, 0.68611, 0.15972, 0], + "81": [0.19444, 0.68611, 0, 0], + "82": [0, 0.68611, 0.00421, 0], + "83": [0, 0.68611, 0.05382, 0], + "84": [0, 0.68611, 0.15972, 0], + "85": [0, 0.68611, 0.11424, 0], + "86": [0, 0.68611, 0.25555, 0], + "87": [0, 0.68611, 0.15972, 0], + "88": [0, 0.68611, 0.07778, 0], + "89": [0, 0.68611, 0.25555, 0], + "90": [0, 0.68611, 0.06979, 0], + "97": [0, 0.44444, 0, 0], + "98": [0, 0.69444, 0, 0], + "99": [0, 0.44444, 0, 0], + "100": [0, 0.69444, 0, 0], + "101": [0, 0.44444, 0, 0], + "102": [0.19444, 0.69444, 0.11042, 0], + "103": [0.19444, 0.44444, 0.03704, 0], + "104": [0, 0.69444, 0, 0], + "105": [0, 0.69326, 0, 0], + "106": [0.19444, 0.69326, 0.0622, 0], + "107": [0, 0.69444, 0.01852, 0], + "108": [0, 0.69444, 0.0088, 0], + "109": [0, 0.44444, 0, 0], + "110": [0, 0.44444, 0, 0], + "111": [0, 0.44444, 0, 0], + "112": [0.19444, 0.44444, 0, 0], + "113": [0.19444, 0.44444, 0.03704, 0], + "114": [0, 0.44444, 0.03194, 0], + "115": [0, 0.44444, 0, 0], + "116": [0, 0.63492, 0, 0], + "117": [0, 0.44444, 0, 0], + "118": [0, 0.44444, 0.03704, 0], + "119": [0, 0.44444, 0.02778, 0], + "120": [0, 0.44444, 0, 0], + "121": [0.19444, 0.44444, 0.03704, 0], + "122": [0, 0.44444, 0.04213, 0], + "915": [0, 0.68611, 0.15972, 0], + "916": [0, 0.68611, 0, 0], + "920": [0, 0.68611, 0.03194, 0], + "923": [0, 0.68611, 0, 0], + "926": [0, 0.68611, 0.07458, 0], + "928": [0, 0.68611, 0.08229, 0], + "931": [0, 0.68611, 0.05451, 0], + "933": [0, 0.68611, 0.15972, 0], + "934": [0, 0.68611, 0, 0], + "936": [0, 0.68611, 0.11653, 0], + "937": [0, 0.68611, 0.04835, 0], + "945": [0, 0.44444, 0, 0], + "946": [0.19444, 0.69444, 0.03403, 0], + "947": [0.19444, 0.44444, 0.06389, 0], + "948": [0, 0.69444, 0.03819, 0], + "949": [0, 0.44444, 0, 0], + "950": [0.19444, 0.69444, 0.06215, 0], + "951": [0.19444, 0.44444, 0.03704, 0], + "952": [0, 0.69444, 0.03194, 0], + "953": [0, 0.44444, 0, 0], + "954": [0, 0.44444, 0, 0], + "955": [0, 0.69444, 0, 0], + "956": [0.19444, 0.44444, 0, 0], + "957": [0, 0.44444, 0.06898, 0], + "958": [0.19444, 0.69444, 0.03021, 0], + "959": [0, 0.44444, 0, 0], + "960": [0, 0.44444, 0.03704, 0], + "961": [0.19444, 0.44444, 0, 0], + "962": [0.09722, 0.44444, 0.07917, 0], + "963": [0, 0.44444, 0.03704, 0], + "964": [0, 0.44444, 0.13472, 0], + "965": [0, 0.44444, 0.03704, 0], + "966": [0.19444, 0.44444, 0, 0], + "967": [0.19444, 0.44444, 0, 0], + "968": [0.19444, 0.69444, 0.03704, 0], + "969": [0, 0.44444, 0.03704, 0], + "977": [0, 0.69444, 0, 0], + "981": [0.19444, 0.69444, 0, 0], + "982": [0, 0.44444, 0.03194, 0], + "1009": [0.19444, 0.44444, 0, 0], + "1013": [0, 0.44444, 0, 0], + }, + "Math-Italic": { + "47": [0.19444, 0.69444, 0, 0], + "65": [0, 0.68333, 0, 0.13889], + "66": [0, 0.68333, 0.05017, 0.08334], + "67": [0, 0.68333, 0.07153, 0.08334], + "68": [0, 0.68333, 0.02778, 0.05556], + "69": [0, 0.68333, 0.05764, 0.08334], + "70": [0, 0.68333, 0.13889, 0.08334], + "71": [0, 0.68333, 0, 0.08334], + "72": [0, 0.68333, 0.08125, 0.05556], + "73": [0, 0.68333, 0.07847, 0.11111], + "74": [0, 0.68333, 0.09618, 0.16667], + "75": [0, 0.68333, 0.07153, 0.05556], + "76": [0, 0.68333, 0, 0.02778], + "77": [0, 0.68333, 0.10903, 0.08334], + "78": [0, 0.68333, 0.10903, 0.08334], + "79": [0, 0.68333, 0.02778, 0.08334], + "80": [0, 0.68333, 0.13889, 0.08334], + "81": [0.19444, 0.68333, 0, 0.08334], + "82": [0, 0.68333, 0.00773, 0.08334], + "83": [0, 0.68333, 0.05764, 0.08334], + "84": [0, 0.68333, 0.13889, 0.08334], + "85": [0, 0.68333, 0.10903, 0.02778], + "86": [0, 0.68333, 0.22222, 0], + "87": [0, 0.68333, 0.13889, 0], + "88": [0, 0.68333, 0.07847, 0.08334], + "89": [0, 0.68333, 0.22222, 0], + "90": [0, 0.68333, 0.07153, 0.08334], + "97": [0, 0.43056, 0, 0], + "98": [0, 0.69444, 0, 0], + "99": [0, 0.43056, 0, 0.05556], + "100": [0, 0.69444, 0, 0.16667], + "101": [0, 0.43056, 0, 0.05556], + "102": [0.19444, 0.69444, 0.10764, 0.16667], + "103": [0.19444, 0.43056, 0.03588, 0.02778], + "104": [0, 0.69444, 0, 0], + "105": [0, 0.65952, 0, 0], + "106": [0.19444, 0.65952, 0.05724, 0], + "107": [0, 0.69444, 0.03148, 0], + "108": [0, 0.69444, 0.01968, 0.08334], + "109": [0, 0.43056, 0, 0], + "110": [0, 0.43056, 0, 0], + "111": [0, 0.43056, 0, 0.05556], + "112": [0.19444, 0.43056, 0, 0.08334], + "113": [0.19444, 0.43056, 0.03588, 0.08334], + "114": [0, 0.43056, 0.02778, 0.05556], + "115": [0, 0.43056, 0, 0.05556], + "116": [0, 0.61508, 0, 0.08334], + "117": [0, 0.43056, 0, 0.02778], + "118": [0, 0.43056, 0.03588, 0.02778], + "119": [0, 0.43056, 0.02691, 0.08334], + "120": [0, 0.43056, 0, 0.02778], + "121": [0.19444, 0.43056, 0.03588, 0.05556], + "122": [0, 0.43056, 0.04398, 0.05556], + "915": [0, 0.68333, 0.13889, 0.08334], + "916": [0, 0.68333, 0, 0.16667], + "920": [0, 0.68333, 0.02778, 0.08334], + "923": [0, 0.68333, 0, 0.16667], + "926": [0, 0.68333, 0.07569, 0.08334], + "928": [0, 0.68333, 0.08125, 0.05556], + "931": [0, 0.68333, 0.05764, 0.08334], + "933": [0, 0.68333, 0.13889, 0.05556], + "934": [0, 0.68333, 0, 0.08334], + "936": [0, 0.68333, 0.11, 0.05556], + "937": [0, 0.68333, 0.05017, 0.08334], + "945": [0, 0.43056, 0.0037, 0.02778], + "946": [0.19444, 0.69444, 0.05278, 0.08334], + "947": [0.19444, 0.43056, 0.05556, 0], + "948": [0, 0.69444, 0.03785, 0.05556], + "949": [0, 0.43056, 0, 0.08334], + "950": [0.19444, 0.69444, 0.07378, 0.08334], + "951": [0.19444, 0.43056, 0.03588, 0.05556], + "952": [0, 0.69444, 0.02778, 0.08334], + "953": [0, 0.43056, 0, 0.05556], + "954": [0, 0.43056, 0, 0], + "955": [0, 0.69444, 0, 0], + "956": [0.19444, 0.43056, 0, 0.02778], + "957": [0, 0.43056, 0.06366, 0.02778], + "958": [0.19444, 0.69444, 0.04601, 0.11111], + "959": [0, 0.43056, 0, 0.05556], + "960": [0, 0.43056, 0.03588, 0], + "961": [0.19444, 0.43056, 0, 0.08334], + "962": [0.09722, 0.43056, 0.07986, 0.08334], + "963": [0, 0.43056, 0.03588, 0], + "964": [0, 0.43056, 0.1132, 0.02778], + "965": [0, 0.43056, 0.03588, 0.02778], + "966": [0.19444, 0.43056, 0, 0.08334], + "967": [0.19444, 0.43056, 0, 0.05556], + "968": [0.19444, 0.69444, 0.03588, 0.11111], + "969": [0, 0.43056, 0.03588, 0], + "977": [0, 0.69444, 0, 0.08334], + "981": [0.19444, 0.69444, 0, 0.08334], + "982": [0, 0.43056, 0.02778, 0], + "1009": [0.19444, 0.43056, 0, 0.08334], + "1013": [0, 0.43056, 0, 0.05556], + }, + "Math-Regular": { + "65": [0, 0.68333, 0, 0.13889], + "66": [0, 0.68333, 0.05017, 0.08334], + "67": [0, 0.68333, 0.07153, 0.08334], + "68": [0, 0.68333, 0.02778, 0.05556], + "69": [0, 0.68333, 0.05764, 0.08334], + "70": [0, 0.68333, 0.13889, 0.08334], + "71": [0, 0.68333, 0, 0.08334], + "72": [0, 0.68333, 0.08125, 0.05556], + "73": [0, 0.68333, 0.07847, 0.11111], + "74": [0, 0.68333, 0.09618, 0.16667], + "75": [0, 0.68333, 0.07153, 0.05556], + "76": [0, 0.68333, 0, 0.02778], + "77": [0, 0.68333, 0.10903, 0.08334], + "78": [0, 0.68333, 0.10903, 0.08334], + "79": [0, 0.68333, 0.02778, 0.08334], + "80": [0, 0.68333, 0.13889, 0.08334], + "81": [0.19444, 0.68333, 0, 0.08334], + "82": [0, 0.68333, 0.00773, 0.08334], + "83": [0, 0.68333, 0.05764, 0.08334], + "84": [0, 0.68333, 0.13889, 0.08334], + "85": [0, 0.68333, 0.10903, 0.02778], + "86": [0, 0.68333, 0.22222, 0], + "87": [0, 0.68333, 0.13889, 0], + "88": [0, 0.68333, 0.07847, 0.08334], + "89": [0, 0.68333, 0.22222, 0], + "90": [0, 0.68333, 0.07153, 0.08334], + "97": [0, 0.43056, 0, 0], + "98": [0, 0.69444, 0, 0], + "99": [0, 0.43056, 0, 0.05556], + "100": [0, 0.69444, 0, 0.16667], + "101": [0, 0.43056, 0, 0.05556], + "102": [0.19444, 0.69444, 0.10764, 0.16667], + "103": [0.19444, 0.43056, 0.03588, 0.02778], + "104": [0, 0.69444, 0, 0], + "105": [0, 0.65952, 0, 0], + "106": [0.19444, 0.65952, 0.05724, 0], + "107": [0, 0.69444, 0.03148, 0], + "108": [0, 0.69444, 0.01968, 0.08334], + "109": [0, 0.43056, 0, 0], + "110": [0, 0.43056, 0, 0], + "111": [0, 0.43056, 0, 0.05556], + "112": [0.19444, 0.43056, 0, 0.08334], + "113": [0.19444, 0.43056, 0.03588, 0.08334], + "114": [0, 0.43056, 0.02778, 0.05556], + "115": [0, 0.43056, 0, 0.05556], + "116": [0, 0.61508, 0, 0.08334], + "117": [0, 0.43056, 0, 0.02778], + "118": [0, 0.43056, 0.03588, 0.02778], + "119": [0, 0.43056, 0.02691, 0.08334], + "120": [0, 0.43056, 0, 0.02778], + "121": [0.19444, 0.43056, 0.03588, 0.05556], + "122": [0, 0.43056, 0.04398, 0.05556], + "915": [0, 0.68333, 0.13889, 0.08334], + "916": [0, 0.68333, 0, 0.16667], + "920": [0, 0.68333, 0.02778, 0.08334], + "923": [0, 0.68333, 0, 0.16667], + "926": [0, 0.68333, 0.07569, 0.08334], + "928": [0, 0.68333, 0.08125, 0.05556], + "931": [0, 0.68333, 0.05764, 0.08334], + "933": [0, 0.68333, 0.13889, 0.05556], + "934": [0, 0.68333, 0, 0.08334], + "936": [0, 0.68333, 0.11, 0.05556], + "937": [0, 0.68333, 0.05017, 0.08334], + "945": [0, 0.43056, 0.0037, 0.02778], + "946": [0.19444, 0.69444, 0.05278, 0.08334], + "947": [0.19444, 0.43056, 0.05556, 0], + "948": [0, 0.69444, 0.03785, 0.05556], + "949": [0, 0.43056, 0, 0.08334], + "950": [0.19444, 0.69444, 0.07378, 0.08334], + "951": [0.19444, 0.43056, 0.03588, 0.05556], + "952": [0, 0.69444, 0.02778, 0.08334], + "953": [0, 0.43056, 0, 0.05556], + "954": [0, 0.43056, 0, 0], + "955": [0, 0.69444, 0, 0], + "956": [0.19444, 0.43056, 0, 0.02778], + "957": [0, 0.43056, 0.06366, 0.02778], + "958": [0.19444, 0.69444, 0.04601, 0.11111], + "959": [0, 0.43056, 0, 0.05556], + "960": [0, 0.43056, 0.03588, 0], + "961": [0.19444, 0.43056, 0, 0.08334], + "962": [0.09722, 0.43056, 0.07986, 0.08334], + "963": [0, 0.43056, 0.03588, 0], + "964": [0, 0.43056, 0.1132, 0.02778], + "965": [0, 0.43056, 0.03588, 0.02778], + "966": [0.19444, 0.43056, 0, 0.08334], + "967": [0.19444, 0.43056, 0, 0.05556], + "968": [0.19444, 0.69444, 0.03588, 0.11111], + "969": [0, 0.43056, 0.03588, 0], + "977": [0, 0.69444, 0, 0.08334], + "981": [0.19444, 0.69444, 0, 0.08334], + "982": [0, 0.43056, 0.02778, 0], + "1009": [0.19444, 0.43056, 0, 0.08334], + "1013": [0, 0.43056, 0, 0.05556], + }, + "SansSerif-Regular": { + "33": [0, 0.69444, 0, 0], + "34": [0, 0.69444, 0, 0], + "35": [0.19444, 0.69444, 0, 0], + "36": [0.05556, 0.75, 0, 0], + "37": [0.05556, 0.75, 0, 0], + "38": [0, 0.69444, 0, 0], + "39": [0, 0.69444, 0, 0], + "40": [0.25, 0.75, 0, 0], + "41": [0.25, 0.75, 0, 0], + "42": [0, 0.75, 0, 0], + "43": [0.08333, 0.58333, 0, 0], + "44": [0.125, 0.08333, 0, 0], + "45": [0, 0.44444, 0, 0], + "46": [0, 0.08333, 0, 0], + "47": [0.25, 0.75, 0, 0], + "48": [0, 0.65556, 0, 0], + "49": [0, 0.65556, 0, 0], + "50": [0, 0.65556, 0, 0], + "51": [0, 0.65556, 0, 0], + "52": [0, 0.65556, 0, 0], + "53": [0, 0.65556, 0, 0], + "54": [0, 0.65556, 0, 0], + "55": [0, 0.65556, 0, 0], + "56": [0, 0.65556, 0, 0], + "57": [0, 0.65556, 0, 0], + "58": [0, 0.44444, 0, 0], + "59": [0.125, 0.44444, 0, 0], + "61": [-0.13, 0.37, 0, 0], + "63": [0, 0.69444, 0, 0], + "64": [0, 0.69444, 0, 0], + "65": [0, 0.69444, 0, 0], + "66": [0, 0.69444, 0, 0], + "67": [0, 0.69444, 0, 0], + "68": [0, 0.69444, 0, 0], + "69": [0, 0.69444, 0, 0], + "70": [0, 0.69444, 0, 0], + "71": [0, 0.69444, 0, 0], + "72": [0, 0.69444, 0, 0], + "73": [0, 0.69444, 0, 0], + "74": [0, 0.69444, 0, 0], + "75": [0, 0.69444, 0, 0], + "76": [0, 0.69444, 0, 0], + "77": [0, 0.69444, 0, 0], + "78": [0, 0.69444, 0, 0], + "79": [0, 0.69444, 0, 0], + "80": [0, 0.69444, 0, 0], + "81": [0.125, 0.69444, 0, 0], + "82": [0, 0.69444, 0, 0], + "83": [0, 0.69444, 0, 0], + "84": [0, 0.69444, 0, 0], + "85": [0, 0.69444, 0, 0], + "86": [0, 0.69444, 0.01389, 0], + "87": [0, 0.69444, 0.01389, 0], + "88": [0, 0.69444, 0, 0], + "89": [0, 0.69444, 0.025, 0], + "90": [0, 0.69444, 0, 0], + "91": [0.25, 0.75, 0, 0], + "93": [0.25, 0.75, 0, 0], + "94": [0, 0.69444, 0, 0], + "95": [0.35, 0.09444, 0.02778, 0], + "97": [0, 0.44444, 0, 0], + "98": [0, 0.69444, 0, 0], + "99": [0, 0.44444, 0, 0], + "100": [0, 0.69444, 0, 0], + "101": [0, 0.44444, 0, 0], + "102": [0, 0.69444, 0.06944, 0], + "103": [0.19444, 0.44444, 0.01389, 0], + "104": [0, 0.69444, 0, 0], + "105": [0, 0.67937, 0, 0], + "106": [0.19444, 0.67937, 0, 0], + "107": [0, 0.69444, 0, 0], + "108": [0, 0.69444, 0, 0], + "109": [0, 0.44444, 0, 0], + "110": [0, 0.44444, 0, 0], + "111": [0, 0.44444, 0, 0], + "112": [0.19444, 0.44444, 0, 0], + "113": [0.19444, 0.44444, 0, 0], + "114": [0, 0.44444, 0.01389, 0], + "115": [0, 0.44444, 0, 0], + "116": [0, 0.57143, 0, 0], + "117": [0, 0.44444, 0, 0], + "118": [0, 0.44444, 0.01389, 0], + "119": [0, 0.44444, 0.01389, 0], + "120": [0, 0.44444, 0, 0], + "121": [0.19444, 0.44444, 0.01389, 0], + "122": [0, 0.44444, 0, 0], + "126": [0.35, 0.32659, 0, 0], + "305": [0, 0.44444, 0, 0], + "567": [0.19444, 0.44444, 0, 0], + "768": [0, 0.69444, 0, 0], + "769": [0, 0.69444, 0, 0], + "770": [0, 0.69444, 0, 0], + "771": [0, 0.67659, 0, 0], + "772": [0, 0.60889, 0, 0], + "774": [0, 0.69444, 0, 0], + "775": [0, 0.67937, 0, 0], + "776": [0, 0.67937, 0, 0], + "778": [0, 0.69444, 0, 0], + "779": [0, 0.69444, 0, 0], + "780": [0, 0.63194, 0, 0], + "915": [0, 0.69444, 0, 0], + "916": [0, 0.69444, 0, 0], + "920": [0, 0.69444, 0, 0], + "923": [0, 0.69444, 0, 0], + "926": [0, 0.69444, 0, 0], + "928": [0, 0.69444, 0, 0], + "931": [0, 0.69444, 0, 0], + "933": [0, 0.69444, 0, 0], + "934": [0, 0.69444, 0, 0], + "936": [0, 0.69444, 0, 0], + "937": [0, 0.69444, 0, 0], + "8211": [0, 0.44444, 0.02778, 0], + "8212": [0, 0.44444, 0.02778, 0], + "8216": [0, 0.69444, 0, 0], + "8217": [0, 0.69444, 0, 0], + "8220": [0, 0.69444, 0, 0], + "8221": [0, 0.69444, 0, 0], + }, + "Script-Regular": { + "65": [0, 0.7, 0.22925, 0], + "66": [0, 0.7, 0.04087, 0], + "67": [0, 0.7, 0.1689, 0], + "68": [0, 0.7, 0.09371, 0], + "69": [0, 0.7, 0.18583, 0], + "70": [0, 0.7, 0.13634, 0], + "71": [0, 0.7, 0.17322, 0], + "72": [0, 0.7, 0.29694, 0], + "73": [0, 0.7, 0.19189, 0], + "74": [0.27778, 0.7, 0.19189, 0], + "75": [0, 0.7, 0.31259, 0], + "76": [0, 0.7, 0.19189, 0], + "77": [0, 0.7, 0.15981, 0], + "78": [0, 0.7, 0.3525, 0], + "79": [0, 0.7, 0.08078, 0], + "80": [0, 0.7, 0.08078, 0], + "81": [0, 0.7, 0.03305, 0], + "82": [0, 0.7, 0.06259, 0], + "83": [0, 0.7, 0.19189, 0], + "84": [0, 0.7, 0.29087, 0], + "85": [0, 0.7, 0.25815, 0], + "86": [0, 0.7, 0.27523, 0], + "87": [0, 0.7, 0.27523, 0], + "88": [0, 0.7, 0.26006, 0], + "89": [0, 0.7, 0.2939, 0], + "90": [0, 0.7, 0.24037, 0], + }, + "Size1-Regular": { + "40": [0.35001, 0.85, 0, 0], + "41": [0.35001, 0.85, 0, 0], + "47": [0.35001, 0.85, 0, 0], + "91": [0.35001, 0.85, 0, 0], + "92": [0.35001, 0.85, 0, 0], + "93": [0.35001, 0.85, 0, 0], + "123": [0.35001, 0.85, 0, 0], + "125": [0.35001, 0.85, 0, 0], + "710": [0, 0.72222, 0, 0], + "732": [0, 0.72222, 0, 0], + "770": [0, 0.72222, 0, 0], + "771": [0, 0.72222, 0, 0], + "8214": [-0.00099, 0.601, 0, 0], + "8593": [1e-05, 0.6, 0, 0], + "8595": [1e-05, 0.6, 0, 0], + "8657": [1e-05, 0.6, 0, 0], + "8659": [1e-05, 0.6, 0, 0], + "8719": [0.25001, 0.75, 0, 0], + "8720": [0.25001, 0.75, 0, 0], + "8721": [0.25001, 0.75, 0, 0], + "8730": [0.35001, 0.85, 0, 0], + "8739": [-0.00599, 0.606, 0, 0], + "8741": [-0.00599, 0.606, 0, 0], + "8747": [0.30612, 0.805, 0.19445, 0], + "8748": [0.306, 0.805, 0.19445, 0], + "8749": [0.306, 0.805, 0.19445, 0], + "8750": [0.30612, 0.805, 0.19445, 0], + "8896": [0.25001, 0.75, 0, 0], + "8897": [0.25001, 0.75, 0, 0], + "8898": [0.25001, 0.75, 0, 0], + "8899": [0.25001, 0.75, 0, 0], + "8968": [0.35001, 0.85, 0, 0], + "8969": [0.35001, 0.85, 0, 0], + "8970": [0.35001, 0.85, 0, 0], + "8971": [0.35001, 0.85, 0, 0], + "9168": [-0.00099, 0.601, 0, 0], + "10216": [0.35001, 0.85, 0, 0], + "10217": [0.35001, 0.85, 0, 0], + "10752": [0.25001, 0.75, 0, 0], + "10753": [0.25001, 0.75, 0, 0], + "10754": [0.25001, 0.75, 0, 0], + "10756": [0.25001, 0.75, 0, 0], + "10758": [0.25001, 0.75, 0, 0], + }, + "Size2-Regular": { + "40": [0.65002, 1.15, 0, 0], + "41": [0.65002, 1.15, 0, 0], + "47": [0.65002, 1.15, 0, 0], + "91": [0.65002, 1.15, 0, 0], + "92": [0.65002, 1.15, 0, 0], + "93": [0.65002, 1.15, 0, 0], + "123": [0.65002, 1.15, 0, 0], + "125": [0.65002, 1.15, 0, 0], + "710": [0, 0.75, 0, 0], + "732": [0, 0.75, 0, 0], + "770": [0, 0.75, 0, 0], + "771": [0, 0.75, 0, 0], + "8719": [0.55001, 1.05, 0, 0], + "8720": [0.55001, 1.05, 0, 0], + "8721": [0.55001, 1.05, 0, 0], + "8730": [0.65002, 1.15, 0, 0], + "8747": [0.86225, 1.36, 0.44445, 0], + "8748": [0.862, 1.36, 0.44445, 0], + "8749": [0.862, 1.36, 0.44445, 0], + "8750": [0.86225, 1.36, 0.44445, 0], + "8896": [0.55001, 1.05, 0, 0], + "8897": [0.55001, 1.05, 0, 0], + "8898": [0.55001, 1.05, 0, 0], + "8899": [0.55001, 1.05, 0, 0], + "8968": [0.65002, 1.15, 0, 0], + "8969": [0.65002, 1.15, 0, 0], + "8970": [0.65002, 1.15, 0, 0], + "8971": [0.65002, 1.15, 0, 0], + "10216": [0.65002, 1.15, 0, 0], + "10217": [0.65002, 1.15, 0, 0], + "10752": [0.55001, 1.05, 0, 0], + "10753": [0.55001, 1.05, 0, 0], + "10754": [0.55001, 1.05, 0, 0], + "10756": [0.55001, 1.05, 0, 0], + "10758": [0.55001, 1.05, 0, 0], + }, + "Size3-Regular": { + "40": [0.95003, 1.45, 0, 0], + "41": [0.95003, 1.45, 0, 0], + "47": [0.95003, 1.45, 0, 0], + "91": [0.95003, 1.45, 0, 0], + "92": [0.95003, 1.45, 0, 0], + "93": [0.95003, 1.45, 0, 0], + "123": [0.95003, 1.45, 0, 0], + "125": [0.95003, 1.45, 0, 0], + "710": [0, 0.75, 0, 0], + "732": [0, 0.75, 0, 0], + "770": [0, 0.75, 0, 0], + "771": [0, 0.75, 0, 0], + "8730": [0.95003, 1.45, 0, 0], + "8968": [0.95003, 1.45, 0, 0], + "8969": [0.95003, 1.45, 0, 0], + "8970": [0.95003, 1.45, 0, 0], + "8971": [0.95003, 1.45, 0, 0], + "10216": [0.95003, 1.45, 0, 0], + "10217": [0.95003, 1.45, 0, 0], + }, + "Size4-Regular": { + "40": [1.25003, 1.75, 0, 0], + "41": [1.25003, 1.75, 0, 0], + "47": [1.25003, 1.75, 0, 0], + "91": [1.25003, 1.75, 0, 0], + "92": [1.25003, 1.75, 0, 0], + "93": [1.25003, 1.75, 0, 0], + "123": [1.25003, 1.75, 0, 0], + "125": [1.25003, 1.75, 0, 0], + "710": [0, 0.825, 0, 0], + "732": [0, 0.825, 0, 0], + "770": [0, 0.825, 0, 0], + "771": [0, 0.825, 0, 0], + "8730": [1.25003, 1.75, 0, 0], + "8968": [1.25003, 1.75, 0, 0], + "8969": [1.25003, 1.75, 0, 0], + "8970": [1.25003, 1.75, 0, 0], + "8971": [1.25003, 1.75, 0, 0], + "9115": [0.64502, 1.155, 0, 0], + "9116": [1e-05, 0.6, 0, 0], + "9117": [0.64502, 1.155, 0, 0], + "9118": [0.64502, 1.155, 0, 0], + "9119": [1e-05, 0.6, 0, 0], + "9120": [0.64502, 1.155, 0, 0], + "9121": [0.64502, 1.155, 0, 0], + "9122": [-0.00099, 0.601, 0, 0], + "9123": [0.64502, 1.155, 0, 0], + "9124": [0.64502, 1.155, 0, 0], + "9125": [-0.00099, 0.601, 0, 0], + "9126": [0.64502, 1.155, 0, 0], + "9127": [1e-05, 0.9, 0, 0], + "9128": [0.65002, 1.15, 0, 0], + "9129": [0.90001, 0, 0, 0], + "9130": [0, 0.3, 0, 0], + "9131": [1e-05, 0.9, 0, 0], + "9132": [0.65002, 1.15, 0, 0], + "9133": [0.90001, 0, 0, 0], + "9143": [0.88502, 0.915, 0, 0], + "10216": [1.25003, 1.75, 0, 0], + "10217": [1.25003, 1.75, 0, 0], + "57344": [-0.00499, 0.605, 0, 0], + "57345": [-0.00499, 0.605, 0, 0], + "57680": [0, 0.12, 0, 0], + "57681": [0, 0.12, 0, 0], + "57682": [0, 0.12, 0, 0], + "57683": [0, 0.12, 0, 0], + }, + "Typewriter-Regular": { + "33": [0, 0.61111, 0, 0], + "34": [0, 0.61111, 0, 0], + "35": [0, 0.61111, 0, 0], + "36": [0.08333, 0.69444, 0, 0], + "37": [0.08333, 0.69444, 0, 0], + "38": [0, 0.61111, 0, 0], + "39": [0, 0.61111, 0, 0], + "40": [0.08333, 0.69444, 0, 0], + "41": [0.08333, 0.69444, 0, 0], + "42": [0, 0.52083, 0, 0], + "43": [-0.08056, 0.53055, 0, 0], + "44": [0.13889, 0.125, 0, 0], + "45": [-0.08056, 0.53055, 0, 0], + "46": [0, 0.125, 0, 0], + "47": [0.08333, 0.69444, 0, 0], + "48": [0, 0.61111, 0, 0], + "49": [0, 0.61111, 0, 0], + "50": [0, 0.61111, 0, 0], + "51": [0, 0.61111, 0, 0], + "52": [0, 0.61111, 0, 0], + "53": [0, 0.61111, 0, 0], + "54": [0, 0.61111, 0, 0], + "55": [0, 0.61111, 0, 0], + "56": [0, 0.61111, 0, 0], + "57": [0, 0.61111, 0, 0], + "58": [0, 0.43056, 0, 0], + "59": [0.13889, 0.43056, 0, 0], + "60": [-0.05556, 0.55556, 0, 0], + "61": [-0.19549, 0.41562, 0, 0], + "62": [-0.05556, 0.55556, 0, 0], + "63": [0, 0.61111, 0, 0], + "64": [0, 0.61111, 0, 0], + "65": [0, 0.61111, 0, 0], + "66": [0, 0.61111, 0, 0], + "67": [0, 0.61111, 0, 0], + "68": [0, 0.61111, 0, 0], + "69": [0, 0.61111, 0, 0], + "70": [0, 0.61111, 0, 0], + "71": [0, 0.61111, 0, 0], + "72": [0, 0.61111, 0, 0], + "73": [0, 0.61111, 0, 0], + "74": [0, 0.61111, 0, 0], + "75": [0, 0.61111, 0, 0], + "76": [0, 0.61111, 0, 0], + "77": [0, 0.61111, 0, 0], + "78": [0, 0.61111, 0, 0], + "79": [0, 0.61111, 0, 0], + "80": [0, 0.61111, 0, 0], + "81": [0.13889, 0.61111, 0, 0], + "82": [0, 0.61111, 0, 0], + "83": [0, 0.61111, 0, 0], + "84": [0, 0.61111, 0, 0], + "85": [0, 0.61111, 0, 0], + "86": [0, 0.61111, 0, 0], + "87": [0, 0.61111, 0, 0], + "88": [0, 0.61111, 0, 0], + "89": [0, 0.61111, 0, 0], + "90": [0, 0.61111, 0, 0], + "91": [0.08333, 0.69444, 0, 0], + "92": [0.08333, 0.69444, 0, 0], + "93": [0.08333, 0.69444, 0, 0], + "94": [0, 0.61111, 0, 0], + "95": [0.09514, 0, 0, 0], + "96": [0, 0.61111, 0, 0], + "97": [0, 0.43056, 0, 0], + "98": [0, 0.61111, 0, 0], + "99": [0, 0.43056, 0, 0], + "100": [0, 0.61111, 0, 0], + "101": [0, 0.43056, 0, 0], + "102": [0, 0.61111, 0, 0], + "103": [0.22222, 0.43056, 0, 0], + "104": [0, 0.61111, 0, 0], + "105": [0, 0.61111, 0, 0], + "106": [0.22222, 0.61111, 0, 0], + "107": [0, 0.61111, 0, 0], + "108": [0, 0.61111, 0, 0], + "109": [0, 0.43056, 0, 0], + "110": [0, 0.43056, 0, 0], + "111": [0, 0.43056, 0, 0], + "112": [0.22222, 0.43056, 0, 0], + "113": [0.22222, 0.43056, 0, 0], + "114": [0, 0.43056, 0, 0], + "115": [0, 0.43056, 0, 0], + "116": [0, 0.55358, 0, 0], + "117": [0, 0.43056, 0, 0], + "118": [0, 0.43056, 0, 0], + "119": [0, 0.43056, 0, 0], + "120": [0, 0.43056, 0, 0], + "121": [0.22222, 0.43056, 0, 0], + "122": [0, 0.43056, 0, 0], + "123": [0.08333, 0.69444, 0, 0], + "124": [0.08333, 0.69444, 0, 0], + "125": [0.08333, 0.69444, 0, 0], + "126": [0, 0.61111, 0, 0], + "127": [0, 0.61111, 0, 0], + "305": [0, 0.43056, 0, 0], + "567": [0.22222, 0.43056, 0, 0], + "768": [0, 0.61111, 0, 0], + "769": [0, 0.61111, 0, 0], + "770": [0, 0.61111, 0, 0], + "771": [0, 0.61111, 0, 0], + "772": [0, 0.56555, 0, 0], + "774": [0, 0.61111, 0, 0], + "776": [0, 0.61111, 0, 0], + "778": [0, 0.61111, 0, 0], + "780": [0, 0.56597, 0, 0], + "915": [0, 0.61111, 0, 0], + "916": [0, 0.61111, 0, 0], + "920": [0, 0.61111, 0, 0], + "923": [0, 0.61111, 0, 0], + "926": [0, 0.61111, 0, 0], + "928": [0, 0.61111, 0, 0], + "931": [0, 0.61111, 0, 0], + "933": [0, 0.61111, 0, 0], + "934": [0, 0.61111, 0, 0], + "936": [0, 0.61111, 0, 0], + "937": [0, 0.61111, 0, 0], + "2018": [0, 0.61111, 0, 0], + "2019": [0, 0.61111, 0, 0], + "8242": [0, 0.61111, 0, 0], + }, +}; + +},{}],19:[function(require,module,exports){ +var utils = require("./utils"); +var ParseError = require("./ParseError"); + +/* This file contains a list of functions that we parse, identified by + * the calls to defineFunction. + * + * The first argument to defineFunction is a single name or a list of names. + * All functions named in such a list will share a single implementation. + * + * Each declared function can have associated properties, which + * include the following: + * + * - numArgs: The number of arguments the function takes. + * If this is the only property, it can be passed as a number + * instead of an element of a properties object. + * - argTypes: (optional) An array corresponding to each argument of the + * function, giving the type of argument that should be parsed. Its + * length should be equal to `numArgs + numOptionalArgs`. Valid + * types: + * - "size": A size-like thing, such as "1em" or "5ex" + * - "color": An html color, like "#abc" or "blue" + * - "original": The same type as the environment that the + * function being parsed is in (e.g. used for the + * bodies of functions like \color where the first + * argument is special and the second argument is + * parsed normally) + * Other possible types (probably shouldn't be used) + * - "text": Text-like (e.g. \text) + * - "math": Normal math + * If undefined, this will be treated as an appropriate length + * array of "original" strings + * - greediness: (optional) The greediness of the function to use ungrouped + * arguments. + * + * E.g. if you have an expression + * \sqrt \frac 1 2 + * since \frac has greediness=2 vs \sqrt's greediness=1, \frac + * will use the two arguments '1' and '2' as its two arguments, + * then that whole function will be used as the argument to + * \sqrt. On the other hand, the expressions + * \frac \frac 1 2 3 + * and + * \frac \sqrt 1 2 + * will fail because \frac and \frac have equal greediness + * and \sqrt has a lower greediness than \frac respectively. To + * make these parse, we would have to change them to: + * \frac {\frac 1 2} 3 + * and + * \frac {\sqrt 1} 2 + * + * The default value is `1` + * - allowedInText: (optional) Whether or not the function is allowed inside + * text mode (default false) + * - numOptionalArgs: (optional) The number of optional arguments the function + * should parse. If the optional arguments aren't found, + * `null` will be passed to the handler in their place. + * (default 0) + * - infix: (optional) Must be true if the function is an infix operator. + * + * The last argument is that implementation, the handler for the function(s). + * It is called to handle these functions and their arguments. + * It receives two arguments: + * - context contains information and references provided by the parser + * - args is an array of arguments obtained from TeX input + * The context contains the following properties: + * - funcName: the text (i.e. name) of the function, including \ + * - parser: the parser object + * - lexer: the lexer object + * - positions: the positions in the overall string of the function + * and the arguments. + * The latter three should only be used to produce error messages. + * + * The function should return an object with the following keys: + * - type: The type of element that this is. This is then used in + * buildHTML/buildMathML to determine which function + * should be called to build this node into a DOM node + * Any other data can be added to the object, which will be passed + * in to the function in buildHTML/buildMathML as `group.value`. + */ + +function defineFunction(names, props, handler) { + if (typeof names === "string") { + names = [names]; + } + if (typeof props === "number") { + props = { numArgs: props }; + } + // Set default values of functions + var data = { + numArgs: props.numArgs, + argTypes: props.argTypes, + greediness: (props.greediness === undefined) ? 1 : props.greediness, + allowedInText: !!props.allowedInText, + numOptionalArgs: props.numOptionalArgs || 0, + infix: !!props.infix, + handler: handler, + }; + for (var i = 0; i < names.length; ++i) { + module.exports[names[i]] = data; + } +} + +// A normal square root +defineFunction("\\sqrt", { + numArgs: 1, + numOptionalArgs: 1, +}, function(context, args) { + var index = args[0]; + var body = args[1]; + return { + type: "sqrt", + body: body, + index: index, + }; +}); + +// Some non-mathy text +defineFunction("\\text", { + numArgs: 1, + argTypes: ["text"], + greediness: 2, +}, function(context, args) { + var body = args[0]; + // Since the corresponding buildHTML/buildMathML function expects a + // list of elements, we normalize for different kinds of arguments + // TODO(emily): maybe this should be done somewhere else + var inner; + if (body.type === "ordgroup") { + inner = body.value; + } else { + inner = [body]; + } + + return { + type: "text", + body: inner, + }; +}); + +// A two-argument custom color +defineFunction("\\color", { + numArgs: 2, + allowedInText: true, + greediness: 3, + argTypes: ["color", "original"], +}, function(context, args) { + var color = args[0]; + var body = args[1]; + // Normalize the different kinds of bodies (see \text above) + var inner; + if (body.type === "ordgroup") { + inner = body.value; + } else { + inner = [body]; + } + + return { + type: "color", + color: color.value, + value: inner, + }; +}); + +// An overline +defineFunction("\\overline", { + numArgs: 1, +}, function(context, args) { + var body = args[0]; + return { + type: "overline", + body: body, + }; +}); + +// An underline +defineFunction("\\underline", { + numArgs: 1, +}, function(context, args) { + var body = args[0]; + return { + type: "underline", + body: body, + }; +}); + +// A box of the width and height +defineFunction("\\rule", { + numArgs: 2, + numOptionalArgs: 1, + argTypes: ["size", "size", "size"], +}, function(context, args) { + var shift = args[0]; + var width = args[1]; + var height = args[2]; + return { + type: "rule", + shift: shift && shift.value, + width: width.value, + height: height.value, + }; +}); + +defineFunction("\\kern", { + numArgs: 1, + argTypes: ["size"], +}, function(context, args) { + return { + type: "kern", + dimension: args[0].value, + }; +}); + +// A KaTeX logo +defineFunction("\\KaTeX", { + numArgs: 0, +}, function(context) { + return { + type: "katex", + }; +}); + +defineFunction("\\phantom", { + numArgs: 1, +}, function(context, args) { + var body = args[0]; + var inner; + if (body.type === "ordgroup") { + inner = body.value; + } else { + inner = [body]; + } + + return { + type: "phantom", + value: inner, + }; +}); + +// Extra data needed for the delimiter handler down below +var delimiterSizes = { + "\\bigl" : {type: "open", size: 1}, + "\\Bigl" : {type: "open", size: 2}, + "\\biggl": {type: "open", size: 3}, + "\\Biggl": {type: "open", size: 4}, + "\\bigr" : {type: "close", size: 1}, + "\\Bigr" : {type: "close", size: 2}, + "\\biggr": {type: "close", size: 3}, + "\\Biggr": {type: "close", size: 4}, + "\\bigm" : {type: "rel", size: 1}, + "\\Bigm" : {type: "rel", size: 2}, + "\\biggm": {type: "rel", size: 3}, + "\\Biggm": {type: "rel", size: 4}, + "\\big" : {type: "textord", size: 1}, + "\\Big" : {type: "textord", size: 2}, + "\\bigg" : {type: "textord", size: 3}, + "\\Bigg" : {type: "textord", size: 4}, +}; + +var delimiters = [ + "(", ")", "[", "\\lbrack", "]", "\\rbrack", + "\\{", "\\lbrace", "\\}", "\\rbrace", + "\\lfloor", "\\rfloor", "\\lceil", "\\rceil", + "<", ">", "\\langle", "\\rangle", "\\lt", "\\gt", + "\\lvert", "\\rvert", "\\lVert", "\\rVert", + "\\lgroup", "\\rgroup", "\\lmoustache", "\\rmoustache", + "/", "\\backslash", + "|", "\\vert", "\\|", "\\Vert", + "\\uparrow", "\\Uparrow", + "\\downarrow", "\\Downarrow", + "\\updownarrow", "\\Updownarrow", + ".", +]; + +var fontAliases = { + "\\Bbb": "\\mathbb", + "\\bold": "\\mathbf", + "\\frak": "\\mathfrak", +}; + +// Single-argument color functions +defineFunction([ + "\\blue", "\\orange", "\\pink", "\\red", + "\\green", "\\gray", "\\purple", + "\\blueA", "\\blueB", "\\blueC", "\\blueD", "\\blueE", + "\\tealA", "\\tealB", "\\tealC", "\\tealD", "\\tealE", + "\\greenA", "\\greenB", "\\greenC", "\\greenD", "\\greenE", + "\\goldA", "\\goldB", "\\goldC", "\\goldD", "\\goldE", + "\\redA", "\\redB", "\\redC", "\\redD", "\\redE", + "\\maroonA", "\\maroonB", "\\maroonC", "\\maroonD", "\\maroonE", + "\\purpleA", "\\purpleB", "\\purpleC", "\\purpleD", "\\purpleE", + "\\mintA", "\\mintB", "\\mintC", + "\\grayA", "\\grayB", "\\grayC", "\\grayD", "\\grayE", + "\\grayF", "\\grayG", "\\grayH", "\\grayI", + "\\kaBlue", "\\kaGreen", +], { + numArgs: 1, + allowedInText: true, + greediness: 3, +}, function(context, args) { + var body = args[0]; + var atoms; + if (body.type === "ordgroup") { + atoms = body.value; + } else { + atoms = [body]; + } + + return { + type: "color", + color: "katex-" + context.funcName.slice(1), + value: atoms, + }; +}); + +// There are 2 flags for operators; whether they produce limits in +// displaystyle, and whether they are symbols and should grow in +// displaystyle. These four groups cover the four possible choices. + +// No limits, not symbols +defineFunction([ + "\\arcsin", "\\arccos", "\\arctan", "\\arg", "\\cos", "\\cosh", + "\\cot", "\\coth", "\\csc", "\\deg", "\\dim", "\\exp", "\\hom", + "\\ker", "\\lg", "\\ln", "\\log", "\\sec", "\\sin", "\\sinh", + "\\tan", "\\tanh", +], { + numArgs: 0, +}, function(context) { + return { + type: "op", + limits: false, + symbol: false, + body: context.funcName, + }; +}); + +// Limits, not symbols +defineFunction([ + "\\det", "\\gcd", "\\inf", "\\lim", "\\liminf", "\\limsup", "\\max", + "\\min", "\\Pr", "\\sup", +], { + numArgs: 0, +}, function(context) { + return { + type: "op", + limits: true, + symbol: false, + body: context.funcName, + }; +}); + +// No limits, symbols +defineFunction([ + "\\int", "\\iint", "\\iiint", "\\oint", +], { + numArgs: 0, +}, function(context) { + return { + type: "op", + limits: false, + symbol: true, + body: context.funcName, + }; +}); + +// Limits, symbols +defineFunction([ + "\\coprod", "\\bigvee", "\\bigwedge", "\\biguplus", "\\bigcap", + "\\bigcup", "\\intop", "\\prod", "\\sum", "\\bigotimes", + "\\bigoplus", "\\bigodot", "\\bigsqcup", "\\smallint", +], { + numArgs: 0, +}, function(context) { + return { + type: "op", + limits: true, + symbol: true, + body: context.funcName, + }; +}); + +// Fractions +defineFunction([ + "\\dfrac", "\\frac", "\\tfrac", + "\\dbinom", "\\binom", "\\tbinom", +], { + numArgs: 2, + greediness: 2, +}, function(context, args) { + var numer = args[0]; + var denom = args[1]; + var hasBarLine; + var leftDelim = null; + var rightDelim = null; + var size = "auto"; + + switch (context.funcName) { + case "\\dfrac": + case "\\frac": + case "\\tfrac": + hasBarLine = true; + break; + case "\\dbinom": + case "\\binom": + case "\\tbinom": + hasBarLine = false; + leftDelim = "("; + rightDelim = ")"; + break; + default: + throw new Error("Unrecognized genfrac command"); + } + + switch (context.funcName) { + case "\\dfrac": + case "\\dbinom": + size = "display"; + break; + case "\\tfrac": + case "\\tbinom": + size = "text"; + break; + } + + return { + type: "genfrac", + numer: numer, + denom: denom, + hasBarLine: hasBarLine, + leftDelim: leftDelim, + rightDelim: rightDelim, + size: size, + }; +}); + +// Left and right overlap functions +defineFunction(["\\llap", "\\rlap"], { + numArgs: 1, + allowedInText: true, +}, function(context, args) { + var body = args[0]; + return { + type: context.funcName.slice(1), + body: body, + }; +}); + +// Delimiter functions +defineFunction([ + "\\bigl", "\\Bigl", "\\biggl", "\\Biggl", + "\\bigr", "\\Bigr", "\\biggr", "\\Biggr", + "\\bigm", "\\Bigm", "\\biggm", "\\Biggm", + "\\big", "\\Big", "\\bigg", "\\Bigg", + "\\left", "\\right", +], { + numArgs: 1, +}, function(context, args) { + var delim = args[0]; + if (!utils.contains(delimiters, delim.value)) { + throw new ParseError( + "Invalid delimiter: '" + delim.value + "' after '" + + context.funcName + "'", delim); + } + + // \left and \right are caught somewhere in Parser.js, which is + // why this data doesn't match what is in buildHTML. + if (context.funcName === "\\left" || context.funcName === "\\right") { + return { + type: "leftright", + value: delim.value, + }; + } else { + return { + type: "delimsizing", + size: delimiterSizes[context.funcName].size, + delimType: delimiterSizes[context.funcName].type, + value: delim.value, + }; + } +}); + +// Sizing functions (handled in Parser.js explicitly, hence no handler) +defineFunction([ + "\\tiny", "\\scriptsize", "\\footnotesize", "\\small", + "\\normalsize", "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge", +], 0, null); + +// Style changing functions (handled in Parser.js explicitly, hence no +// handler) +defineFunction([ + "\\displaystyle", "\\textstyle", "\\scriptstyle", + "\\scriptscriptstyle", +], 0, null); + +defineFunction([ + // styles + "\\mathrm", "\\mathit", "\\mathbf", + + // families + "\\mathbb", "\\mathcal", "\\mathfrak", "\\mathscr", "\\mathsf", + "\\mathtt", + + // aliases + "\\Bbb", "\\bold", "\\frak", +], { + numArgs: 1, + greediness: 2, +}, function(context, args) { + var body = args[0]; + var func = context.funcName; + if (func in fontAliases) { + func = fontAliases[func]; + } + return { + type: "font", + font: func.slice(1), + body: body, + }; +}); + +// Accents +defineFunction([ + "\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve", + "\\check", "\\hat", "\\vec", "\\dot", + // We don't support expanding accents yet + // "\\widetilde", "\\widehat" +], { + numArgs: 1, +}, function(context, args) { + var base = args[0]; + return { + type: "accent", + accent: context.funcName, + base: base, + }; +}); + +// Infix generalized fractions +defineFunction(["\\over", "\\choose"], { + numArgs: 0, + infix: true, +}, function(context) { + var replaceWith; + switch (context.funcName) { + case "\\over": + replaceWith = "\\frac"; + break; + case "\\choose": + replaceWith = "\\binom"; + break; + default: + throw new Error("Unrecognized infix genfrac command"); + } + return { + type: "infix", + replaceWith: replaceWith, + token: context.token, + }; +}); + +// Row breaks for aligned data +defineFunction(["\\\\", "\\cr"], { + numArgs: 0, + numOptionalArgs: 1, + argTypes: ["size"], +}, function(context, args) { + var size = args[0]; + return { + type: "cr", + size: size, + }; +}); + +// Environment delimiters +defineFunction(["\\begin", "\\end"], { + numArgs: 1, + argTypes: ["text"], +}, function(context, args) { + var nameGroup = args[0]; + if (nameGroup.type !== "ordgroup") { + throw new ParseError("Invalid environment name", nameGroup); + } + var name = ""; + for (var i = 0; i < nameGroup.value.length; ++i) { + name += nameGroup.value[i].value; + } + return { + type: "environment", + name: name, + nameGroup: nameGroup, + }; +}); + +},{"./ParseError":6,"./utils":25}],20:[function(require,module,exports){ +/** + * These objects store data about MathML nodes. This is the MathML equivalent + * of the types in domTree.js. Since MathML handles its own rendering, and + * since we're mainly using MathML to improve accessibility, we don't manage + * any of the styling state that the plain DOM nodes do. + * + * The `toNode` and `toMarkup` functions work simlarly to how they do in + * domTree.js, creating namespaced DOM nodes and HTML text markup respectively. + */ + +var utils = require("./utils"); + +/** + * This node represents a general purpose MathML node of any type. The + * constructor requires the type of node to create (for example, `"mo"` or + * `"mspace"`, corresponding to `<mo>` and `<mspace>` tags). + */ +function MathNode(type, children) { + this.type = type; + this.attributes = {}; + this.children = children || []; +} + +/** + * Sets an attribute on a MathML node. MathML depends on attributes to convey a + * semantic content, so this is used heavily. + */ +MathNode.prototype.setAttribute = function(name, value) { + this.attributes[name] = value; +}; + +/** + * Converts the math node into a MathML-namespaced DOM element. + */ +MathNode.prototype.toNode = function() { + var node = document.createElementNS( + "http://www.w3.org/1998/Math/MathML", this.type); + + for (var attr in this.attributes) { + if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { + node.setAttribute(attr, this.attributes[attr]); + } + } + + for (var i = 0; i < this.children.length; i++) { + node.appendChild(this.children[i].toNode()); + } + + return node; +}; + +/** + * Converts the math node into an HTML markup string. + */ +MathNode.prototype.toMarkup = function() { + var markup = "<" + this.type; + + // Add the attributes + for (var attr in this.attributes) { + if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { + markup += " " + attr + "=\""; + markup += utils.escape(this.attributes[attr]); + markup += "\""; + } + } + + markup += ">"; + + for (var i = 0; i < this.children.length; i++) { + markup += this.children[i].toMarkup(); + } + + markup += "</" + this.type + ">"; + + return markup; +}; + +/** + * This node represents a piece of text. + */ +function TextNode(text) { + this.text = text; +} + +/** + * Converts the text node into a DOM text node. + */ +TextNode.prototype.toNode = function() { + return document.createTextNode(this.text); +}; + +/** + * Converts the text node into HTML markup (which is just the text itself). + */ +TextNode.prototype.toMarkup = function() { + return utils.escape(this.text); +}; + +module.exports = { + MathNode: MathNode, + TextNode: TextNode, +}; + +},{"./utils":25}],21:[function(require,module,exports){ +/** + * The resulting parse tree nodes of the parse tree. + * + * It is possible to provide position information, so that a ParseNode can + * fulfil a role similar to a Token in error reporting. + * For details on the corresponding properties see Token constructor. + * Providing such information can lead to better error reporting. + * + * @param {string} type type of node, like e.g. "ordgroup" + * @param {?object} value type-specific representation of the node + * @param {string} mode parse mode in action for this node, + * "math" or "text" + * @param {Token=} firstToken first token of the input for this node, + * will omit position information if unset + * @param {Token=} lastToken last token of the input for this node, + * will default to firstToken if unset + */ +function ParseNode(type, value, mode, firstToken, lastToken) { + this.type = type; + this.value = value; + this.mode = mode; + if (firstToken && (!lastToken || lastToken.lexer === firstToken.lexer)) { + this.lexer = firstToken.lexer; + this.start = firstToken.start; + this.end = (lastToken || firstToken).end; + } +} + +module.exports = { + ParseNode: ParseNode, +}; + + +},{}],22:[function(require,module,exports){ +/** + * Provides a single function for parsing an expression using a Parser + * TODO(emily): Remove this + */ + +var Parser = require("./Parser"); + +/** + * Parses an expression using a Parser, then returns the parsed result. + */ +var parseTree = function(toParse, settings) { + if (!(typeof toParse === 'string' || toParse instanceof String)) { + throw new TypeError('KaTeX can only parse string typed expression'); + } + var parser = new Parser(toParse, settings); + + return parser.parse(); +}; + +module.exports = parseTree; + +},{"./Parser":7}],23:[function(require,module,exports){ +/** + * This file holds a list of all no-argument functions and single-character + * symbols (like 'a' or ';'). + * + * For each of the symbols, there are three properties they can have: + * - font (required): the font to be used for this symbol. Either "main" (the + normal font), or "ams" (the ams fonts). + * - group (required): the ParseNode group type the symbol should have (i.e. + "textord", "mathord", etc). + See https://github.com/Khan/KaTeX/wiki/Examining-TeX#group-types + * - replace: the character that this symbol or function should be + * replaced with (i.e. "\phi" has a replace value of "\u03d5", the phi + * character in the main font). + * + * The outermost map in the table indicates what mode the symbols should be + * accepted in (e.g. "math" or "text"). + */ + +module.exports = { + math: {}, + text: {}, +}; + +function defineSymbol(mode, font, group, replace, name) { + module.exports[mode][name] = { + font: font, + group: group, + replace: replace, + }; +} + +// Some abbreviations for commonly used strings. +// This helps minify the code, and also spotting typos using jshint. + +// modes: +var math = "math"; +var text = "text"; + +// fonts: +var main = "main"; +var ams = "ams"; + +// groups: +var accent = "accent"; +var bin = "bin"; +var close = "close"; +var inner = "inner"; +var mathord = "mathord"; +var op = "op"; +var open = "open"; +var punct = "punct"; +var rel = "rel"; +var spacing = "spacing"; +var textord = "textord"; + +// Now comes the symbol table + +// Relation Symbols +defineSymbol(math, main, rel, "\u2261", "\\equiv"); +defineSymbol(math, main, rel, "\u227a", "\\prec"); +defineSymbol(math, main, rel, "\u227b", "\\succ"); +defineSymbol(math, main, rel, "\u223c", "\\sim"); +defineSymbol(math, main, rel, "\u22a5", "\\perp"); +defineSymbol(math, main, rel, "\u2aaf", "\\preceq"); +defineSymbol(math, main, rel, "\u2ab0", "\\succeq"); +defineSymbol(math, main, rel, "\u2243", "\\simeq"); +defineSymbol(math, main, rel, "\u2223", "\\mid"); +defineSymbol(math, main, rel, "\u226a", "\\ll"); +defineSymbol(math, main, rel, "\u226b", "\\gg"); +defineSymbol(math, main, rel, "\u224d", "\\asymp"); +defineSymbol(math, main, rel, "\u2225", "\\parallel"); +defineSymbol(math, main, rel, "\u22c8", "\\bowtie"); +defineSymbol(math, main, rel, "\u2323", "\\smile"); +defineSymbol(math, main, rel, "\u2291", "\\sqsubseteq"); +defineSymbol(math, main, rel, "\u2292", "\\sqsupseteq"); +defineSymbol(math, main, rel, "\u2250", "\\doteq"); +defineSymbol(math, main, rel, "\u2322", "\\frown"); +defineSymbol(math, main, rel, "\u220b", "\\ni"); +defineSymbol(math, main, rel, "\u221d", "\\propto"); +defineSymbol(math, main, rel, "\u22a2", "\\vdash"); +defineSymbol(math, main, rel, "\u22a3", "\\dashv"); +defineSymbol(math, main, rel, "\u220b", "\\owns"); + +// Punctuation +defineSymbol(math, main, punct, "\u002e", "\\ldotp"); +defineSymbol(math, main, punct, "\u22c5", "\\cdotp"); + +// Misc Symbols +defineSymbol(math, main, textord, "\u0023", "\\#"); +defineSymbol(math, main, textord, "\u0026", "\\&"); +defineSymbol(math, main, textord, "\u2135", "\\aleph"); +defineSymbol(math, main, textord, "\u2200", "\\forall"); +defineSymbol(math, main, textord, "\u210f", "\\hbar"); +defineSymbol(math, main, textord, "\u2203", "\\exists"); +defineSymbol(math, main, textord, "\u2207", "\\nabla"); +defineSymbol(math, main, textord, "\u266d", "\\flat"); +defineSymbol(math, main, textord, "\u2113", "\\ell"); +defineSymbol(math, main, textord, "\u266e", "\\natural"); +defineSymbol(math, main, textord, "\u2663", "\\clubsuit"); +defineSymbol(math, main, textord, "\u2118", "\\wp"); +defineSymbol(math, main, textord, "\u266f", "\\sharp"); +defineSymbol(math, main, textord, "\u2662", "\\diamondsuit"); +defineSymbol(math, main, textord, "\u211c", "\\Re"); +defineSymbol(math, main, textord, "\u2661", "\\heartsuit"); +defineSymbol(math, main, textord, "\u2111", "\\Im"); +defineSymbol(math, main, textord, "\u2660", "\\spadesuit"); + +// Math and Text +defineSymbol(math, main, textord, "\u2020", "\\dag"); +defineSymbol(math, main, textord, "\u2021", "\\ddag"); + +// Large Delimiters +defineSymbol(math, main, close, "\u23b1", "\\rmoustache"); +defineSymbol(math, main, open, "\u23b0", "\\lmoustache"); +defineSymbol(math, main, close, "\u27ef", "\\rgroup"); +defineSymbol(math, main, open, "\u27ee", "\\lgroup"); + +// Binary Operators +defineSymbol(math, main, bin, "\u2213", "\\mp"); +defineSymbol(math, main, bin, "\u2296", "\\ominus"); +defineSymbol(math, main, bin, "\u228e", "\\uplus"); +defineSymbol(math, main, bin, "\u2293", "\\sqcap"); +defineSymbol(math, main, bin, "\u2217", "\\ast"); +defineSymbol(math, main, bin, "\u2294", "\\sqcup"); +defineSymbol(math, main, bin, "\u25ef", "\\bigcirc"); +defineSymbol(math, main, bin, "\u2219", "\\bullet"); +defineSymbol(math, main, bin, "\u2021", "\\ddagger"); +defineSymbol(math, main, bin, "\u2240", "\\wr"); +defineSymbol(math, main, bin, "\u2a3f", "\\amalg"); + +// Arrow Symbols +defineSymbol(math, main, rel, "\u27f5", "\\longleftarrow"); +defineSymbol(math, main, rel, "\u21d0", "\\Leftarrow"); +defineSymbol(math, main, rel, "\u27f8", "\\Longleftarrow"); +defineSymbol(math, main, rel, "\u27f6", "\\longrightarrow"); +defineSymbol(math, main, rel, "\u21d2", "\\Rightarrow"); +defineSymbol(math, main, rel, "\u27f9", "\\Longrightarrow"); +defineSymbol(math, main, rel, "\u2194", "\\leftrightarrow"); +defineSymbol(math, main, rel, "\u27f7", "\\longleftrightarrow"); +defineSymbol(math, main, rel, "\u21d4", "\\Leftrightarrow"); +defineSymbol(math, main, rel, "\u27fa", "\\Longleftrightarrow"); +defineSymbol(math, main, rel, "\u21a6", "\\mapsto"); +defineSymbol(math, main, rel, "\u27fc", "\\longmapsto"); +defineSymbol(math, main, rel, "\u2197", "\\nearrow"); +defineSymbol(math, main, rel, "\u21a9", "\\hookleftarrow"); +defineSymbol(math, main, rel, "\u21aa", "\\hookrightarrow"); +defineSymbol(math, main, rel, "\u2198", "\\searrow"); +defineSymbol(math, main, rel, "\u21bc", "\\leftharpoonup"); +defineSymbol(math, main, rel, "\u21c0", "\\rightharpoonup"); +defineSymbol(math, main, rel, "\u2199", "\\swarrow"); +defineSymbol(math, main, rel, "\u21bd", "\\leftharpoondown"); +defineSymbol(math, main, rel, "\u21c1", "\\rightharpoondown"); +defineSymbol(math, main, rel, "\u2196", "\\nwarrow"); +defineSymbol(math, main, rel, "\u21cc", "\\rightleftharpoons"); + +// AMS Negated Binary Relations +defineSymbol(math, ams, rel, "\u226e", "\\nless"); +defineSymbol(math, ams, rel, "\ue010", "\\nleqslant"); +defineSymbol(math, ams, rel, "\ue011", "\\nleqq"); +defineSymbol(math, ams, rel, "\u2a87", "\\lneq"); +defineSymbol(math, ams, rel, "\u2268", "\\lneqq"); +defineSymbol(math, ams, rel, "\ue00c", "\\lvertneqq"); +defineSymbol(math, ams, rel, "\u22e6", "\\lnsim"); +defineSymbol(math, ams, rel, "\u2a89", "\\lnapprox"); +defineSymbol(math, ams, rel, "\u2280", "\\nprec"); +defineSymbol(math, ams, rel, "\u22e0", "\\npreceq"); +defineSymbol(math, ams, rel, "\u22e8", "\\precnsim"); +defineSymbol(math, ams, rel, "\u2ab9", "\\precnapprox"); +defineSymbol(math, ams, rel, "\u2241", "\\nsim"); +defineSymbol(math, ams, rel, "\ue006", "\\nshortmid"); +defineSymbol(math, ams, rel, "\u2224", "\\nmid"); +defineSymbol(math, ams, rel, "\u22ac", "\\nvdash"); +defineSymbol(math, ams, rel, "\u22ad", "\\nvDash"); +defineSymbol(math, ams, rel, "\u22ea", "\\ntriangleleft"); +defineSymbol(math, ams, rel, "\u22ec", "\\ntrianglelefteq"); +defineSymbol(math, ams, rel, "\u228a", "\\subsetneq"); +defineSymbol(math, ams, rel, "\ue01a", "\\varsubsetneq"); +defineSymbol(math, ams, rel, "\u2acb", "\\subsetneqq"); +defineSymbol(math, ams, rel, "\ue017", "\\varsubsetneqq"); +defineSymbol(math, ams, rel, "\u226f", "\\ngtr"); +defineSymbol(math, ams, rel, "\ue00f", "\\ngeqslant"); +defineSymbol(math, ams, rel, "\ue00e", "\\ngeqq"); +defineSymbol(math, ams, rel, "\u2a88", "\\gneq"); +defineSymbol(math, ams, rel, "\u2269", "\\gneqq"); +defineSymbol(math, ams, rel, "\ue00d", "\\gvertneqq"); +defineSymbol(math, ams, rel, "\u22e7", "\\gnsim"); +defineSymbol(math, ams, rel, "\u2a8a", "\\gnapprox"); +defineSymbol(math, ams, rel, "\u2281", "\\nsucc"); +defineSymbol(math, ams, rel, "\u22e1", "\\nsucceq"); +defineSymbol(math, ams, rel, "\u22e9", "\\succnsim"); +defineSymbol(math, ams, rel, "\u2aba", "\\succnapprox"); +defineSymbol(math, ams, rel, "\u2246", "\\ncong"); +defineSymbol(math, ams, rel, "\ue007", "\\nshortparallel"); +defineSymbol(math, ams, rel, "\u2226", "\\nparallel"); +defineSymbol(math, ams, rel, "\u22af", "\\nVDash"); +defineSymbol(math, ams, rel, "\u22eb", "\\ntriangleright"); +defineSymbol(math, ams, rel, "\u22ed", "\\ntrianglerighteq"); +defineSymbol(math, ams, rel, "\ue018", "\\nsupseteqq"); +defineSymbol(math, ams, rel, "\u228b", "\\supsetneq"); +defineSymbol(math, ams, rel, "\ue01b", "\\varsupsetneq"); +defineSymbol(math, ams, rel, "\u2acc", "\\supsetneqq"); +defineSymbol(math, ams, rel, "\ue019", "\\varsupsetneqq"); +defineSymbol(math, ams, rel, "\u22ae", "\\nVdash"); +defineSymbol(math, ams, rel, "\u2ab5", "\\precneqq"); +defineSymbol(math, ams, rel, "\u2ab6", "\\succneqq"); +defineSymbol(math, ams, rel, "\ue016", "\\nsubseteqq"); +defineSymbol(math, ams, bin, "\u22b4", "\\unlhd"); +defineSymbol(math, ams, bin, "\u22b5", "\\unrhd"); + +// AMS Negated Arrows +defineSymbol(math, ams, rel, "\u219a", "\\nleftarrow"); +defineSymbol(math, ams, rel, "\u219b", "\\nrightarrow"); +defineSymbol(math, ams, rel, "\u21cd", "\\nLeftarrow"); +defineSymbol(math, ams, rel, "\u21cf", "\\nRightarrow"); +defineSymbol(math, ams, rel, "\u21ae", "\\nleftrightarrow"); +defineSymbol(math, ams, rel, "\u21ce", "\\nLeftrightarrow"); + +// AMS Misc +defineSymbol(math, ams, rel, "\u25b3", "\\vartriangle"); +defineSymbol(math, ams, textord, "\u210f", "\\hslash"); +defineSymbol(math, ams, textord, "\u25bd", "\\triangledown"); +defineSymbol(math, ams, textord, "\u25ca", "\\lozenge"); +defineSymbol(math, ams, textord, "\u24c8", "\\circledS"); +defineSymbol(math, ams, textord, "\u00ae", "\\circledR"); +defineSymbol(math, ams, textord, "\u2221", "\\measuredangle"); +defineSymbol(math, ams, textord, "\u2204", "\\nexists"); +defineSymbol(math, ams, textord, "\u2127", "\\mho"); +defineSymbol(math, ams, textord, "\u2132", "\\Finv"); +defineSymbol(math, ams, textord, "\u2141", "\\Game"); +defineSymbol(math, ams, textord, "\u006b", "\\Bbbk"); +defineSymbol(math, ams, textord, "\u2035", "\\backprime"); +defineSymbol(math, ams, textord, "\u25b2", "\\blacktriangle"); +defineSymbol(math, ams, textord, "\u25bc", "\\blacktriangledown"); +defineSymbol(math, ams, textord, "\u25a0", "\\blacksquare"); +defineSymbol(math, ams, textord, "\u29eb", "\\blacklozenge"); +defineSymbol(math, ams, textord, "\u2605", "\\bigstar"); +defineSymbol(math, ams, textord, "\u2222", "\\sphericalangle"); +defineSymbol(math, ams, textord, "\u2201", "\\complement"); +defineSymbol(math, ams, textord, "\u00f0", "\\eth"); +defineSymbol(math, ams, textord, "\u2571", "\\diagup"); +defineSymbol(math, ams, textord, "\u2572", "\\diagdown"); +defineSymbol(math, ams, textord, "\u25a1", "\\square"); +defineSymbol(math, ams, textord, "\u25a1", "\\Box"); +defineSymbol(math, ams, textord, "\u25ca", "\\Diamond"); +defineSymbol(math, ams, textord, "\u00a5", "\\yen"); +defineSymbol(math, ams, textord, "\u2713", "\\checkmark"); + +// AMS Hebrew +defineSymbol(math, ams, textord, "\u2136", "\\beth"); +defineSymbol(math, ams, textord, "\u2138", "\\daleth"); +defineSymbol(math, ams, textord, "\u2137", "\\gimel"); + +// AMS Greek +defineSymbol(math, ams, textord, "\u03dd", "\\digamma"); +defineSymbol(math, ams, textord, "\u03f0", "\\varkappa"); + +// AMS Delimiters +defineSymbol(math, ams, open, "\u250c", "\\ulcorner"); +defineSymbol(math, ams, close, "\u2510", "\\urcorner"); +defineSymbol(math, ams, open, "\u2514", "\\llcorner"); +defineSymbol(math, ams, close, "\u2518", "\\lrcorner"); + +// AMS Binary Relations +defineSymbol(math, ams, rel, "\u2266", "\\leqq"); +defineSymbol(math, ams, rel, "\u2a7d", "\\leqslant"); +defineSymbol(math, ams, rel, "\u2a95", "\\eqslantless"); +defineSymbol(math, ams, rel, "\u2272", "\\lesssim"); +defineSymbol(math, ams, rel, "\u2a85", "\\lessapprox"); +defineSymbol(math, ams, rel, "\u224a", "\\approxeq"); +defineSymbol(math, ams, bin, "\u22d6", "\\lessdot"); +defineSymbol(math, ams, rel, "\u22d8", "\\lll"); +defineSymbol(math, ams, rel, "\u2276", "\\lessgtr"); +defineSymbol(math, ams, rel, "\u22da", "\\lesseqgtr"); +defineSymbol(math, ams, rel, "\u2a8b", "\\lesseqqgtr"); +defineSymbol(math, ams, rel, "\u2251", "\\doteqdot"); +defineSymbol(math, ams, rel, "\u2253", "\\risingdotseq"); +defineSymbol(math, ams, rel, "\u2252", "\\fallingdotseq"); +defineSymbol(math, ams, rel, "\u223d", "\\backsim"); +defineSymbol(math, ams, rel, "\u22cd", "\\backsimeq"); +defineSymbol(math, ams, rel, "\u2ac5", "\\subseteqq"); +defineSymbol(math, ams, rel, "\u22d0", "\\Subset"); +defineSymbol(math, ams, rel, "\u228f", "\\sqsubset"); +defineSymbol(math, ams, rel, "\u227c", "\\preccurlyeq"); +defineSymbol(math, ams, rel, "\u22de", "\\curlyeqprec"); +defineSymbol(math, ams, rel, "\u227e", "\\precsim"); +defineSymbol(math, ams, rel, "\u2ab7", "\\precapprox"); +defineSymbol(math, ams, rel, "\u22b2", "\\vartriangleleft"); +defineSymbol(math, ams, rel, "\u22b4", "\\trianglelefteq"); +defineSymbol(math, ams, rel, "\u22a8", "\\vDash"); +defineSymbol(math, ams, rel, "\u22aa", "\\Vvdash"); +defineSymbol(math, ams, rel, "\u2323", "\\smallsmile"); +defineSymbol(math, ams, rel, "\u2322", "\\smallfrown"); +defineSymbol(math, ams, rel, "\u224f", "\\bumpeq"); +defineSymbol(math, ams, rel, "\u224e", "\\Bumpeq"); +defineSymbol(math, ams, rel, "\u2267", "\\geqq"); +defineSymbol(math, ams, rel, "\u2a7e", "\\geqslant"); +defineSymbol(math, ams, rel, "\u2a96", "\\eqslantgtr"); +defineSymbol(math, ams, rel, "\u2273", "\\gtrsim"); +defineSymbol(math, ams, rel, "\u2a86", "\\gtrapprox"); +defineSymbol(math, ams, bin, "\u22d7", "\\gtrdot"); +defineSymbol(math, ams, rel, "\u22d9", "\\ggg"); +defineSymbol(math, ams, rel, "\u2277", "\\gtrless"); +defineSymbol(math, ams, rel, "\u22db", "\\gtreqless"); +defineSymbol(math, ams, rel, "\u2a8c", "\\gtreqqless"); +defineSymbol(math, ams, rel, "\u2256", "\\eqcirc"); +defineSymbol(math, ams, rel, "\u2257", "\\circeq"); +defineSymbol(math, ams, rel, "\u225c", "\\triangleq"); +defineSymbol(math, ams, rel, "\u223c", "\\thicksim"); +defineSymbol(math, ams, rel, "\u2248", "\\thickapprox"); +defineSymbol(math, ams, rel, "\u2ac6", "\\supseteqq"); +defineSymbol(math, ams, rel, "\u22d1", "\\Supset"); +defineSymbol(math, ams, rel, "\u2290", "\\sqsupset"); +defineSymbol(math, ams, rel, "\u227d", "\\succcurlyeq"); +defineSymbol(math, ams, rel, "\u22df", "\\curlyeqsucc"); +defineSymbol(math, ams, rel, "\u227f", "\\succsim"); +defineSymbol(math, ams, rel, "\u2ab8", "\\succapprox"); +defineSymbol(math, ams, rel, "\u22b3", "\\vartriangleright"); +defineSymbol(math, ams, rel, "\u22b5", "\\trianglerighteq"); +defineSymbol(math, ams, rel, "\u22a9", "\\Vdash"); +defineSymbol(math, ams, rel, "\u2223", "\\shortmid"); +defineSymbol(math, ams, rel, "\u2225", "\\shortparallel"); +defineSymbol(math, ams, rel, "\u226c", "\\between"); +defineSymbol(math, ams, rel, "\u22d4", "\\pitchfork"); +defineSymbol(math, ams, rel, "\u221d", "\\varpropto"); +defineSymbol(math, ams, rel, "\u25c0", "\\blacktriangleleft"); +defineSymbol(math, ams, rel, "\u2234", "\\therefore"); +defineSymbol(math, ams, rel, "\u220d", "\\backepsilon"); +defineSymbol(math, ams, rel, "\u25b6", "\\blacktriangleright"); +defineSymbol(math, ams, rel, "\u2235", "\\because"); +defineSymbol(math, ams, rel, "\u22d8", "\\llless"); +defineSymbol(math, ams, rel, "\u22d9", "\\gggtr"); +defineSymbol(math, ams, bin, "\u22b2", "\\lhd"); +defineSymbol(math, ams, bin, "\u22b3", "\\rhd"); +defineSymbol(math, ams, rel, "\u2242", "\\eqsim"); +defineSymbol(math, main, rel, "\u22c8", "\\Join"); +defineSymbol(math, ams, rel, "\u2251", "\\Doteq"); + +// AMS Binary Operators +defineSymbol(math, ams, bin, "\u2214", "\\dotplus"); +defineSymbol(math, ams, bin, "\u2216", "\\smallsetminus"); +defineSymbol(math, ams, bin, "\u22d2", "\\Cap"); +defineSymbol(math, ams, bin, "\u22d3", "\\Cup"); +defineSymbol(math, ams, bin, "\u2a5e", "\\doublebarwedge"); +defineSymbol(math, ams, bin, "\u229f", "\\boxminus"); +defineSymbol(math, ams, bin, "\u229e", "\\boxplus"); +defineSymbol(math, ams, bin, "\u22c7", "\\divideontimes"); +defineSymbol(math, ams, bin, "\u22c9", "\\ltimes"); +defineSymbol(math, ams, bin, "\u22ca", "\\rtimes"); +defineSymbol(math, ams, bin, "\u22cb", "\\leftthreetimes"); +defineSymbol(math, ams, bin, "\u22cc", "\\rightthreetimes"); +defineSymbol(math, ams, bin, "\u22cf", "\\curlywedge"); +defineSymbol(math, ams, bin, "\u22ce", "\\curlyvee"); +defineSymbol(math, ams, bin, "\u229d", "\\circleddash"); +defineSymbol(math, ams, bin, "\u229b", "\\circledast"); +defineSymbol(math, ams, bin, "\u22c5", "\\centerdot"); +defineSymbol(math, ams, bin, "\u22ba", "\\intercal"); +defineSymbol(math, ams, bin, "\u22d2", "\\doublecap"); +defineSymbol(math, ams, bin, "\u22d3", "\\doublecup"); +defineSymbol(math, ams, bin, "\u22a0", "\\boxtimes"); + +// AMS Arrows +defineSymbol(math, ams, rel, "\u21e2", "\\dashrightarrow"); +defineSymbol(math, ams, rel, "\u21e0", "\\dashleftarrow"); +defineSymbol(math, ams, rel, "\u21c7", "\\leftleftarrows"); +defineSymbol(math, ams, rel, "\u21c6", "\\leftrightarrows"); +defineSymbol(math, ams, rel, "\u21da", "\\Lleftarrow"); +defineSymbol(math, ams, rel, "\u219e", "\\twoheadleftarrow"); +defineSymbol(math, ams, rel, "\u21a2", "\\leftarrowtail"); +defineSymbol(math, ams, rel, "\u21ab", "\\looparrowleft"); +defineSymbol(math, ams, rel, "\u21cb", "\\leftrightharpoons"); +defineSymbol(math, ams, rel, "\u21b6", "\\curvearrowleft"); +defineSymbol(math, ams, rel, "\u21ba", "\\circlearrowleft"); +defineSymbol(math, ams, rel, "\u21b0", "\\Lsh"); +defineSymbol(math, ams, rel, "\u21c8", "\\upuparrows"); +defineSymbol(math, ams, rel, "\u21bf", "\\upharpoonleft"); +defineSymbol(math, ams, rel, "\u21c3", "\\downharpoonleft"); +defineSymbol(math, ams, rel, "\u22b8", "\\multimap"); +defineSymbol(math, ams, rel, "\u21ad", "\\leftrightsquigarrow"); +defineSymbol(math, ams, rel, "\u21c9", "\\rightrightarrows"); +defineSymbol(math, ams, rel, "\u21c4", "\\rightleftarrows"); +defineSymbol(math, ams, rel, "\u21a0", "\\twoheadrightarrow"); +defineSymbol(math, ams, rel, "\u21a3", "\\rightarrowtail"); +defineSymbol(math, ams, rel, "\u21ac", "\\looparrowright"); +defineSymbol(math, ams, rel, "\u21b7", "\\curvearrowright"); +defineSymbol(math, ams, rel, "\u21bb", "\\circlearrowright"); +defineSymbol(math, ams, rel, "\u21b1", "\\Rsh"); +defineSymbol(math, ams, rel, "\u21ca", "\\downdownarrows"); +defineSymbol(math, ams, rel, "\u21be", "\\upharpoonright"); +defineSymbol(math, ams, rel, "\u21c2", "\\downharpoonright"); +defineSymbol(math, ams, rel, "\u21dd", "\\rightsquigarrow"); +defineSymbol(math, ams, rel, "\u21dd", "\\leadsto"); +defineSymbol(math, ams, rel, "\u21db", "\\Rrightarrow"); +defineSymbol(math, ams, rel, "\u21be", "\\restriction"); + +defineSymbol(math, main, textord, "\u2018", "`"); +defineSymbol(math, main, textord, "$", "\\$"); +defineSymbol(math, main, textord, "%", "\\%"); +defineSymbol(math, main, textord, "_", "\\_"); +defineSymbol(math, main, textord, "\u2220", "\\angle"); +defineSymbol(math, main, textord, "\u221e", "\\infty"); +defineSymbol(math, main, textord, "\u2032", "\\prime"); +defineSymbol(math, main, textord, "\u25b3", "\\triangle"); +defineSymbol(math, main, textord, "\u0393", "\\Gamma"); +defineSymbol(math, main, textord, "\u0394", "\\Delta"); +defineSymbol(math, main, textord, "\u0398", "\\Theta"); +defineSymbol(math, main, textord, "\u039b", "\\Lambda"); +defineSymbol(math, main, textord, "\u039e", "\\Xi"); +defineSymbol(math, main, textord, "\u03a0", "\\Pi"); +defineSymbol(math, main, textord, "\u03a3", "\\Sigma"); +defineSymbol(math, main, textord, "\u03a5", "\\Upsilon"); +defineSymbol(math, main, textord, "\u03a6", "\\Phi"); +defineSymbol(math, main, textord, "\u03a8", "\\Psi"); +defineSymbol(math, main, textord, "\u03a9", "\\Omega"); +defineSymbol(math, main, textord, "\u00ac", "\\neg"); +defineSymbol(math, main, textord, "\u00ac", "\\lnot"); +defineSymbol(math, main, textord, "\u22a4", "\\top"); +defineSymbol(math, main, textord, "\u22a5", "\\bot"); +defineSymbol(math, main, textord, "\u2205", "\\emptyset"); +defineSymbol(math, ams, textord, "\u2205", "\\varnothing"); +defineSymbol(math, main, mathord, "\u03b1", "\\alpha"); +defineSymbol(math, main, mathord, "\u03b2", "\\beta"); +defineSymbol(math, main, mathord, "\u03b3", "\\gamma"); +defineSymbol(math, main, mathord, "\u03b4", "\\delta"); +defineSymbol(math, main, mathord, "\u03f5", "\\epsilon"); +defineSymbol(math, main, mathord, "\u03b6", "\\zeta"); +defineSymbol(math, main, mathord, "\u03b7", "\\eta"); +defineSymbol(math, main, mathord, "\u03b8", "\\theta"); +defineSymbol(math, main, mathord, "\u03b9", "\\iota"); +defineSymbol(math, main, mathord, "\u03ba", "\\kappa"); +defineSymbol(math, main, mathord, "\u03bb", "\\lambda"); +defineSymbol(math, main, mathord, "\u03bc", "\\mu"); +defineSymbol(math, main, mathord, "\u03bd", "\\nu"); +defineSymbol(math, main, mathord, "\u03be", "\\xi"); +defineSymbol(math, main, mathord, "o", "\\omicron"); +defineSymbol(math, main, mathord, "\u03c0", "\\pi"); +defineSymbol(math, main, mathord, "\u03c1", "\\rho"); +defineSymbol(math, main, mathord, "\u03c3", "\\sigma"); +defineSymbol(math, main, mathord, "\u03c4", "\\tau"); +defineSymbol(math, main, mathord, "\u03c5", "\\upsilon"); +defineSymbol(math, main, mathord, "\u03d5", "\\phi"); +defineSymbol(math, main, mathord, "\u03c7", "\\chi"); +defineSymbol(math, main, mathord, "\u03c8", "\\psi"); +defineSymbol(math, main, mathord, "\u03c9", "\\omega"); +defineSymbol(math, main, mathord, "\u03b5", "\\varepsilon"); +defineSymbol(math, main, mathord, "\u03d1", "\\vartheta"); +defineSymbol(math, main, mathord, "\u03d6", "\\varpi"); +defineSymbol(math, main, mathord, "\u03f1", "\\varrho"); +defineSymbol(math, main, mathord, "\u03c2", "\\varsigma"); +defineSymbol(math, main, mathord, "\u03c6", "\\varphi"); +defineSymbol(math, main, bin, "\u2217", "*"); +defineSymbol(math, main, bin, "+", "+"); +defineSymbol(math, main, bin, "\u2212", "-"); +defineSymbol(math, main, bin, "\u22c5", "\\cdot"); +defineSymbol(math, main, bin, "\u2218", "\\circ"); +defineSymbol(math, main, bin, "\u00f7", "\\div"); +defineSymbol(math, main, bin, "\u00b1", "\\pm"); +defineSymbol(math, main, bin, "\u00d7", "\\times"); +defineSymbol(math, main, bin, "\u2229", "\\cap"); +defineSymbol(math, main, bin, "\u222a", "\\cup"); +defineSymbol(math, main, bin, "\u2216", "\\setminus"); +defineSymbol(math, main, bin, "\u2227", "\\land"); +defineSymbol(math, main, bin, "\u2228", "\\lor"); +defineSymbol(math, main, bin, "\u2227", "\\wedge"); +defineSymbol(math, main, bin, "\u2228", "\\vee"); +defineSymbol(math, main, textord, "\u221a", "\\surd"); +defineSymbol(math, main, open, "(", "("); +defineSymbol(math, main, open, "[", "["); +defineSymbol(math, main, open, "\u27e8", "\\langle"); +defineSymbol(math, main, open, "\u2223", "\\lvert"); +defineSymbol(math, main, open, "\u2225", "\\lVert"); +defineSymbol(math, main, close, ")", ")"); +defineSymbol(math, main, close, "]", "]"); +defineSymbol(math, main, close, "?", "?"); +defineSymbol(math, main, close, "!", "!"); +defineSymbol(math, main, close, "\u27e9", "\\rangle"); +defineSymbol(math, main, close, "\u2223", "\\rvert"); +defineSymbol(math, main, close, "\u2225", "\\rVert"); +defineSymbol(math, main, rel, "=", "="); +defineSymbol(math, main, rel, "<", "<"); +defineSymbol(math, main, rel, ">", ">"); +defineSymbol(math, main, rel, ":", ":"); +defineSymbol(math, main, rel, "\u2248", "\\approx"); +defineSymbol(math, main, rel, "\u2245", "\\cong"); +defineSymbol(math, main, rel, "\u2265", "\\ge"); +defineSymbol(math, main, rel, "\u2265", "\\geq"); +defineSymbol(math, main, rel, "\u2190", "\\gets"); +defineSymbol(math, main, rel, ">", "\\gt"); +defineSymbol(math, main, rel, "\u2208", "\\in"); +defineSymbol(math, main, rel, "\u2209", "\\notin"); +defineSymbol(math, main, rel, "\u2282", "\\subset"); +defineSymbol(math, main, rel, "\u2283", "\\supset"); +defineSymbol(math, main, rel, "\u2286", "\\subseteq"); +defineSymbol(math, main, rel, "\u2287", "\\supseteq"); +defineSymbol(math, ams, rel, "\u2288", "\\nsubseteq"); +defineSymbol(math, ams, rel, "\u2289", "\\nsupseteq"); +defineSymbol(math, main, rel, "\u22a8", "\\models"); +defineSymbol(math, main, rel, "\u2190", "\\leftarrow"); +defineSymbol(math, main, rel, "\u2264", "\\le"); +defineSymbol(math, main, rel, "\u2264", "\\leq"); +defineSymbol(math, main, rel, "<", "\\lt"); +defineSymbol(math, main, rel, "\u2260", "\\ne"); +defineSymbol(math, main, rel, "\u2260", "\\neq"); +defineSymbol(math, main, rel, "\u2192", "\\rightarrow"); +defineSymbol(math, main, rel, "\u2192", "\\to"); +defineSymbol(math, ams, rel, "\u2271", "\\ngeq"); +defineSymbol(math, ams, rel, "\u2270", "\\nleq"); +defineSymbol(math, main, spacing, null, "\\!"); +defineSymbol(math, main, spacing, "\u00a0", "\\ "); +defineSymbol(math, main, spacing, "\u00a0", "~"); +defineSymbol(math, main, spacing, null, "\\,"); +defineSymbol(math, main, spacing, null, "\\:"); +defineSymbol(math, main, spacing, null, "\\;"); +defineSymbol(math, main, spacing, null, "\\enspace"); +defineSymbol(math, main, spacing, null, "\\qquad"); +defineSymbol(math, main, spacing, null, "\\quad"); +defineSymbol(math, main, spacing, "\u00a0", "\\space"); +defineSymbol(math, main, punct, ",", ","); +defineSymbol(math, main, punct, ";", ";"); +defineSymbol(math, main, punct, ":", "\\colon"); +defineSymbol(math, ams, bin, "\u22bc", "\\barwedge"); +defineSymbol(math, ams, bin, "\u22bb", "\\veebar"); +defineSymbol(math, main, bin, "\u2299", "\\odot"); +defineSymbol(math, main, bin, "\u2295", "\\oplus"); +defineSymbol(math, main, bin, "\u2297", "\\otimes"); +defineSymbol(math, main, textord, "\u2202", "\\partial"); +defineSymbol(math, main, bin, "\u2298", "\\oslash"); +defineSymbol(math, ams, bin, "\u229a", "\\circledcirc"); +defineSymbol(math, ams, bin, "\u22a1", "\\boxdot"); +defineSymbol(math, main, bin, "\u25b3", "\\bigtriangleup"); +defineSymbol(math, main, bin, "\u25bd", "\\bigtriangledown"); +defineSymbol(math, main, bin, "\u2020", "\\dagger"); +defineSymbol(math, main, bin, "\u22c4", "\\diamond"); +defineSymbol(math, main, bin, "\u22c6", "\\star"); +defineSymbol(math, main, bin, "\u25c3", "\\triangleleft"); +defineSymbol(math, main, bin, "\u25b9", "\\triangleright"); +defineSymbol(math, main, open, "{", "\\{"); +defineSymbol(math, main, close, "}", "\\}"); +defineSymbol(math, main, open, "{", "\\lbrace"); +defineSymbol(math, main, close, "}", "\\rbrace"); +defineSymbol(math, main, open, "[", "\\lbrack"); +defineSymbol(math, main, close, "]", "\\rbrack"); +defineSymbol(math, main, open, "\u230a", "\\lfloor"); +defineSymbol(math, main, close, "\u230b", "\\rfloor"); +defineSymbol(math, main, open, "\u2308", "\\lceil"); +defineSymbol(math, main, close, "\u2309", "\\rceil"); +defineSymbol(math, main, textord, "\\", "\\backslash"); +defineSymbol(math, main, textord, "\u2223", "|"); +defineSymbol(math, main, textord, "\u2223", "\\vert"); +defineSymbol(math, main, textord, "\u2225", "\\|"); +defineSymbol(math, main, textord, "\u2225", "\\Vert"); +defineSymbol(math, main, rel, "\u2191", "\\uparrow"); +defineSymbol(math, main, rel, "\u21d1", "\\Uparrow"); +defineSymbol(math, main, rel, "\u2193", "\\downarrow"); +defineSymbol(math, main, rel, "\u21d3", "\\Downarrow"); +defineSymbol(math, main, rel, "\u2195", "\\updownarrow"); +defineSymbol(math, main, rel, "\u21d5", "\\Updownarrow"); +defineSymbol(math, math, op, "\u2210", "\\coprod"); +defineSymbol(math, math, op, "\u22c1", "\\bigvee"); +defineSymbol(math, math, op, "\u22c0", "\\bigwedge"); +defineSymbol(math, math, op, "\u2a04", "\\biguplus"); +defineSymbol(math, math, op, "\u22c2", "\\bigcap"); +defineSymbol(math, math, op, "\u22c3", "\\bigcup"); +defineSymbol(math, math, op, "\u222b", "\\int"); +defineSymbol(math, math, op, "\u222b", "\\intop"); +defineSymbol(math, math, op, "\u222c", "\\iint"); +defineSymbol(math, math, op, "\u222d", "\\iiint"); +defineSymbol(math, math, op, "\u220f", "\\prod"); +defineSymbol(math, math, op, "\u2211", "\\sum"); +defineSymbol(math, math, op, "\u2a02", "\\bigotimes"); +defineSymbol(math, math, op, "\u2a01", "\\bigoplus"); +defineSymbol(math, math, op, "\u2a00", "\\bigodot"); +defineSymbol(math, math, op, "\u222e", "\\oint"); +defineSymbol(math, math, op, "\u2a06", "\\bigsqcup"); +defineSymbol(math, math, op, "\u222b", "\\smallint"); +defineSymbol(math, main, inner, "\u2026", "\\ldots"); +defineSymbol(math, main, inner, "\u22ef", "\\cdots"); +defineSymbol(math, main, inner, "\u22f1", "\\ddots"); +defineSymbol(math, main, textord, "\u22ee", "\\vdots"); +defineSymbol(math, main, accent, "\u00b4", "\\acute"); +defineSymbol(math, main, accent, "\u0060", "\\grave"); +defineSymbol(math, main, accent, "\u00a8", "\\ddot"); +defineSymbol(math, main, accent, "\u007e", "\\tilde"); +defineSymbol(math, main, accent, "\u00af", "\\bar"); +defineSymbol(math, main, accent, "\u02d8", "\\breve"); +defineSymbol(math, main, accent, "\u02c7", "\\check"); +defineSymbol(math, main, accent, "\u005e", "\\hat"); +defineSymbol(math, main, accent, "\u20d7", "\\vec"); +defineSymbol(math, main, accent, "\u02d9", "\\dot"); +defineSymbol(math, main, mathord, "\u0131", "\\imath"); +defineSymbol(math, main, mathord, "\u0237", "\\jmath"); + +defineSymbol(text, main, textord, "\u2013", "--"); +defineSymbol(text, main, textord, "\u2014", "---"); +defineSymbol(text, main, textord, "\u2018", "`"); +defineSymbol(text, main, textord, "\u2019", "'"); +defineSymbol(text, main, textord, "\u201c", "``"); +defineSymbol(text, main, textord, "\u201d", "''"); +defineSymbol(math, main, textord, "\u00b0", "\\degree"); +defineSymbol(text, main, textord, "\u00b0", "\\degree"); +defineSymbol(math, main, mathord, "\u00a3", "\\pounds"); +defineSymbol(math, ams, textord, "\u2720", "\\maltese"); +defineSymbol(text, ams, textord, "\u2720", "\\maltese"); + +defineSymbol(text, main, spacing, "\u00a0", "\\ "); +defineSymbol(text, main, spacing, "\u00a0", " "); +defineSymbol(text, main, spacing, "\u00a0", "~"); + +// There are lots of symbols which are the same, so we add them in afterwards. +var i; +var ch; + +// All of these are textords in math mode +var mathTextSymbols = "0123456789/@.\""; +for (i = 0; i < mathTextSymbols.length; i++) { + ch = mathTextSymbols.charAt(i); + defineSymbol(math, main, textord, ch, ch); +} + +// All of these are textords in text mode +var textSymbols = "0123456789!@*()-=+[]\";:?/.,"; +for (i = 0; i < textSymbols.length; i++) { + ch = textSymbols.charAt(i); + defineSymbol(text, main, textord, ch, ch); +} + +// All of these are textords in text mode, and mathords in math mode +var letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; +for (i = 0; i < letters.length; i++) { + ch = letters.charAt(i); + defineSymbol(math, main, mathord, ch, ch); + defineSymbol(text, main, textord, ch, ch); +} + +// Latin-1 letters +for (i = 0x00C0; i <= 0x00D6; i++) { + ch = String.fromCharCode(i); + defineSymbol(text, main, textord, ch, ch); +} + +for (i = 0x00D8; i <= 0x00F6; i++) { + ch = String.fromCharCode(i); + defineSymbol(text, main, textord, ch, ch); +} + +for (i = 0x00F8; i <= 0x00FF; i++) { + ch = String.fromCharCode(i); + defineSymbol(text, main, textord, ch, ch); +} + +// Cyrillic +for (i = 0x0410; i <= 0x044F; i++) { + ch = String.fromCharCode(i); + defineSymbol(text, main, textord, ch, ch); +} + +},{}],24:[function(require,module,exports){ +var hangulRegex = /[\uAC00-\uD7AF]/; + +// This regex combines +// - Hiragana: [\u3040-\u309F] +// - Katakana: [\u30A0-\u30FF] +// - CJK ideograms: [\u4E00-\u9FAF] +// - Hangul syllables: [\uAC00-\uD7AF] +// Notably missing are halfwidth Katakana and Romanji glyphs. +var cjkRegex = + /[\u3040-\u309F]|[\u30A0-\u30FF]|[\u4E00-\u9FAF]|[\uAC00-\uD7AF]/; + +module.exports = { + cjkRegex: cjkRegex, + hangulRegex: hangulRegex, +}; + +},{}],25:[function(require,module,exports){ +/** + * This file contains a list of utility functions which are useful in other + * files. + */ + +/** + * Provide an `indexOf` function which works in IE8, but defers to native if + * possible. + */ +var nativeIndexOf = Array.prototype.indexOf; +var indexOf = function(list, elem) { + if (list == null) { + return -1; + } + if (nativeIndexOf && list.indexOf === nativeIndexOf) { + return list.indexOf(elem); + } + var i = 0; + var l = list.length; + for (; i < l; i++) { + if (list[i] === elem) { + return i; + } + } + return -1; +}; + +/** + * Return whether an element is contained in a list + */ +var contains = function(list, elem) { + return indexOf(list, elem) !== -1; +}; + +/** + * Provide a default value if a setting is undefined + */ +var deflt = function(setting, defaultIfUndefined) { + return setting === undefined ? defaultIfUndefined : setting; +}; + +// hyphenate and escape adapted from Facebook's React under Apache 2 license + +var uppercase = /([A-Z])/g; +var hyphenate = function(str) { + return str.replace(uppercase, "-$1").toLowerCase(); +}; + +var ESCAPE_LOOKUP = { + "&": "&", + ">": ">", + "<": "<", + "\"": """, + "'": "'", +}; + +var ESCAPE_REGEX = /[&><"']/g; + +function escaper(match) { + return ESCAPE_LOOKUP[match]; +} + +/** + * Escapes text to prevent scripting attacks. + * + * @param {*} text Text value to escape. + * @return {string} An escaped string. + */ +function escape(text) { + return ("" + text).replace(ESCAPE_REGEX, escaper); +} + +/** + * A function to set the text content of a DOM element in all supported + * browsers. Note that we don't define this if there is no document. + */ +var setTextContent; +if (typeof document !== "undefined") { + var testNode = document.createElement("span"); + if ("textContent" in testNode) { + setTextContent = function(node, text) { + node.textContent = text; + }; + } else { + setTextContent = function(node, text) { + node.innerText = text; + }; + } +} + +/** + * A function to clear a node. + */ +function clearNode(node) { + setTextContent(node, ""); +} + +module.exports = { + contains: contains, + deflt: deflt, + escape: escape, + hyphenate: hyphenate, + indexOf: indexOf, + setTextContent: setTextContent, + clearNode: clearNode, +}; + +},{}]},{},[1])(1) +}); \ No newline at end of file diff --git a/vendor/assets/stylesheets/katex.css b/vendor/assets/stylesheets/katex.css new file mode 100644 index 00000000000..e684d697072 --- /dev/null +++ b/vendor/assets/stylesheets/katex.css @@ -0,0 +1,934 @@ +@font-face { + font-family: 'KaTeX_AMS'; + src: url('KaTeX_AMS-Regular.eot'); + src: url('KaTeX_AMS-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_AMS-Regular.woff2') format('woff2'), url('KaTeX_AMS-Regular.woff') format('woff'), url('KaTeX_AMS-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Caligraphic'; + src: url('KaTeX_Caligraphic-Bold.eot'); + src: url('KaTeX_Caligraphic-Bold.eot#iefix') format('embedded-opentype'), url('KaTeX_Caligraphic-Bold.woff2') format('woff2'), url('KaTeX_Caligraphic-Bold.woff') format('woff'), url('KaTeX_Caligraphic-Bold.ttf') format('truetype'); + font-weight: bold; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Caligraphic'; + src: url('KaTeX_Caligraphic-Regular.eot'); + src: url('KaTeX_Caligraphic-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Caligraphic-Regular.woff2') format('woff2'), url('KaTeX_Caligraphic-Regular.woff') format('woff'), url('KaTeX_Caligraphic-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Fraktur'; + src: url('KaTeX_Fraktur-Bold.eot'); + src: url('KaTeX_Fraktur-Bold.eot#iefix') format('embedded-opentype'), url('KaTeX_Fraktur-Bold.woff2') format('woff2'), url('KaTeX_Fraktur-Bold.woff') format('woff'), url('KaTeX_Fraktur-Bold.ttf') format('truetype'); + font-weight: bold; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Fraktur'; + src: url('KaTeX_Fraktur-Regular.eot'); + src: url('KaTeX_Fraktur-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Fraktur-Regular.woff2') format('woff2'), url('KaTeX_Fraktur-Regular.woff') format('woff'), url('KaTeX_Fraktur-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Main'; + src: url('KaTeX_Main-Bold.eot'); + src: url('KaTeX_Main-Bold.eot#iefix') format('embedded-opentype'), url('KaTeX_Main-Bold.woff2') format('woff2'), url('KaTeX_Main-Bold.woff') format('woff'), url('KaTeX_Main-Bold.ttf') format('truetype'); + font-weight: bold; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Main'; + src: url('KaTeX_Main-Italic.eot'); + src: url('KaTeX_Main-Italic.eot#iefix') format('embedded-opentype'), url('KaTeX_Main-Italic.woff2') format('woff2'), url('KaTeX_Main-Italic.woff') format('woff'), url('KaTeX_Main-Italic.ttf') format('truetype'); + font-weight: normal; + font-style: italic; +} +@font-face { + font-family: 'KaTeX_Main'; + src: url('KaTeX_Main-Regular.eot'); + src: url('KaTeX_Main-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Main-Regular.woff2') format('woff2'), url('KaTeX_Main-Regular.woff') format('woff'), url('KaTeX_Main-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Math'; + src: url('KaTeX_Math-Italic.eot'); + src: url('KaTeX_Math-Italic.eot#iefix') format('embedded-opentype'), url('KaTeX_Math-Italic.woff2') format('woff2'), url('KaTeX_Math-Italic.woff') format('woff'), url('KaTeX_Math-Italic.ttf') format('truetype'); + font-weight: normal; + font-style: italic; +} +@font-face { + font-family: 'KaTeX_SansSerif'; + src: url('KaTeX_SansSerif-Regular.eot'); + src: url('KaTeX_SansSerif-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_SansSerif-Regular.woff2') format('woff2'), url('KaTeX_SansSerif-Regular.woff') format('woff'), url('KaTeX_SansSerif-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Script'; + src: url('KaTeX_Script-Regular.eot'); + src: url('KaTeX_Script-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Script-Regular.woff2') format('woff2'), url('KaTeX_Script-Regular.woff') format('woff'), url('KaTeX_Script-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Size1'; + src: url('KaTeX_Size1-Regular.eot'); + src: url('KaTeX_Size1-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size1-Regular.woff2') format('woff2'), url('KaTeX_Size1-Regular.woff') format('woff'), url('KaTeX_Size1-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Size2'; + src: url('KaTeX_Size2-Regular.eot'); + src: url('KaTeX_Size2-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size2-Regular.woff2') format('woff2'), url('KaTeX_Size2-Regular.woff') format('woff'), url('KaTeX_Size2-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Size3'; + src: url('KaTeX_Size3-Regular.eot'); + src: url('KaTeX_Size3-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size3-Regular.woff2') format('woff2'), url('KaTeX_Size3-Regular.woff') format('woff'), url('KaTeX_Size3-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Size4'; + src: url('KaTeX_Size4-Regular.eot'); + src: url('KaTeX_Size4-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size4-Regular.woff2') format('woff2'), url('KaTeX_Size4-Regular.woff') format('woff'), url('KaTeX_Size4-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Typewriter'; + src: url('KaTeX_Typewriter-Regular.eot'); + src: url('KaTeX_Typewriter-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Typewriter-Regular.woff2') format('woff2'), url('KaTeX_Typewriter-Regular.woff') format('woff'), url('KaTeX_Typewriter-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} +.katex-display { + display: block; + margin: 1em 0; + text-align: center; +} +.katex-display > .katex { + display: inline-block; + text-align: initial; +} +.katex { + font: normal 1.21em KaTeX_Main, Times New Roman, serif; + line-height: 1.2; + white-space: nowrap; + text-indent: 0; +} +.katex .katex-html { + display: inline-block; +} +.katex .katex-mathml { + position: absolute; + clip: rect(1px, 1px, 1px, 1px); + padding: 0; + border: 0; + height: 1px; + width: 1px; + overflow: hidden; +} +.katex .base { + display: inline-block; +} +.katex .strut { + display: inline-block; +} +.katex .mathit { + font-family: KaTeX_Math; + font-style: italic; +} +.katex .mathbf { + font-family: KaTeX_Main; + font-weight: bold; +} +.katex .amsrm { + font-family: KaTeX_AMS; +} +.katex .mathbb { + font-family: KaTeX_AMS; +} +.katex .mathcal { + font-family: KaTeX_Caligraphic; +} +.katex .mathfrak { + font-family: KaTeX_Fraktur; +} +.katex .mathtt { + font-family: KaTeX_Typewriter; +} +.katex .mathscr { + font-family: KaTeX_Script; +} +.katex .mathsf { + font-family: KaTeX_SansSerif; +} +.katex .mainit { + font-family: KaTeX_Main; + font-style: italic; +} +.katex .textstyle > .mord + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .mord + .mbin { + margin-left: 0.22222em; +} +.katex .textstyle > .mord + .mrel { + margin-left: 0.27778em; +} +.katex .textstyle > .mord + .minner { + margin-left: 0.16667em; +} +.katex .textstyle > .mop + .mord { + margin-left: 0.16667em; +} +.katex .textstyle > .mop + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .mop + .mrel { + margin-left: 0.27778em; +} +.katex .textstyle > .mop + .minner { + margin-left: 0.16667em; +} +.katex .textstyle > .mbin + .mord { + margin-left: 0.22222em; +} +.katex .textstyle > .mbin + .mop { + margin-left: 0.22222em; +} +.katex .textstyle > .mbin + .mopen { + margin-left: 0.22222em; +} +.katex .textstyle > .mbin + .minner { + margin-left: 0.22222em; +} +.katex .textstyle > .mrel + .mord { + margin-left: 0.27778em; +} +.katex .textstyle > .mrel + .mop { + margin-left: 0.27778em; +} +.katex .textstyle > .mrel + .mopen { + margin-left: 0.27778em; +} +.katex .textstyle > .mrel + .minner { + margin-left: 0.27778em; +} +.katex .textstyle > .mclose + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .mclose + .mbin { + margin-left: 0.22222em; +} +.katex .textstyle > .mclose + .mrel { + margin-left: 0.27778em; +} +.katex .textstyle > .mclose + .minner { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mord { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mrel { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mopen { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mclose { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mpunct { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .minner { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .mord { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .mbin { + margin-left: 0.22222em; +} +.katex .textstyle > .minner + .mrel { + margin-left: 0.27778em; +} +.katex .textstyle > .minner + .mopen { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .mpunct { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .minner { + margin-left: 0.16667em; +} +.katex .mord + .mop { + margin-left: 0.16667em; +} +.katex .mop + .mord { + margin-left: 0.16667em; +} +.katex .mop + .mop { + margin-left: 0.16667em; +} +.katex .mclose + .mop { + margin-left: 0.16667em; +} +.katex .minner + .mop { + margin-left: 0.16667em; +} +.katex .reset-textstyle.textstyle { + font-size: 1em; +} +.katex .reset-textstyle.scriptstyle { + font-size: 0.7em; +} +.katex .reset-textstyle.scriptscriptstyle { + font-size: 0.5em; +} +.katex .reset-scriptstyle.textstyle { + font-size: 1.42857em; +} +.katex .reset-scriptstyle.scriptstyle { + font-size: 1em; +} +.katex .reset-scriptstyle.scriptscriptstyle { + font-size: 0.71429em; +} +.katex .reset-scriptscriptstyle.textstyle { + font-size: 2em; +} +.katex .reset-scriptscriptstyle.scriptstyle { + font-size: 1.4em; +} +.katex .reset-scriptscriptstyle.scriptscriptstyle { + font-size: 1em; +} +.katex .style-wrap { + position: relative; +} +.katex .vlist { + display: inline-block; +} +.katex .vlist > span { + display: block; + height: 0; + position: relative; +} +.katex .vlist > span > span { + display: inline-block; +} +.katex .vlist .baseline-fix { + display: inline-table; + table-layout: fixed; +} +.katex .msupsub { + text-align: left; +} +.katex .mfrac > span > span { + text-align: center; +} +.katex .mfrac .frac-line { + width: 100%; +} +.katex .mfrac .frac-line:before { + border-bottom-style: solid; + border-bottom-width: 1px; + content: ""; + display: block; +} +.katex .mfrac .frac-line:after { + border-bottom-style: solid; + border-bottom-width: 0.04em; + content: ""; + display: block; + margin-top: -1px; +} +.katex .mspace { + display: inline-block; +} +.katex .mspace.negativethinspace { + margin-left: -0.16667em; +} +.katex .mspace.thinspace { + width: 0.16667em; +} +.katex .mspace.mediumspace { + width: 0.22222em; +} +.katex .mspace.thickspace { + width: 0.27778em; +} +.katex .mspace.enspace { + width: 0.5em; +} +.katex .mspace.quad { + width: 1em; +} +.katex .mspace.qquad { + width: 2em; +} +.katex .llap, +.katex .rlap { + width: 0; + position: relative; +} +.katex .llap > .inner, +.katex .rlap > .inner { + position: absolute; +} +.katex .llap > .fix, +.katex .rlap > .fix { + display: inline-block; +} +.katex .llap > .inner { + right: 0; +} +.katex .rlap > .inner { + left: 0; +} +.katex .katex-logo .a { + font-size: 0.75em; + margin-left: -0.32em; + position: relative; + top: -0.2em; +} +.katex .katex-logo .t { + margin-left: -0.23em; +} +.katex .katex-logo .e { + margin-left: -0.1667em; + position: relative; + top: 0.2155em; +} +.katex .katex-logo .x { + margin-left: -0.125em; +} +.katex .rule { + display: inline-block; + border: solid 0; + position: relative; +} +.katex .overline .overline-line, +.katex .underline .underline-line { + width: 100%; +} +.katex .overline .overline-line:before, +.katex .underline .underline-line:before { + border-bottom-style: solid; + border-bottom-width: 1px; + content: ""; + display: block; +} +.katex .overline .overline-line:after, +.katex .underline .underline-line:after { + border-bottom-style: solid; + border-bottom-width: 0.04em; + content: ""; + display: block; + margin-top: -1px; +} +.katex .sqrt > .sqrt-sign { + position: relative; +} +.katex .sqrt .sqrt-line { + width: 100%; +} +.katex .sqrt .sqrt-line:before { + border-bottom-style: solid; + border-bottom-width: 1px; + content: ""; + display: block; +} +.katex .sqrt .sqrt-line:after { + border-bottom-style: solid; + border-bottom-width: 0.04em; + content: ""; + display: block; + margin-top: -1px; +} +.katex .sqrt > .root { + margin-left: 0.27777778em; + margin-right: -0.55555556em; +} +.katex .sizing, +.katex .fontsize-ensurer { + display: inline-block; +} +.katex .sizing.reset-size1.size1, +.katex .fontsize-ensurer.reset-size1.size1 { + font-size: 1em; +} +.katex .sizing.reset-size1.size2, +.katex .fontsize-ensurer.reset-size1.size2 { + font-size: 1.4em; +} +.katex .sizing.reset-size1.size3, +.katex .fontsize-ensurer.reset-size1.size3 { + font-size: 1.6em; +} +.katex .sizing.reset-size1.size4, +.katex .fontsize-ensurer.reset-size1.size4 { + font-size: 1.8em; +} +.katex .sizing.reset-size1.size5, +.katex .fontsize-ensurer.reset-size1.size5 { + font-size: 2em; +} +.katex .sizing.reset-size1.size6, +.katex .fontsize-ensurer.reset-size1.size6 { + font-size: 2.4em; +} +.katex .sizing.reset-size1.size7, +.katex .fontsize-ensurer.reset-size1.size7 { + font-size: 2.88em; +} +.katex .sizing.reset-size1.size8, +.katex .fontsize-ensurer.reset-size1.size8 { + font-size: 3.46em; +} +.katex .sizing.reset-size1.size9, +.katex .fontsize-ensurer.reset-size1.size9 { + font-size: 4.14em; +} +.katex .sizing.reset-size1.size10, +.katex .fontsize-ensurer.reset-size1.size10 { + font-size: 4.98em; +} +.katex .sizing.reset-size2.size1, +.katex .fontsize-ensurer.reset-size2.size1 { + font-size: 0.71428571em; +} +.katex .sizing.reset-size2.size2, +.katex .fontsize-ensurer.reset-size2.size2 { + font-size: 1em; +} +.katex .sizing.reset-size2.size3, +.katex .fontsize-ensurer.reset-size2.size3 { + font-size: 1.14285714em; +} +.katex .sizing.reset-size2.size4, +.katex .fontsize-ensurer.reset-size2.size4 { + font-size: 1.28571429em; +} +.katex .sizing.reset-size2.size5, +.katex .fontsize-ensurer.reset-size2.size5 { + font-size: 1.42857143em; +} +.katex .sizing.reset-size2.size6, +.katex .fontsize-ensurer.reset-size2.size6 { + font-size: 1.71428571em; +} +.katex .sizing.reset-size2.size7, +.katex .fontsize-ensurer.reset-size2.size7 { + font-size: 2.05714286em; +} +.katex .sizing.reset-size2.size8, +.katex .fontsize-ensurer.reset-size2.size8 { + font-size: 2.47142857em; +} +.katex .sizing.reset-size2.size9, +.katex .fontsize-ensurer.reset-size2.size9 { + font-size: 2.95714286em; +} +.katex .sizing.reset-size2.size10, +.katex .fontsize-ensurer.reset-size2.size10 { + font-size: 3.55714286em; +} +.katex .sizing.reset-size3.size1, +.katex .fontsize-ensurer.reset-size3.size1 { + font-size: 0.625em; +} +.katex .sizing.reset-size3.size2, +.katex .fontsize-ensurer.reset-size3.size2 { + font-size: 0.875em; +} +.katex .sizing.reset-size3.size3, +.katex .fontsize-ensurer.reset-size3.size3 { + font-size: 1em; +} +.katex .sizing.reset-size3.size4, +.katex .fontsize-ensurer.reset-size3.size4 { + font-size: 1.125em; +} +.katex .sizing.reset-size3.size5, +.katex .fontsize-ensurer.reset-size3.size5 { + font-size: 1.25em; +} +.katex .sizing.reset-size3.size6, +.katex .fontsize-ensurer.reset-size3.size6 { + font-size: 1.5em; +} +.katex .sizing.reset-size3.size7, +.katex .fontsize-ensurer.reset-size3.size7 { + font-size: 1.8em; +} +.katex .sizing.reset-size3.size8, +.katex .fontsize-ensurer.reset-size3.size8 { + font-size: 2.1625em; +} +.katex .sizing.reset-size3.size9, +.katex .fontsize-ensurer.reset-size3.size9 { + font-size: 2.5875em; +} +.katex .sizing.reset-size3.size10, +.katex .fontsize-ensurer.reset-size3.size10 { + font-size: 3.1125em; +} +.katex .sizing.reset-size4.size1, +.katex .fontsize-ensurer.reset-size4.size1 { + font-size: 0.55555556em; +} +.katex .sizing.reset-size4.size2, +.katex .fontsize-ensurer.reset-size4.size2 { + font-size: 0.77777778em; +} +.katex .sizing.reset-size4.size3, +.katex .fontsize-ensurer.reset-size4.size3 { + font-size: 0.88888889em; +} +.katex .sizing.reset-size4.size4, +.katex .fontsize-ensurer.reset-size4.size4 { + font-size: 1em; +} +.katex .sizing.reset-size4.size5, +.katex .fontsize-ensurer.reset-size4.size5 { + font-size: 1.11111111em; +} +.katex .sizing.reset-size4.size6, +.katex .fontsize-ensurer.reset-size4.size6 { + font-size: 1.33333333em; +} +.katex .sizing.reset-size4.size7, +.katex .fontsize-ensurer.reset-size4.size7 { + font-size: 1.6em; +} +.katex .sizing.reset-size4.size8, +.katex .fontsize-ensurer.reset-size4.size8 { + font-size: 1.92222222em; +} +.katex .sizing.reset-size4.size9, +.katex .fontsize-ensurer.reset-size4.size9 { + font-size: 2.3em; +} +.katex .sizing.reset-size4.size10, +.katex .fontsize-ensurer.reset-size4.size10 { + font-size: 2.76666667em; +} +.katex .sizing.reset-size5.size1, +.katex .fontsize-ensurer.reset-size5.size1 { + font-size: 0.5em; +} +.katex .sizing.reset-size5.size2, +.katex .fontsize-ensurer.reset-size5.size2 { + font-size: 0.7em; +} +.katex .sizing.reset-size5.size3, +.katex .fontsize-ensurer.reset-size5.size3 { + font-size: 0.8em; +} +.katex .sizing.reset-size5.size4, +.katex .fontsize-ensurer.reset-size5.size4 { + font-size: 0.9em; +} +.katex .sizing.reset-size5.size5, +.katex .fontsize-ensurer.reset-size5.size5 { + font-size: 1em; +} +.katex .sizing.reset-size5.size6, +.katex .fontsize-ensurer.reset-size5.size6 { + font-size: 1.2em; +} +.katex .sizing.reset-size5.size7, +.katex .fontsize-ensurer.reset-size5.size7 { + font-size: 1.44em; +} +.katex .sizing.reset-size5.size8, +.katex .fontsize-ensurer.reset-size5.size8 { + font-size: 1.73em; +} +.katex .sizing.reset-size5.size9, +.katex .fontsize-ensurer.reset-size5.size9 { + font-size: 2.07em; +} +.katex .sizing.reset-size5.size10, +.katex .fontsize-ensurer.reset-size5.size10 { + font-size: 2.49em; +} +.katex .sizing.reset-size6.size1, +.katex .fontsize-ensurer.reset-size6.size1 { + font-size: 0.41666667em; +} +.katex .sizing.reset-size6.size2, +.katex .fontsize-ensurer.reset-size6.size2 { + font-size: 0.58333333em; +} +.katex .sizing.reset-size6.size3, +.katex .fontsize-ensurer.reset-size6.size3 { + font-size: 0.66666667em; +} +.katex .sizing.reset-size6.size4, +.katex .fontsize-ensurer.reset-size6.size4 { + font-size: 0.75em; +} +.katex .sizing.reset-size6.size5, +.katex .fontsize-ensurer.reset-size6.size5 { + font-size: 0.83333333em; +} +.katex .sizing.reset-size6.size6, +.katex .fontsize-ensurer.reset-size6.size6 { + font-size: 1em; +} +.katex .sizing.reset-size6.size7, +.katex .fontsize-ensurer.reset-size6.size7 { + font-size: 1.2em; +} +.katex .sizing.reset-size6.size8, +.katex .fontsize-ensurer.reset-size6.size8 { + font-size: 1.44166667em; +} +.katex .sizing.reset-size6.size9, +.katex .fontsize-ensurer.reset-size6.size9 { + font-size: 1.725em; +} +.katex .sizing.reset-size6.size10, +.katex .fontsize-ensurer.reset-size6.size10 { + font-size: 2.075em; +} +.katex .sizing.reset-size7.size1, +.katex .fontsize-ensurer.reset-size7.size1 { + font-size: 0.34722222em; +} +.katex .sizing.reset-size7.size2, +.katex .fontsize-ensurer.reset-size7.size2 { + font-size: 0.48611111em; +} +.katex .sizing.reset-size7.size3, +.katex .fontsize-ensurer.reset-size7.size3 { + font-size: 0.55555556em; +} +.katex .sizing.reset-size7.size4, +.katex .fontsize-ensurer.reset-size7.size4 { + font-size: 0.625em; +} +.katex .sizing.reset-size7.size5, +.katex .fontsize-ensurer.reset-size7.size5 { + font-size: 0.69444444em; +} +.katex .sizing.reset-size7.size6, +.katex .fontsize-ensurer.reset-size7.size6 { + font-size: 0.83333333em; +} +.katex .sizing.reset-size7.size7, +.katex .fontsize-ensurer.reset-size7.size7 { + font-size: 1em; +} +.katex .sizing.reset-size7.size8, +.katex .fontsize-ensurer.reset-size7.size8 { + font-size: 1.20138889em; +} +.katex .sizing.reset-size7.size9, +.katex .fontsize-ensurer.reset-size7.size9 { + font-size: 1.4375em; +} +.katex .sizing.reset-size7.size10, +.katex .fontsize-ensurer.reset-size7.size10 { + font-size: 1.72916667em; +} +.katex .sizing.reset-size8.size1, +.katex .fontsize-ensurer.reset-size8.size1 { + font-size: 0.28901734em; +} +.katex .sizing.reset-size8.size2, +.katex .fontsize-ensurer.reset-size8.size2 { + font-size: 0.40462428em; +} +.katex .sizing.reset-size8.size3, +.katex .fontsize-ensurer.reset-size8.size3 { + font-size: 0.46242775em; +} +.katex .sizing.reset-size8.size4, +.katex .fontsize-ensurer.reset-size8.size4 { + font-size: 0.52023121em; +} +.katex .sizing.reset-size8.size5, +.katex .fontsize-ensurer.reset-size8.size5 { + font-size: 0.57803468em; +} +.katex .sizing.reset-size8.size6, +.katex .fontsize-ensurer.reset-size8.size6 { + font-size: 0.69364162em; +} +.katex .sizing.reset-size8.size7, +.katex .fontsize-ensurer.reset-size8.size7 { + font-size: 0.83236994em; +} +.katex .sizing.reset-size8.size8, +.katex .fontsize-ensurer.reset-size8.size8 { + font-size: 1em; +} +.katex .sizing.reset-size8.size9, +.katex .fontsize-ensurer.reset-size8.size9 { + font-size: 1.19653179em; +} +.katex .sizing.reset-size8.size10, +.katex .fontsize-ensurer.reset-size8.size10 { + font-size: 1.43930636em; +} +.katex .sizing.reset-size9.size1, +.katex .fontsize-ensurer.reset-size9.size1 { + font-size: 0.24154589em; +} +.katex .sizing.reset-size9.size2, +.katex .fontsize-ensurer.reset-size9.size2 { + font-size: 0.33816425em; +} +.katex .sizing.reset-size9.size3, +.katex .fontsize-ensurer.reset-size9.size3 { + font-size: 0.38647343em; +} +.katex .sizing.reset-size9.size4, +.katex .fontsize-ensurer.reset-size9.size4 { + font-size: 0.43478261em; +} +.katex .sizing.reset-size9.size5, +.katex .fontsize-ensurer.reset-size9.size5 { + font-size: 0.48309179em; +} +.katex .sizing.reset-size9.size6, +.katex .fontsize-ensurer.reset-size9.size6 { + font-size: 0.57971014em; +} +.katex .sizing.reset-size9.size7, +.katex .fontsize-ensurer.reset-size9.size7 { + font-size: 0.69565217em; +} +.katex .sizing.reset-size9.size8, +.katex .fontsize-ensurer.reset-size9.size8 { + font-size: 0.83574879em; +} +.katex .sizing.reset-size9.size9, +.katex .fontsize-ensurer.reset-size9.size9 { + font-size: 1em; +} +.katex .sizing.reset-size9.size10, +.katex .fontsize-ensurer.reset-size9.size10 { + font-size: 1.20289855em; +} +.katex .sizing.reset-size10.size1, +.katex .fontsize-ensurer.reset-size10.size1 { + font-size: 0.20080321em; +} +.katex .sizing.reset-size10.size2, +.katex .fontsize-ensurer.reset-size10.size2 { + font-size: 0.2811245em; +} +.katex .sizing.reset-size10.size3, +.katex .fontsize-ensurer.reset-size10.size3 { + font-size: 0.32128514em; +} +.katex .sizing.reset-size10.size4, +.katex .fontsize-ensurer.reset-size10.size4 { + font-size: 0.36144578em; +} +.katex .sizing.reset-size10.size5, +.katex .fontsize-ensurer.reset-size10.size5 { + font-size: 0.40160643em; +} +.katex .sizing.reset-size10.size6, +.katex .fontsize-ensurer.reset-size10.size6 { + font-size: 0.48192771em; +} +.katex .sizing.reset-size10.size7, +.katex .fontsize-ensurer.reset-size10.size7 { + font-size: 0.57831325em; +} +.katex .sizing.reset-size10.size8, +.katex .fontsize-ensurer.reset-size10.size8 { + font-size: 0.69477912em; +} +.katex .sizing.reset-size10.size9, +.katex .fontsize-ensurer.reset-size10.size9 { + font-size: 0.8313253em; +} +.katex .sizing.reset-size10.size10, +.katex .fontsize-ensurer.reset-size10.size10 { + font-size: 1em; +} +.katex .delimsizing.size1 { + font-family: KaTeX_Size1; +} +.katex .delimsizing.size2 { + font-family: KaTeX_Size2; +} +.katex .delimsizing.size3 { + font-family: KaTeX_Size3; +} +.katex .delimsizing.size4 { + font-family: KaTeX_Size4; +} +.katex .delimsizing.mult .delim-size1 > span { + font-family: KaTeX_Size1; +} +.katex .delimsizing.mult .delim-size4 > span { + font-family: KaTeX_Size4; +} +.katex .nulldelimiter { + display: inline-block; + width: 0.12em; +} +.katex .op-symbol { + position: relative; +} +.katex .op-symbol.small-op { + font-family: KaTeX_Size1; +} +.katex .op-symbol.large-op { + font-family: KaTeX_Size2; +} +.katex .op-limits > .vlist > span { + text-align: center; +} +.katex .accent > .vlist > span { + text-align: center; +} +.katex .accent .accent-body > span { + width: 0; +} +.katex .accent .accent-body.accent-vec > span { + position: relative; + left: 0.326em; +} +.katex .mtable .vertical-separator { + display: inline-block; + margin: 0 -0.025em; + border-right: 0.05em solid black; +} +.katex .mtable .arraycolsep { + display: inline-block; +} +.katex .mtable .col-align-c > .vlist { + text-align: center; +} +.katex .mtable .col-align-l > .vlist { + text-align: left; +} +.katex .mtable .col-align-r > .vlist { + text-align: right; +} -- cgit v1.2.1 From aa2c437fe05ea272e180527a848455599f9da916 Mon Sep 17 00:00:00 2001 From: Munken <mm.munk@gmail.com> Date: Thu, 8 Dec 2016 22:59:37 +0000 Subject: Hacked in Math Lexer --- lib/banzai/filter/syntax_highlight_filter.rb | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb index 026b81ac175..f6a813ac923 100644 --- a/lib/banzai/filter/syntax_highlight_filter.rb +++ b/lib/banzai/filter/syntax_highlight_filter.rb @@ -1,5 +1,29 @@ require 'rouge/plugins/redcarpet' +module Rouge + module Lexers + class Math < Lexer + title "Plain Text" + desc "A boring lexer that doesn't highlight anything" + + tag 'math' + aliases 'text' + filenames '*.txt' + mimetypes 'text/plain' + + default_options :token => 'Text' + + def token + @token ||= Token[option :token] + end + + def stream_tokens(string, &b) + yield self.token, string + end + end + end +end + module Banzai module Filter # HTML Filter to highlight fenced code blocks @@ -48,6 +72,9 @@ module Banzai end def lexer_for(language) + if language == 'math' + return Rouge::Lexers::Math.new + end (Rouge::Lexer.find(language) || Rouge::Lexers::PlainText).new end -- cgit v1.2.1 From 63bc23cfe242edd800730827c2d47dd025a26b2f Mon Sep 17 00:00:00 2001 From: Munken <mm.munk@gmail.com> Date: Thu, 8 Dec 2016 23:04:07 +0000 Subject: Detect whether math is display style --- app/assets/javascripts/syntax_highlight.js.erb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/syntax_highlight.js.erb b/app/assets/javascripts/syntax_highlight.js.erb index 5664c32d7b6..3b9a7d9baf9 100644 --- a/app/assets/javascripts/syntax_highlight.js.erb +++ b/app/assets/javascripts/syntax_highlight.js.erb @@ -23,7 +23,8 @@ var mathNode = $( "<math>Test</math>" ); mathNode.insertAfter($(this)); - katex.render($(this).text(), mathNode.get(0), { displayMode: false }) + var display = $(this).hasClass('highlight'); + katex.render($(this).text(), mathNode.get(0), { displayMode: display }) }) }; var handleMath = function () { -- cgit v1.2.1 From f7b09848c558b1a44571f27a9534c02c0501f181 Mon Sep 17 00:00:00 2001 From: Munken <mm.munk@gmail.com> Date: Thu, 8 Dec 2016 23:09:34 +0000 Subject: Removed alias and filenames --- lib/banzai/filter/syntax_highlight_filter.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb index f6a813ac923..278d4d92d08 100644 --- a/lib/banzai/filter/syntax_highlight_filter.rb +++ b/lib/banzai/filter/syntax_highlight_filter.rb @@ -7,8 +7,6 @@ module Rouge desc "A boring lexer that doesn't highlight anything" tag 'math' - aliases 'text' - filenames '*.txt' mimetypes 'text/plain' default_options :token => 'Text' -- cgit v1.2.1 From a36ee0ad4400b7e38bfa33ae99838dcb9527e870 Mon Sep 17 00:00:00 2001 From: Munken <mm.munk@gmail.com> Date: Thu, 8 Dec 2016 23:13:44 +0000 Subject: Better location for math lexer --- lib/banzai/filter/syntax_highlight_filter.rb | 22 ---------------------- lib/rouge/lexers/math.rb | 21 +++++++++++++++++++++ 2 files changed, 21 insertions(+), 22 deletions(-) create mode 100644 lib/rouge/lexers/math.rb diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb index 278d4d92d08..e7f6b715ba8 100644 --- a/lib/banzai/filter/syntax_highlight_filter.rb +++ b/lib/banzai/filter/syntax_highlight_filter.rb @@ -1,27 +1,5 @@ require 'rouge/plugins/redcarpet' -module Rouge - module Lexers - class Math < Lexer - title "Plain Text" - desc "A boring lexer that doesn't highlight anything" - - tag 'math' - mimetypes 'text/plain' - - default_options :token => 'Text' - - def token - @token ||= Token[option :token] - end - - def stream_tokens(string, &b) - yield self.token, string - end - end - end -end - module Banzai module Filter # HTML Filter to highlight fenced code blocks diff --git a/lib/rouge/lexers/math.rb b/lib/rouge/lexers/math.rb new file mode 100644 index 00000000000..ae980da8283 --- /dev/null +++ b/lib/rouge/lexers/math.rb @@ -0,0 +1,21 @@ +module Rouge + module Lexers + class Math < Lexer + title "Plain Text" + desc "A boring lexer that doesn't highlight anything" + + tag 'math' + mimetypes 'text/plain' + + default_options :token => 'Text' + + def token + @token ||= Token[option :token] + end + + def stream_tokens(string, &b) + yield self.token, string + end + end + end +end \ No newline at end of file -- cgit v1.2.1 From 7f829f52e1faff770eca93b9ffdcd1c4832f62fd Mon Sep 17 00:00:00 2001 From: Munken <mm.munk@gmail.com> Date: Thu, 8 Dec 2016 23:21:27 +0000 Subject: Don't double render --- app/assets/javascripts/syntax_highlight.js.erb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/javascripts/syntax_highlight.js.erb b/app/assets/javascripts/syntax_highlight.js.erb index 3b9a7d9baf9..ea79e82d887 100644 --- a/app/assets/javascripts/syntax_highlight.js.erb +++ b/app/assets/javascripts/syntax_highlight.js.erb @@ -19,6 +19,9 @@ // Loop over all math elements and render math var renderWithKaTeX = function (elements) { elements.each(function () { + if (!!$(this).attr('rendered')) return; + + $(this).attr('rendered', true); $(this).hide(); var mathNode = $( "<math>Test</math>" ); mathNode.insertAfter($(this)); -- cgit v1.2.1 From 2bfed70a0be4d8eaef0d7fea75c3ceb6990949a1 Mon Sep 17 00:00:00 2001 From: Munken <mm.munk@gmail.com> Date: Thu, 8 Dec 2016 23:30:41 +0000 Subject: More tests --- spec/lib/banzai/filter/inline_math_filter_spec.rb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/spec/lib/banzai/filter/inline_math_filter_spec.rb b/spec/lib/banzai/filter/inline_math_filter_spec.rb index 1edec83a6f2..e6cccd79723 100644 --- a/spec/lib/banzai/filter/inline_math_filter_spec.rb +++ b/spec/lib/banzai/filter/inline_math_filter_spec.rb @@ -23,9 +23,19 @@ describe Banzai::Filter::InlineMathFilter, lib: true do expect(doc.to_s).to eq 'test $<code class="code math">2+2</code>$ test' end - it 'ignores cases with missing dolar sign' do + it 'ignores cases with missing dolar sign at the end' do doc = filter("test $<code>2+2</code> test") expect(doc.to_s).to eq 'test $<code>2+2</code> test' end + it 'ignores cases with missing dolar sign at the beginning' do + doc = filter("test <code>2+2</code>$ test") + expect(doc.to_s).to eq 'test <code>2+2</code>$ test' + end + + it 'ignores dollar signs if it is not adjacent' do + doc = filter("$test <code>2+2</code>$ test") + expect(doc.to_s).to eq '$test <code>2+2</code>$ test' + end + end -- cgit v1.2.1 From e3f5c4c5f66c42ebf3c25c4d23507b56843b006d Mon Sep 17 00:00:00 2001 From: Munken <mm.munk@gmail.com> Date: Thu, 8 Dec 2016 23:48:54 +0000 Subject: More tests --- spec/lib/banzai/filter/inline_math_filter_spec.rb | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/spec/lib/banzai/filter/inline_math_filter_spec.rb b/spec/lib/banzai/filter/inline_math_filter_spec.rb index e6cccd79723..01d791f9ca1 100644 --- a/spec/lib/banzai/filter/inline_math_filter_spec.rb +++ b/spec/lib/banzai/filter/inline_math_filter_spec.rb @@ -4,8 +4,9 @@ describe Banzai::Filter::InlineMathFilter, lib: true do include FilterSpecHelper it 'leaves regular inline code unchanged' do - doc = filter("<code>2+2</code>") - expect(doc.to_s).to eq "<code>2+2</code>" + input = "<code>2+2</code>" + doc = filter(input) + expect(doc.to_s).to eq input end it 'removes surrounding dollar signs and adds class' do @@ -24,18 +25,21 @@ describe Banzai::Filter::InlineMathFilter, lib: true do end it 'ignores cases with missing dolar sign at the end' do - doc = filter("test $<code>2+2</code> test") - expect(doc.to_s).to eq 'test $<code>2+2</code> test' + input = "test $<code>2+2</code> test" + doc = filter(input) + expect(doc.to_s).to eq input end it 'ignores cases with missing dolar sign at the beginning' do - doc = filter("test <code>2+2</code>$ test") - expect(doc.to_s).to eq 'test <code>2+2</code>$ test' + input = "test <code>2+2</code>$ test" + doc = filter(input) + expect(doc.to_s).to eq input end it 'ignores dollar signs if it is not adjacent' do - doc = filter("$test <code>2+2</code>$ test") - expect(doc.to_s).to eq '$test <code>2+2</code>$ test' + input = '<p>We check strictly $<code>2+2</code> and <code>2+2</code>$ </p>' + doc = filter(input) + expect(doc.to_s).to eq input end end -- cgit v1.2.1 From 47bc0125beba2351c5d78323677a34d19b102047 Mon Sep 17 00:00:00 2001 From: Robert Speicher <robert@gitlab.com> Date: Wed, 7 Dec 2016 00:51:33 +0000 Subject: Merge branch 'destroy-session' into 'security' Destroy a user session when they delete their own account via browser This patch destroys a user's session when they delete their own account using a browser. A new session is created as they are redirected to the sign_in page. Issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/25015 See merge request !2042 --- app/controllers/registrations_controller.rb | 5 ++++- changelogs/unreleased/destroy-session.yml | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/destroy-session.yml diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 3327f4f2b87..c45196cc3e9 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -27,7 +27,10 @@ class RegistrationsController < Devise::RegistrationsController DeleteUserService.new(current_user).execute(current_user) respond_to do |format| - format.html { redirect_to new_user_session_path, notice: "Account successfully removed." } + format.html do + session.try(:destroy) + redirect_to new_user_session_path, notice: "Account successfully removed." + end end end diff --git a/changelogs/unreleased/destroy-session.yml b/changelogs/unreleased/destroy-session.yml new file mode 100644 index 00000000000..e713e2dc424 --- /dev/null +++ b/changelogs/unreleased/destroy-session.yml @@ -0,0 +1,4 @@ +--- +title: Destroy a user's session when they delete their own account +merge_request: +author: -- cgit v1.2.1 From 6e1b52b8b9b83cb774a5f2f52d4b4355590f14f7 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@gitlab.com> Date: Tue, 6 Dec 2016 01:05:06 +0000 Subject: Merge branch 'rs-filter-authentication_token' into 'security' Add authentication_token to filter_parameters list See merge request !2041 --- config/application.rb | 3 ++- config/initializers/sentry.rb | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/config/application.rb b/config/application.rb index fb84870dfbd..0aa2873f94a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -45,7 +45,7 @@ module Gitlab # # Parameters filtered: # - Password (:password, :password_confirmation) - # - Private tokens (:private_token) + # - Private tokens (:private_token, :authentication_token) # - Two-factor tokens (:otp_attempt) # - Repo/Project Import URLs (:import_url) # - Build variables (:variables) @@ -55,6 +55,7 @@ module Gitlab # - Sentry DSN (:sentry_dsn) # - Deploy keys (:key) config.filter_parameters += %i( + authentication_token certificate encrypted_key hook diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index 4f30d1265c8..6b0cff75653 100644 --- a/config/initializers/sentry.rb +++ b/config/initializers/sentry.rb @@ -15,7 +15,7 @@ if Rails.env.production? Raven.configure do |config| config.dsn = current_application_settings.sentry_dsn config.release = Gitlab::REVISION - + # Sanitize fields based on those sanitized from Rails. config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s) # Sanitize authentication headers -- cgit v1.2.1 From edf7dbfacd5a6b884ae1af72204e3718e89f3c35 Mon Sep 17 00:00:00 2001 From: Robert Speicher <robert@gitlab.com> Date: Fri, 2 Dec 2016 08:48:32 +0000 Subject: Merge branch 'html-safe-diff-line-content' into 'security' Don't accidentally mark unsafe diff lines as HTML safe Fixes potential XSS issue when a legacy diff note is created on a merge request whose diff contained HTML See https://gitlab.com/gitlab-org/gitlab-ce/issues/25249 See merge request !2040 --- app/helpers/diff_helper.rb | 4 +- .../unreleased/html-safe-diff-line-content.yml | 4 ++ spec/helpers/diff_helper_spec.rb | 61 ++++++++++++++++++---- 3 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 changelogs/unreleased/html-safe-diff-line-content.yml diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index f489f9aa0d6..c35d6611ab0 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -55,7 +55,9 @@ module DiffHelper if line.blank? " ".html_safe else - line.sub(/^[\-+ ]/, '').html_safe + # We can't use `sub` because the HTML-safeness of `line` will not survive. + line[0] = '' if line.start_with?('+', '-', ' ') + line end end diff --git a/changelogs/unreleased/html-safe-diff-line-content.yml b/changelogs/unreleased/html-safe-diff-line-content.yml new file mode 100644 index 00000000000..8f8bbc51963 --- /dev/null +++ b/changelogs/unreleased/html-safe-diff-line-content.yml @@ -0,0 +1,4 @@ +--- +title: Don't accidentally mark unsafe diff lines as HTML safe +merge_request: +author: diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 837e7afa7e8..468bcc7badc 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -60,15 +60,58 @@ describe DiffHelper do end describe '#diff_line_content' do - it 'returns non breaking space when line is empty' do - expect(diff_line_content(nil)).to eq(' ') - end - - it 'returns the line itself' do - expect(diff_line_content(diff_file.diff_lines.first.text)). - to eq('@@ -6,12 +6,18 @@ module Popen') - expect(diff_line_content(diff_file.diff_lines.first.type)).to eq('match') - expect(diff_file.diff_lines.first.new_pos).to eq(6) + context 'when the line is empty' do + it 'returns a non breaking space' do + expect(diff_line_content(nil)).to eq(' ') + end + + it 'returns an HTML-safe string' do + expect(diff_line_content(nil)).to be_html_safe + end + end + + context 'when the line is not empty' do + context 'when the line starts with +, -, or a space' do + it 'strips the first character' do + expect(diff_line_content('+new line')).to eq('new line') + expect(diff_line_content('-new line')).to eq('new line') + expect(diff_line_content(' new line')).to eq('new line') + end + + context 'when the line is HTML-safe' do + it 'returns an HTML-safe string' do + expect(diff_line_content('+new line'.html_safe)).to be_html_safe + expect(diff_line_content('-new line'.html_safe)).to be_html_safe + expect(diff_line_content(' new line'.html_safe)).to be_html_safe + end + end + + context 'when the line is not HTML-safe' do + it 'returns a non-HTML-safe string' do + expect(diff_line_content('+new line')).not_to be_html_safe + expect(diff_line_content('-new line')).not_to be_html_safe + expect(diff_line_content(' new line')).not_to be_html_safe + end + end + end + + context 'when the line does not start with a +, -, or a space' do + it 'returns the string' do + expect(diff_line_content('@@ -6,12 +6,18 @@ module Popen')).to eq('@@ -6,12 +6,18 @@ module Popen') + end + + context 'when the line is HTML-safe' do + it 'returns an HTML-safe string' do + expect(diff_line_content('@@ -6,12 +6,18 @@ module Popen'.html_safe)).to be_html_safe + end + end + + context 'when the line is not HTML-safe' do + it 'returns a non-HTML-safe string' do + expect(diff_line_content('@@ -6,12 +6,18 @@ module Popen')).not_to be_html_safe + end + end + end end end -- cgit v1.2.1 From f23b1cb453deea2659c0cb9e9047c72d859bbf9d Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@gitlab.com> Date: Tue, 29 Nov 2016 13:47:43 +0000 Subject: Merge branch 'jej-23867-use-mr-finder-instead-of-access-check' into 'security' Replace MR access checks with use of MergeRequestsFinder Split from !2024 to partially solve https://gitlab.com/gitlab-org/gitlab-ce/issues/23867 :warning: - Potentially untested :bomb: - No test coverage :traffic_light: - Test coverage of some sort exists (a test failed when error raised) :vertical_traffic_light: - Test coverage of return value (a test failed when nil used) :white_check_mark: - Permissions check tested - [x] :bomb: app/finders/notes_finder.rb:17 - [x] :warning: app/views/layouts/nav/_project.html.haml:80 [`.count`] - [x] :bomb: app/controllers/concerns/creates_commit.rb:84 - [x] :traffic_light: app/controllers/projects/commits_controller.rb:24 - [x] :traffic_light: app/controllers/projects/compare_controller.rb:56 - [x] :vertical_traffic_light: app/controllers/projects/discussions_controller.rb:29 - [x] :white_check_mark: app/controllers/projects/todos_controller.rb:27 - [x] :vertical_traffic_light: app/models/commit.rb:268 - [x] :white_check_mark: lib/gitlab/search_results.rb:71 - [x] https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/2024/diffs#d1c10892daedb4d4dd3d4b12b6d071091eea83df_267_266 Memoize ` merged_merge_request(current_user)` - [x] https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/2024/diffs#d1c10892daedb4d4dd3d4b12b6d071091eea83df_248_247 Expected side effect for `merged_merge_request!`, consider `skip_authorization: true`. - [x] https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/2024/diffs#d1c10892daedb4d4dd3d4b12b6d071091eea83df_269_269 Scary use of unchecked `merged_merge_request?` See merge request !2033 --- app/controllers/concerns/creates_commit.rb | 6 ++-- app/controllers/projects/commit_controller.rb | 16 ++++----- app/controllers/projects/commits_controller.rb | 2 +- app/controllers/projects/compare_controller.rb | 2 +- app/controllers/projects/discussions_controller.rb | 2 +- app/controllers/projects/todos_controller.rb | 2 +- app/finders/issuable_finder.rb | 4 +++ app/finders/notes_finder.rb | 2 +- app/helpers/commits_helper.rb | 4 +-- app/models/commit.rb | 41 ++++++++++++++-------- app/models/concerns/milestoneish.rb | 10 +++--- app/models/merge_request.rb | 2 +- app/models/repository.rb | 2 +- app/services/commits/change_service.rb | 2 +- app/views/layouts/nav/_project.html.haml | 2 +- app/views/projects/commit/_change.html.haml | 2 +- ...23867-use-mr-finder-instead-of-access-check.yml | 4 +++ lib/gitlab/search_results.rb | 2 +- spec/controllers/projects/todo_controller_spec.rb | 15 +++++++- spec/lib/gitlab/search_results_spec.rb | 16 +++++++++ spec/models/commit_range_spec.rb | 15 ++++---- spec/models/commit_spec.rb | 11 +++--- 22 files changed, 104 insertions(+), 60 deletions(-) create mode 100644 changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index dacb5679dd3..936d9bab57e 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -81,10 +81,8 @@ module CreatesCommit def merge_request_exists? return @merge_request if defined?(@merge_request) - @merge_request = @mr_target_project.merge_requests.opened.find_by( - source_branch: @mr_source_branch, - target_branch: @mr_target_branch - ) + @merge_request = MergeRequestsFinder.new(current_user, project_id: @mr_target_project.id).execute.opened. + find_by(source_branch: @mr_source_branch, target_branch: @mr_target_branch) end def different_project? diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index cdfc1ba7b92..8197d9e4c99 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -65,7 +65,7 @@ class Projects::CommitController < Projects::ApplicationController return render_404 if @target_branch.blank? - create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title} has been successfully reverted.", + create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully reverted.", success_path: successful_change_path, failure_path: failed_change_path) end @@ -74,26 +74,24 @@ class Projects::CommitController < Projects::ApplicationController return render_404 if @target_branch.blank? - create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title} has been successfully cherry-picked.", + create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked.", success_path: successful_change_path, failure_path: failed_change_path) end private def successful_change_path - return referenced_merge_request_url if @commit.merged_merge_request - - namespace_project_commits_url(@project.namespace, @project, @target_branch) + referenced_merge_request_url || namespace_project_commits_url(@project.namespace, @project, @target_branch) end def failed_change_path - return referenced_merge_request_url if @commit.merged_merge_request - - namespace_project_commit_url(@project.namespace, @project, params[:id]) + referenced_merge_request_url || namespace_project_commit_url(@project.namespace, @project, params[:id]) end def referenced_merge_request_url - namespace_project_merge_request_url(@project.namespace, @project, @commit.merged_merge_request) + if merge_request = @commit.merged_merge_request(current_user) + namespace_project_merge_request_url(@project.namespace, @project, merge_request) + end end def commit diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index aba87b6144b..ad92f05a42d 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -21,7 +21,7 @@ class Projects::CommitsController < Projects::ApplicationController @note_counts = project.notes.where(commit_id: @commits.map(&:id)). group(:commit_id).count - @merge_request = @project.merge_requests.opened. + @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened. find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref) respond_to do |format| diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index bee3d56076c..ec02fc15d35 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -53,7 +53,7 @@ class Projects::CompareController < Projects::ApplicationController end def merge_request - @merge_request ||= @project.merge_requests.opened. + @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened. find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref) end end diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb index 148e39630e3..1349b015a63 100644 --- a/app/controllers/projects/discussions_controller.rb +++ b/app/controllers/projects/discussions_controller.rb @@ -24,7 +24,7 @@ class Projects::DiscussionsController < Projects::ApplicationController private def merge_request - @merge_request ||= @project.merge_requests.find_by!(iid: params[:merge_request_id]) + @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).find_by!(iid: params[:merge_request_id]) end def discussion diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb index 52517381c65..a41fcb85c40 100644 --- a/app/controllers/projects/todos_controller.rb +++ b/app/controllers/projects/todos_controller.rb @@ -18,7 +18,7 @@ class Projects::TodosController < Projects::ApplicationController when "issue" IssuesFinder.new(current_user, project_id: @project.id).find(params[:issuable_id]) when "merge_request" - @project.merge_requests.find(params[:issuable_id]) + MergeRequestsFinder.new(current_user, project_id: @project.id).find(params[:issuable_id]) end end end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index c9bee01b9ad..b4c14d05eaf 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -77,6 +77,10 @@ class IssuableFinder counts end + def find_by!(*params) + execute.find_by!(*params) + end + def group return @group if defined?(@group) diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index a653a6d59c6..2484339e3a4 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -14,7 +14,7 @@ class NotesFinder when "issue" IssuesFinder.new(current_user, project_id: project.id).find(target_id).notes.inc_author when "merge_request" - project.merge_requests.find(target_id).mr_and_commit_notes.inc_author + MergeRequestsFinder.new(current_user, project_id: project.id).find(target_id).mr_and_commit_notes.inc_author when "snippet", "project_snippet" project.snippets.find(target_id).notes else diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index ed402b698fb..66a720a9426 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -130,7 +130,7 @@ module CommitsHelper def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) return unless current_user - tooltip = "Revert this #{commit.change_type_title} in a new merge request" if has_tooltip + tooltip = "Revert this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip if can_collaborate_with_project? btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil? @@ -154,7 +154,7 @@ module CommitsHelper def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) return unless current_user - tooltip = "Cherry-pick this #{commit.change_type_title} in a new merge request" + tooltip = "Cherry-pick this #{commit.change_type_title(current_user)} in a new merge request" if can_collaborate_with_project? btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil? diff --git a/app/models/commit.rb b/app/models/commit.rb index 248140f421b..1831cc7e175 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -245,44 +245,47 @@ class Commit project.repository.next_branch("cherry-pick-#{short_id}", mild: true) end - def revert_description - if merged_merge_request - "This reverts merge request #{merged_merge_request.to_reference}" + def revert_description(user) + if merged_merge_request?(user) + "This reverts merge request #{merged_merge_request(user).to_reference}" else "This reverts commit #{sha}" end end - def revert_message - %Q{Revert "#{title.strip}"\n\n#{revert_description}} + def revert_message(user) + %Q{Revert "#{title.strip}"\n\n#{revert_description(user)}} end - def reverts_commit?(commit) - description? && description.include?(commit.revert_description) + def reverts_commit?(commit, user) + description? && description.include?(commit.revert_description(user)) end def merge_commit? parents.size > 1 end - def merged_merge_request - return @merged_merge_request if defined?(@merged_merge_request) - - @merged_merge_request = project.merge_requests.find_by(merge_commit_sha: id) if merge_commit? + def merged_merge_request(current_user) + # Memoize with per-user access check + @merged_merge_request_hash ||= Hash.new do |hash, user| + hash[user] = merged_merge_request_no_cache(user) + end + + @merged_merge_request_hash[current_user] end - def has_been_reverted?(current_user = nil, noteable = self) + def has_been_reverted?(current_user, noteable = self) ext = all_references(current_user) noteable.notes_with_associations.system.each do |note| note.all_references(current_user, extractor: ext) end - ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self) } + ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self, current_user) } end - def change_type_title - merged_merge_request ? 'merge request' : 'commit' + def change_type_title(user) + merged_merge_request?(user) ? 'merge request' : 'commit' end # Get the URI type of the given path @@ -350,4 +353,12 @@ class Commit changes end + + def merged_merge_request?(user) + !!merged_merge_request(user) + end + + def merged_merge_request_no_cache(user) + MergeRequestsFinder.new(user, project_id: project.id).find_by(merge_commit_sha: id) if merge_commit? + end end diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index e65fc9eaa09..875e9834487 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -1,17 +1,17 @@ module Milestoneish - def closed_items_count(user = nil) + def closed_items_count(user) issues_visible_to_user(user).closed.size + merge_requests.closed_and_merged.size end - def total_items_count(user = nil) + def total_items_count(user) issues_visible_to_user(user).size + merge_requests.size end - def complete?(user = nil) + def complete?(user) total_items_count(user) > 0 && total_items_count(user) == closed_items_count(user) end - def percent_complete(user = nil) + def percent_complete(user) ((closed_items_count(user) * 100) / total_items_count(user)).abs rescue ZeroDivisionError 0 @@ -29,7 +29,7 @@ module Milestoneish (Date.today - start_date).to_i end - def issues_visible_to_user(user = nil) + def issues_visible_to_user(user) issues.visible_to_user(user) end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 33b578e12c1..dd9f1a7c85b 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -805,7 +805,7 @@ class MergeRequest < ActiveRecord::Base @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha end - def can_be_reverted?(current_user = nil) + def can_be_reverted?(current_user) merge_commit && !merge_commit.has_been_reverted?(current_user, self) end diff --git a/app/models/repository.rb b/app/models/repository.rb index 3c4b0212af7..1ccabdb7c1f 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -950,7 +950,7 @@ class Repository update_branch_with_hooks(user, base_branch) do committer = user_to_committer(user) source_sha = Rugged::Commit.create(rugged, - message: commit.revert_message, + message: commit.revert_message(user), author: committer, committer: committer, tree: revert_tree_id, diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index 1c82599c579..db5f2bf9b2e 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -34,7 +34,7 @@ module Commits repository.public_send(action, current_user, @commit, into, tree_id) success else - error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title} automatically. + error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically. It may have already been #{action.to_s.dasherize}, or a more recent commit may have updated some of its content." raise ChangeError, error_msg end diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 701bcd3ab71..7bd11f5727a 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -77,7 +77,7 @@ = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do %span Merge Requests - %span.badge.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count) + %span.badge.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) - if project_nav_tab? :wiki = nav_link(controller: :wikis) do diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index e4cd55b9f7a..f6e3d5e76f5 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -11,7 +11,7 @@ .modal-content .modal-header %a.close{href: "#", "data-dismiss" => "modal"} × - %h3.page-title== #{label} this #{commit.change_type_title} + %h3.page-title== #{label} this #{commit.change_type_title(current_user)} .modal-body = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-#{type}-form js-requires-input' do .form-group.branch diff --git a/changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml b/changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml new file mode 100644 index 00000000000..5a4a44b9562 --- /dev/null +++ b/changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml @@ -0,0 +1,4 @@ +--- +title: Replace MR access checks with use of MergeRequestsFinder +merge_request: +author: diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index 47d8599e298..35212992698 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -68,7 +68,7 @@ module Gitlab end def merge_requests - merge_requests = MergeRequest.in_projects(project_ids_relation) + merge_requests = MergeRequestsFinder.new(current_user).execute.in_projects(project_ids_relation) if query =~ /[#!](\d+)\z/ merge_requests = merge_requests.where(iid: $1) else diff --git a/spec/controllers/projects/todo_controller_spec.rb b/spec/controllers/projects/todo_controller_spec.rb index 193a3f6b5a3..415c264e0dd 100644 --- a/spec/controllers/projects/todo_controller_spec.rb +++ b/spec/controllers/projects/todo_controller_spec.rb @@ -110,7 +110,7 @@ describe Projects::TodosController do end end - context 'when not authorized' do + context 'when not authorized for project' do it 'does not create todo for merge request user has no access to' do sign_in(user) expect do @@ -128,6 +128,19 @@ describe Projects::TodosController do expect(response).to have_http_status(302) end end + + context 'when not authorized for merge_request' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE) + sign_in(user) + end + + it "doesn't create todo" do + expect{ go }.not_to change { user.todos.count } + expect(response).to have_http_status(404) + end + end end end end diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index f23e3522625..9614aad3e73 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -40,6 +40,15 @@ describe Gitlab::SearchResults do expect(results.milestones_count).to eq(1) end end + + it 'includes merge requests from source and target projects' do + forked_project = create(:empty_project, forked_from_project: project) + merge_request_2 = create(:merge_request, target_project: project, source_project: forked_project, title: 'foo') + + results = described_class.new(user, Project.where(id: forked_project.id), 'foo') + + expect(results.objects('merge_requests')).to include merge_request_2 + end end it 'does not list issues on private projects' do @@ -152,4 +161,11 @@ describe Gitlab::SearchResults do expect(results.issues_count).to eq 5 end end + + it 'does not list merge requests on projects with limited access' do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE) + + expect(results.objects('merge_requests')).not_to include merge_request + end end diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb index d89d4342dea..30782ca75a0 100644 --- a/spec/models/commit_range_spec.rb +++ b/spec/models/commit_range_spec.rb @@ -137,26 +137,25 @@ describe CommitRange, models: true do end describe '#has_been_reverted?' do - it 'returns true if the commit has been reverted' do - issue = create(:issue) + let(:issue) { create(:issue) } + let(:user) { issue.author } + it 'returns true if the commit has been reverted' do create(:note_on_issue, noteable: issue, system: true, - note: commit1.revert_description, + note: commit1.revert_description(user), project: issue.project) expect_any_instance_of(Commit).to receive(:reverts_commit?). - with(commit1). + with(commit1, user). and_return(true) - expect(commit1.has_been_reverted?(nil, issue)).to eq(true) + expect(commit1.has_been_reverted?(user, issue)).to eq(true) end it 'returns false a commit has not been reverted' do - issue = create(:issue) - - expect(commit1.has_been_reverted?(nil, issue)).to eq(false) + expect(commit1.has_been_reverted?(user, issue)).to eq(false) end end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index eb482c7f913..0935fc0561c 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -179,25 +179,26 @@ eos describe '#reverts_commit?' do let(:another_commit) { double(:commit, revert_description: "This reverts commit #{commit.sha}") } + let(:user) { commit.author } - it { expect(commit.reverts_commit?(another_commit)).to be_falsy } + it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy } context 'commit has no description' do before { allow(commit).to receive(:description?).and_return(false) } - it { expect(commit.reverts_commit?(another_commit)).to be_falsy } + it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy } end context "another_commit's description does not revert commit" do before { allow(commit).to receive(:description).and_return("Foo Bar") } - it { expect(commit.reverts_commit?(another_commit)).to be_falsy } + it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy } end context "another_commit's description reverts commit" do before { allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar") } - it { expect(commit.reverts_commit?(another_commit)).to be_truthy } + it { expect(commit.reverts_commit?(another_commit, user)).to be_truthy } end context "another_commit's description reverts merged merge request" do @@ -207,7 +208,7 @@ eos allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar") end - it { expect(commit.reverts_commit?(another_commit)).to be_truthy } + it { expect(commit.reverts_commit?(another_commit, user)).to be_truthy } end end -- cgit v1.2.1 From b727a4399cb3f4ec2937d8135b3e2b769d887b0d Mon Sep 17 00:00:00 2001 From: Victor Wu <victor@gitlab.com> Date: Fri, 9 Dec 2016 06:43:52 +0000 Subject: Remove unnecessary message --- app/views/projects/merge_requests/show/_commits.html.haml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml index a0e12fb3f38..baa1ade5eee 100644 --- a/app/views/projects/merge_requests/show/_commits.html.haml +++ b/app/views/projects/merge_requests/show/_commits.html.haml @@ -1,6 +1,2 @@ -.content-block.oneline-block - = icon("sort-amount-desc") - Most recent commits displayed first - %ol#commits-list.list-unstyled = render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch -- cgit v1.2.1 From b9b2ca47b3bcc7dfc56a3199994063988ff8218e Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Fri, 9 Dec 2016 09:49:38 +0100 Subject: Update docs to reflect new defaults on omnibus For mattermost chat commands, new defaults are set in the next release making configuring easier. This commit reflects that in the doc. [ci skip] --- .../img/mattermost_console_integrations.png | Bin 41186 -> 314642 bytes doc/project_services/mattermost_slash_commands.md | 8 ++++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/project_services/img/mattermost_console_integrations.png b/doc/project_services/img/mattermost_console_integrations.png index b3b8c20d7bf..92a30da5be0 100644 Binary files a/doc/project_services/img/mattermost_console_integrations.png and b/doc/project_services/img/mattermost_console_integrations.png differ diff --git a/doc/project_services/mattermost_slash_commands.md b/doc/project_services/mattermost_slash_commands.md index 1507dfa3abd..6fcbf6f1642 100644 --- a/doc/project_services/mattermost_slash_commands.md +++ b/doc/project_services/mattermost_slash_commands.md @@ -22,6 +22,9 @@ commands in Mattermost and then enable the service in GitLab. ### Step 1. Enable custom slash commands in Mattermost +This step is only required when using a source install, omnibus installs will be +preconfigured with the right settings. + The first thing to do in Mattermost is to enable custom slash commands from the administrator console. @@ -32,8 +35,9 @@ the administrator console. --- -1. Click **Custom integrations** and set **Enable Custom Slash Commands** to - true. +1. Click **Custom integrations** and set **Enable Custom Slash Commands**, + **Enable custom integrations to override usernames**, and **Override + custom integrations to override profile picture icons** to true ![Mattermost console](img/mattermost_console_integrations.png) -- cgit v1.2.1 From 1637ce4e6a64c154eca93444f6a62664f4e78e7a Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 9 Dec 2016 10:06:02 +0000 Subject: Updated JS based on review Fixed group links dropdown to match --- app/assets/javascripts/members.js.es6 | 22 ++++++++-------------- app/views/projects/group_links/update.js.haml | 1 + app/views/shared/members/_group.html.haml | 21 +++++++++++++++++++-- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/members.js.es6 b/app/assets/javascripts/members.js.es6 index 64826894965..e3f367a11eb 100644 --- a/app/assets/javascripts/members.js.es6 +++ b/app/assets/javascripts/members.js.es6 @@ -31,14 +31,8 @@ toggleLabel(selected, $el) { return $el.text(); }, - clicked: (selected, $el) => { - const $link = $($el); - const { $toggle, $dateInput } = this.getMemberListItems($link); - - $toggle.attr('disabled', true); - $dateInput.attr('disabled', true); - - $btn.closest('form').trigger('submit.rails'); + clicked: (selected, $link) => { + this.formSubmit(null, $link); }, }); }); @@ -55,21 +49,21 @@ } } - formSubmit(e) { - const $this = $(e.currentTarget); + formSubmit(e, $el = null) { + const $this = e ? $(e.currentTarget) : $el; const { $toggle, $dateInput } = this.getMemberListItems($this); $this.closest('form').trigger('submit.rails'); - $toggle.attr('disabled', true); - $dateInput.attr('disabled', true); + $toggle.disable(); + $dateInput.disable(); } formSuccess(e) { const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member')); - $toggle.removeAttr('disabled'); - $dateInput.removeAttr('disabled'); + $toggle.enable(); + $dateInput.enable(); } getMemberListItems($el) { diff --git a/app/views/projects/group_links/update.js.haml b/app/views/projects/group_links/update.js.haml index af9a5b19060..55520fda494 100644 --- a/app/views/projects/group_links/update.js.haml +++ b/app/views/projects/group_links/update.js.haml @@ -1,3 +1,4 @@ :plain var $listItem = $('#{escape_javascript(render('shared/members/group', group_link: @group_link))}'); $("#group_member_#{@group_link.id} .list-item-name").replaceWith($listItem.find('.list-item-name')); + gl.utils.localTimeAgo($('.js-timeago'), $("#group_member_#{@group_link.id}")); diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml index 1c0346bbc78..8928de9097b 100644 --- a/app/views/shared/members/_group.html.haml +++ b/app/views/shared/members/_group.html.haml @@ -1,7 +1,8 @@ - group_link = local_assigns[:group_link] - group = group_link.group - can_admin_member = can?(current_user, :admin_project_member, @project) -%li.member.group_member{ id: "group_member_#{group_link.id}" } +- dom_id = "group_member_#{group_link.id}" +%li.member.group_member{ id: dom_id } %span{ class: "list-item-name" } = image_tag group_icon(group), class: "avatar s40", alt: '' %strong @@ -14,7 +15,23 @@ Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)} .controls.member-controls = form_tag namespace_project_group_link_path(@project.namespace, @project, group_link), method: :put, remote: true, class: 'form-horizontal js-edit-member-form' do - = select_tag 'group_link[group_access]', options_for_select(ProjectGroupLink.access_options, group_link.group_access), class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{group.id}", disabled: !can_admin_member + = hidden_field_tag "group_link[group_access]", group_link.group_access + .member-form-control.dropdown.append-right-5 + %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button", + disabled: !can_admin_member, + data: { toggle: "dropdown", field_name: "group_link[group_access]" } } + %span.dropdown-toggle-text + = group_link.human_access + = icon("chevron-down") + .dropdown-menu.dropdown-select.dropdown-menu-align-right.dropdown-menu-selectable + = dropdown_title("Change permissions") + .dropdown-content + %ul + - Gitlab::Access.options.each do |role, role_id| + %li + = link_to role, "javascript:void(0)", + class: ("is-active" if group_link.group_access == role_id), + data: { id: role_id, el_id: dom_id } .prepend-left-5.clearable-input.member-form-control = text_field_tag 'group_link[expires_at]', group_link.expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{group.id}", disabled: !can_admin_member %i.clear-icon.js-clear-input -- cgit v1.2.1 From 70cb27926b225f71ccd4bba2342abbc3962da7e9 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 8 Dec 2016 11:42:56 +0000 Subject: Replace play icon svg logic --- .../environments/components/environment.js.es6 | 10 +++++-- .../components/environment_actions.js.es6 | 32 ++++------------------ .../components/environment_item.js.es6 | 11 ++++++++ app/views/projects/environments/index.html.haml | 4 ++- 4 files changed, 28 insertions(+), 29 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index 84faabf938a..1db29dd47fb 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -74,6 +74,8 @@ projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath, newEnvironmentPath: environmentsData.newEnvironmentPath, helpPagePath: environmentsData.helpPagePath, + commitIconSvg: environmentsData.commitIconSvg, + playIconSvg: environmentsData.playIconSvg, }; }, @@ -227,7 +229,9 @@ :model="model" :toggleRow="toggleRow.bind(model)" :can-create-deployment="canCreateDeploymentParsed" - :can-read-environment="canReadEnvironmentParsed"></tr> + :can-read-environment="canReadEnvironmentParsed" + :play-icon-svg="playIconSvg" + :commit-icon-svg="commitIconSvg"></tr> <tr v-if="model.isOpen && model.children && model.children.length > 0" is="environment-item" @@ -235,7 +239,9 @@ :model="children" :toggleRow="toggleRow.bind(children)" :can-create-deployment="canCreateDeploymentParsed" - :can-read-environment="canReadEnvironmentParsed"> + :can-read-environment="canReadEnvironmentParsed" + :play-icon-svg="playIconSvg" + :commit-icon-svg="commitIconSvg"> </tr> </template> diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6 index d149a446e0b..02cdb305217 100644 --- a/app/assets/javascripts/environments/components/environment_actions.js.es6 +++ b/app/assets/javascripts/environments/components/environment_actions.js.es6 @@ -12,38 +12,18 @@ required: false, default: () => [], }, - }, - - /** - * Appends the svg icon that were render in the index page. - * In order to reuse the svg instead of copy and paste in this template - * we need to render it outside this component using =custom_icon partial. - * - * TODO: Remove this when webpack is merged. - * - */ - mounted() { - const playIcon = document.querySelector('.play-icon-svg.hidden svg'); - - const dropdownContainer = this.$el.querySelector('.dropdown-play-icon-container'); - const actionContainers = this.$el.querySelectorAll('.action-play-icon-container'); - // Phantomjs does not have support to iterate a nodelist. - const actionsArray = [].slice.call(actionContainers); - if (playIcon && actionsArray && dropdownContainer) { - dropdownContainer.appendChild(playIcon.cloneNode(true)); - - actionsArray.forEach((element) => { - element.appendChild(playIcon.cloneNode(true)); - }); - } + playIconSvg: { + type: String, + required: false, + }, }, template: ` <div class="inline"> <div class="dropdown"> <a class="dropdown-new btn btn-default" data-toggle="dropdown"> - <span class="dropdown-play-icon-container"></span> + <span v-html='playIconSvg'></span> <i class="fa fa-caret-down"></i> </a> @@ -53,7 +33,7 @@ data-method="post" rel="nofollow" class="js-manual-action-link"> - <span class="action-play-icon-container"></span> + <span v-html='playIconSvg'></span> <span> {{action.name}} </span> diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 6ed14261fc3..4e672bf6b58 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -58,6 +58,16 @@ required: false, default: false, }, + + commitIconSvg: { + type: String, + required: false, + }, + + playIconSvg: { + type: String, + required: false, + }, }, data() { @@ -476,6 +486,7 @@ <div v-if="hasManualActions && canCreateDeployment" class="inline js-manual-actions-container"> <actions-component + :play-icon-svg="playIconSvg" :actions="manualActions"> </actions-component> </div> diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index a9235d6af35..a65a630f2d0 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -17,4 +17,6 @@ "project-stopped-environments-path" => project_environments_path(@project, scope: :stopped), "new-environment-path" => new_namespace_project_environment_path(@project.namespace, @project), "help-page-path" => help_page_path("ci/environments"), - "css-class" => container_class}} + "css-class" => container_class, + "commit-icon-svg" => custom_icon("icon_commit"), + "play-icon-svg" => custom_icon("icon_play")}} -- cgit v1.2.1 From 1c5291715adfb38d0d54417827e4f0a4289e6788 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 8 Dec 2016 11:48:19 +0000 Subject: Replace commit icon svg logic --- .../components/environment_item.js.es6 | 3 ++- .../javascripts/vue_common_component/commit.js.es6 | 25 ++++++---------------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 4e672bf6b58..177ffcb3785 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -465,7 +465,8 @@ :commit_url="commitUrl" :short_sha="commitShortSha" :title="commitTitle" - :author="commitAuthor"> + :author="commitAuthor" + :commit-icon-svg="commitIconSvg"> </commit-component> </div> <p v-if="!isFolder && !hasLastDeploymentKey" class="commit-title"> diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_common_component/commit.js.es6 index 2ef2959cbf4..2b67521d022 100644 --- a/app/assets/javascripts/vue_common_component/commit.js.es6 +++ b/app/assets/javascripts/vue_common_component/commit.js.es6 @@ -68,6 +68,11 @@ required: false, default: () => ({}), }, + + commitIconSvg: { + type: String, + required: false, + }, }, computed: { @@ -110,24 +115,6 @@ }, }, - /** - * In order to reuse the svg instead of copy and paste in this template - * we need to render it outside this component using =custom_icon partial. - * Make sure it has this structure: - * .commit-icon-svg.hidden - * svg - * - * TODO: Find a better way to include SVG - */ - mounted() { - const commitIconContainer = this.$el.querySelector('.commit-icon-container'); - const commitIcon = document.querySelector('.commit-icon-svg.hidden svg'); - - if (commitIconContainer && commitIcon) { - commitIconContainer.appendChild(commitIcon.cloneNode(true)); - } - }, - template: ` <div class="branch-commit"> @@ -142,7 +129,7 @@ {{commit_ref.name}} </a> - <div class="icon-container commit-icon commit-icon-container"></div> + <div v-html="commitIconSvg" class="commit-icon"></div> <a class="commit-id monospace" :href="commit_url"> -- cgit v1.2.1 From 3768de806566c10af980373a5f66a77306968ae9 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 8 Dec 2016 11:54:08 +0000 Subject: Uniformize props name format --- .../components/environment_external_url.js.es6 | 4 ++-- .../environments/components/environment_item.js.es6 | 16 ++++++++-------- .../components/environment_rollback.js.es6 | 9 +++++---- .../environments/components/environment_stop.js.es6 | 4 ++-- .../javascripts/vue_common_component/commit.js.es6 | 18 +++++++++--------- .../environments/environment_external_url_spec.js.es6 | 2 +- .../environments/environment_rollback_spec.js.es6 | 12 ++++++------ .../environments/environment_stop_spec.js.es6 | 2 +- 8 files changed, 34 insertions(+), 33 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_external_url.js.es6 b/app/assets/javascripts/environments/components/environment_external_url.js.es6 index 79cd5ded5bd..aed65b33c04 100644 --- a/app/assets/javascripts/environments/components/environment_external_url.js.es6 +++ b/app/assets/javascripts/environments/components/environment_external_url.js.es6 @@ -7,14 +7,14 @@ window.gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', { props: { - external_url: { + externalUrl: { type: String, default: '', }, }, template: ` - <a class="btn external_url" :href="external_url" target="_blank"> + <a class="btn external_url" :href="externalUrl" target="_blank"> <i class="fa fa-external-link"></i> </a> `, diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 177ffcb3785..2e046a60146 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -461,9 +461,9 @@ <div v-if="!isFolder && hasLastDeploymentKey" class="js-commit-component"> <commit-component :tag="commitTag" - :commit_ref="commitRef" - :commit_url="commitUrl" - :short_sha="commitShortSha" + :commit-ref="commitRef" + :commit-url="commitUrl" + :short-sha="commitShortSha" :title="commitTitle" :author="commitAuthor" :commit-icon-svg="commitIconSvg"> @@ -495,22 +495,22 @@ <div v-if="model.external_url && canReadEnvironment" class="inline js-external-url-container"> <external-url-component - :external_url="model.external_url"> - </external_url-component> + :external-url="model.external_url"> + </external-url-component> </div> <div v-if="isStoppable && canCreateDeployment" class="inline js-stop-component-container"> <stop-component - :stop_url="model.stop_path"> + :stop-url="model.stop_path"> </stop-component> </div> <div v-if="canRetry && canCreateDeployment" class="inline js-rollback-component-container"> <rollback-component - :is_last_deployment="isLastDeployment" - :retry_url="retryUrl"> + :is-last-deployment="isLastDeployment" + :retry-url="retryUrl"> </rollback-component> </div> </div> diff --git a/app/assets/javascripts/environments/components/environment_rollback.js.es6 b/app/assets/javascripts/environments/components/environment_rollback.js.es6 index 55e5c826e07..6d4e8fad604 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.js.es6 +++ b/app/assets/javascripts/environments/components/environment_rollback.js.es6 @@ -7,19 +7,20 @@ window.gl.environmentsList.RollbackComponent = Vue.component('rollback-component', { props: { - retry_url: { + retryUrl: { type: String, default: '', }, - is_last_deployment: { + + isLastDeployment: { type: Boolean, default: true, }, }, template: ` - <a class="btn" :href="retry_url" data-method="post" rel="nofollow"> - <span v-if="is_last_deployment"> + <a class="btn" :href="retryUrl" data-method="post" rel="nofollow"> + <span v-if="isLastDeployment"> Re-deploy </span> <span v-else> diff --git a/app/assets/javascripts/environments/components/environment_stop.js.es6 b/app/assets/javascripts/environments/components/environment_stop.js.es6 index e6d66a0148c..7292f924e5c 100644 --- a/app/assets/javascripts/environments/components/environment_stop.js.es6 +++ b/app/assets/javascripts/environments/components/environment_stop.js.es6 @@ -7,7 +7,7 @@ window.gl.environmentsList.StopComponent = Vue.component('stop-component', { props: { - stop_url: { + stopUrl: { type: String, default: '', }, @@ -15,7 +15,7 @@ template: ` <a class="btn stop-env-link" - :href="stop_url" + :href="stopUrl" data-confirm="Are you sure you want to stop this environment?" data-method="post" rel="nofollow"> diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_common_component/commit.js.es6 index 2b67521d022..1f6995a645a 100644 --- a/app/assets/javascripts/vue_common_component/commit.js.es6 +++ b/app/assets/javascripts/vue_common_component/commit.js.es6 @@ -23,7 +23,7 @@ * name * ref_url */ - commit_ref: { + commitRef: { type: Object, required: false, default: () => ({}), @@ -32,7 +32,7 @@ /** * Used to link to the commit sha. */ - commit_url: { + commitUrl: { type: String, required: false, default: '', @@ -41,7 +41,7 @@ /** * Used to show the commit short_sha that links to the commit url. */ - short_sha: { + shortSha: { type: String, required: false, default: '', @@ -85,7 +85,7 @@ * @returns {Boolean} */ hasCommitRef() { - return this.commit_ref && this.commit_ref.name && this.commit_ref.ref_url; + return this.commitRef && this.commitRef.name && this.commitRef.ref_url; }, /** @@ -125,15 +125,15 @@ <a v-if="hasCommitRef" class="monospace branch-name" - :href="commit_ref.ref_url"> - {{commit_ref.name}} + :href="commitRef.ref_url"> + {{commitRef.name}} </a> <div v-html="commitIconSvg" class="commit-icon"></div> <a class="commit-id monospace" - :href="commit_url"> - {{short_sha}} + :href="commitUrl"> + {{shortSha}} </a> <p class="commit-title"> @@ -149,7 +149,7 @@ </a> <a class="commit-row-message" - :href="commit_url"> + :href="commitUrl"> {{title}} </a> </span> diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js.es6 index 156506ef28f..35177d9b651 100644 --- a/spec/javascripts/environments/environment_external_url_spec.js.es6 +++ b/spec/javascripts/environments/environment_external_url_spec.js.es6 @@ -12,7 +12,7 @@ describe('External URL Component', () => { const component = new window.gl.environmentsList.ExternalUrlComponent({ el: document.querySelector('.test-dom-element'), propsData: { - external_url: externalURL, + externalUrl: externalURL, }, }); diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6 index 29449bbbd9e..d54ec9aa5da 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js.es6 +++ b/spec/javascripts/environments/environment_rollback_spec.js.es6 @@ -13,8 +13,8 @@ describe('Rollback Component', () => { const component = new window.gl.environmentsList.RollbackComponent({ el: document.querySelector('.test-dom-element'), propsData: { - retry_url: retryURL, - is_last_deployment: true, + retryUrl: retryURL, + isLastDeployment: true, }, }); @@ -25,8 +25,8 @@ describe('Rollback Component', () => { const component = new window.gl.environmentsList.RollbackComponent({ el: document.querySelector('.test-dom-element'), propsData: { - retry_url: retryURL, - is_last_deployment: true, + retryUrl: retryURL, + isLastDeployment: true, }, }); @@ -38,8 +38,8 @@ describe('Rollback Component', () => { const component = new window.gl.environmentsList.RollbackComponent({ el: document.querySelector('.test-dom-element'), propsData: { - retry_url: retryURL, - is_last_deployment: false, + retryUrl: retryURL, + isLastDeployment: false, }, }); diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js.es6 index b842be4da61..84a41b2bf46 100644 --- a/spec/javascripts/environments/environment_stop_spec.js.es6 +++ b/spec/javascripts/environments/environment_stop_spec.js.es6 @@ -13,7 +13,7 @@ describe('Stop Component', () => { component = new window.gl.environmentsList.StopComponent({ el: document.querySelector('.test-dom-element'), propsData: { - stop_url: stopURL, + stopUrl: stopURL, }, }); }); -- cgit v1.2.1 From d98f03d3f5b66d84950f01926f85f8892d55a3d9 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 8 Dec 2016 12:06:49 +0000 Subject: Adds tests --- .../components/environment_actions.js.es6 | 8 +++-- .../javascripts/vue_common_component/commit.js.es6 | 2 +- .../environments/environment_actions_spec.js.es6 | 32 ++++++++++++++++++- .../environment_external_url_spec.js.es6 | 2 +- .../environments/environment_rollback_spec.js.es6 | 6 ++-- .../vue_common_components/commit_spec.js.es6 | 37 ++++++++++++---------- 6 files changed, 62 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6 index 02cdb305217..d35a0441961 100644 --- a/app/assets/javascripts/environments/components/environment_actions.js.es6 +++ b/app/assets/javascripts/environments/components/environment_actions.js.es6 @@ -23,8 +23,8 @@ <div class="inline"> <div class="dropdown"> <a class="dropdown-new btn btn-default" data-toggle="dropdown"> - <span v-html='playIconSvg'></span> - <i class="fa fa-caret-down"></i> + <span class="js-dropdown-play-icon-container" v-html='playIconSvg'></span> + <i class"=fa fa-caret-down"></i> </a> <ul class="dropdown-menu dropdown-menu-align-right"> @@ -33,7 +33,9 @@ data-method="post" rel="nofollow" class="js-manual-action-link"> - <span v-html='playIconSvg'></span> + + <span class="js-action-play-icon-container" v-html='playIconSvg'></span> + <span> {{action.name}} </span> diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_common_component/commit.js.es6 index 1f6995a645a..f6f02cc1cd0 100644 --- a/app/assets/javascripts/vue_common_component/commit.js.es6 +++ b/app/assets/javascripts/vue_common_component/commit.js.es6 @@ -129,7 +129,7 @@ {{commitRef.name}} </a> - <div v-html="commitIconSvg" class="commit-icon"></div> + <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div> <a class="commit-id monospace" :href="commitUrl"> diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6 index 76e81233e89..4bae3f30bb5 100644 --- a/spec/javascripts/environments/environment_actions_spec.js.es6 +++ b/spec/javascripts/environments/environment_actions_spec.js.es6 @@ -8,7 +8,7 @@ describe('Actions Component', () => { fixture.load('environments/element.html'); }); - it('Should render a dropdown with the provided actions', () => { + it('should render a dropdown with the provided actions', () => { const actionsMock = [ { name: 'bar', @@ -24,6 +24,7 @@ describe('Actions Component', () => { el: document.querySelector('.test-dom-element'), propsData: { actions: actionsMock, + playIconSvg: '<svg></svg>', }, }); @@ -34,4 +35,33 @@ describe('Actions Component', () => { component.$el.querySelector('.dropdown-menu li a').getAttribute('href'), ).toEqual(actionsMock[0].play_path); }); + + it('should render a dropdown with the provided svg', () => { + const actionsMock = [ + { + name: 'bar', + play_path: 'https://gitlab.com/play', + }, + { + name: 'foo', + play_path: '#', + }, + ]; + + const component = new window.gl.environmentsList.ActionsComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + actions: actionsMock, + playIconSvg: '<svg></svg>', + }, + }); + + expect( + component.$el.querySelector('.js-dropdown-play-icon-container').children, + ).toContain('svg'); + + expect( + component.$el.querySelector('.js-action-play-icon-container').children, + ).toContain('svg'); + }); }); diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js.es6 index 35177d9b651..9f82567c35b 100644 --- a/spec/javascripts/environments/environment_external_url_spec.js.es6 +++ b/spec/javascripts/environments/environment_external_url_spec.js.es6 @@ -7,7 +7,7 @@ describe('External URL Component', () => { fixture.load('environments/element.html'); }); - it('should link to the provided external_url', () => { + it('should link to the provided externalUrl prop', () => { const externalURL = 'https://gitlab.com'; const component = new window.gl.environmentsList.ExternalUrlComponent({ el: document.querySelector('.test-dom-element'), diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6 index d54ec9aa5da..77ba0ab38ec 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js.es6 +++ b/spec/javascripts/environments/environment_rollback_spec.js.es6 @@ -9,7 +9,7 @@ describe('Rollback Component', () => { fixture.load('environments/element.html'); }); - it('Should link to the provided retry_url', () => { + it('Should link to the provided retryUrl', () => { const component = new window.gl.environmentsList.RollbackComponent({ el: document.querySelector('.test-dom-element'), propsData: { @@ -21,7 +21,7 @@ describe('Rollback Component', () => { expect(component.$el.getAttribute('href')).toEqual(retryURL); }); - it('Should render Re-deploy label when is_last_deployment is true', () => { + it('Should render Re-deploy label when isLastDeployment is true', () => { const component = new window.gl.environmentsList.RollbackComponent({ el: document.querySelector('.test-dom-element'), propsData: { @@ -34,7 +34,7 @@ describe('Rollback Component', () => { }); - it('Should render Rollback label when is_last_deployment is false', () => { + it('Should render Rollback label when isLastDeployment is false', () => { const component = new window.gl.environmentsList.RollbackComponent({ el: document.querySelector('.test-dom-element'), propsData: { diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_common_components/commit_spec.js.es6 index d170517dd9b..26dfdb94aae 100644 --- a/spec/javascripts/vue_common_components/commit_spec.js.es6 +++ b/spec/javascripts/vue_common_components/commit_spec.js.es6 @@ -10,12 +10,12 @@ describe('Commit component', () => { el: document.querySelector('.test-commit-container'), propsData: { tag: false, - commit_ref: { + commitRef: { name: 'master', ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', }, - commit_url: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067', - short_sha: 'b7836edd', + commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067', + shortSha: 'b7836edd', title: 'Commit message', author: { avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png', @@ -34,18 +34,19 @@ describe('Commit component', () => { props = { tag: true, - commit_ref: { + commitRef: { name: 'master', ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', }, - commit_url: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067', - short_sha: 'b7836edd', + commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067', + shortSha: 'b7836edd', title: 'Commit message', author: { avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png', web_url: 'https://gitlab.com/jschatz1', username: 'jschatz1', }, + commitIconSvg: '<svg></svg>', }; component = new window.gl.CommitComponent({ @@ -59,20 +60,24 @@ describe('Commit component', () => { }); it('should render a link to the ref url', () => { - expect(component.$el.querySelector('.branch-name').getAttribute('href')).toEqual(props.commit_ref.ref_url); + expect(component.$el.querySelector('.branch-name').getAttribute('href')).toEqual(props.commitRef.ref_url); }); it('should render the ref name', () => { - expect(component.$el.querySelector('.branch-name').textContent).toContain(props.commit_ref.name); + expect(component.$el.querySelector('.branch-name').textContent).toContain(props.commitRef.name); }); it('should render the commit short sha with a link to the commit url', () => { - expect(component.$el.querySelector('.commit-id').getAttribute('href')).toEqual(props.commit_url); - expect(component.$el.querySelector('.commit-id').textContent).toContain(props.short_sha); + expect(component.$el.querySelector('.commit-id').getAttribute('href')).toEqual(props.commitUrl); + expect(component.$el.querySelector('.commit-id').textContent).toContain(props.shortSha); + }); + + it('should render the given commitIconSvg', () => { + expect(component.$el.querySelector('.js-commit-icon').children).toContain('svg'); }); describe('Given commit title and author props', () => { - it('Should render a link to the author profile', () => { + it('should render a link to the author profile', () => { expect( component.$el.querySelector('.commit-title .avatar-image-container').getAttribute('href'), ).toEqual(props.author.web_url); @@ -91,7 +96,7 @@ describe('Commit component', () => { it('should render the commit title', () => { expect( component.$el.querySelector('a.commit-row-message').getAttribute('href'), - ).toEqual(props.commit_url); + ).toEqual(props.commitUrl); expect( component.$el.querySelector('a.commit-row-message').textContent, ).toContain(props.title); @@ -99,16 +104,16 @@ describe('Commit component', () => { }); describe('When commit title is not provided', () => { - it('Should render default message', () => { + it('should render default message', () => { fixture.set('<div class="test-commit-container"></div>'); props = { tag: false, - commit_ref: { + commitRef: { name: 'master', ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', }, - commit_url: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067', - short_sha: 'b7836edd', + commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067', + shortSha: 'b7836edd', title: null, author: {}, }; -- cgit v1.2.1 From c808c1c9c88ff3efba938e6259bb5a2f871edae6 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 8 Dec 2016 12:17:37 +0000 Subject: Adds CHANGELOG entry --- changelogs/unreleased/25374-svg-as-prop.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/25374-svg-as-prop.yml diff --git a/changelogs/unreleased/25374-svg-as-prop.yml b/changelogs/unreleased/25374-svg-as-prop.yml new file mode 100644 index 00000000000..45a71b55b3b --- /dev/null +++ b/changelogs/unreleased/25374-svg-as-prop.yml @@ -0,0 +1,4 @@ +--- +title: Resolve "Provide SVG as a prop instead of hiding and copy them in environments table" +merge_request: 7992 +author: -- cgit v1.2.1 From cf71650220c2d57ae176a47775c0ac3d6ed3d39a Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 8 Dec 2016 14:11:37 +0000 Subject: Fix broken test --- spec/features/environments_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index c7fe622c477..e1b97b31e5d 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -85,14 +85,14 @@ feature 'Environments page', :feature, :js do end scenario 'does show a play button' do - find('.dropdown-play-icon-container').click + find('.js-dropdown-play-icon-container').click expect(page).to have_content(manual.name.humanize) end scenario 'does allow to play manual action', js: true do expect(manual).to be_skipped - find('.dropdown-play-icon-container').click + find('.js-dropdown-play-icon-container').click expect(page).to have_content(manual.name.humanize) expect { click_link(manual.name.humanize) } -- cgit v1.2.1 From 2bc6f88430ba9bc3d5c03a8afc56f4be46a97dd6 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 9 Dec 2016 10:14:00 +0000 Subject: Changes after review --- .../javascripts/environments/components/environment_actions.js.es6 | 6 +++--- app/assets/javascripts/vue_common_component/commit.js.es6 | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6 index d35a0441961..7c743705d51 100644 --- a/app/assets/javascripts/environments/components/environment_actions.js.es6 +++ b/app/assets/javascripts/environments/components/environment_actions.js.es6 @@ -23,8 +23,8 @@ <div class="inline"> <div class="dropdown"> <a class="dropdown-new btn btn-default" data-toggle="dropdown"> - <span class="js-dropdown-play-icon-container" v-html='playIconSvg'></span> - <i class"=fa fa-caret-down"></i> + <span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span> + <i class="fa fa-caret-down"></i> </a> <ul class="dropdown-menu dropdown-menu-align-right"> @@ -34,7 +34,7 @@ rel="nofollow" class="js-manual-action-link"> - <span class="js-action-play-icon-container" v-html='playIconSvg'></span> + <span class="js-action-play-icon-container" v-html="playIconSvg"></span> <span> {{action.name}} diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_common_component/commit.js.es6 index f6f02cc1cd0..62a22e39a3b 100644 --- a/app/assets/javascripts/vue_common_component/commit.js.es6 +++ b/app/assets/javascripts/vue_common_component/commit.js.es6 @@ -39,7 +39,7 @@ }, /** - * Used to show the commit short_sha that links to the commit url. + * Used to show the commit short sha that links to the commit url. */ shortSha: { type: String, -- cgit v1.2.1 From aa0a7aa3b5ba59b772f4d0c0bd061d278295b01f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Fri, 9 Dec 2016 11:58:12 +0100 Subject: Add 8.12.10, 8.12.11, and 8.12.12 CHANGELOG.md items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [ci skip] Signed-off-by: Rémy Coutable <remy@rymai.me> --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f0e29a5d82..fb13db4dd1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -513,6 +513,21 @@ entry. - Fix broken Project API docs (Takuya Noguchi) - Migrate invalid project members (owner -> master) +## 8.12.12 (2016-12-08) + +- Replace MR access checks with use of MergeRequestsFinder +- Reenables /user API request to return private-token if user is admin and request is made with sudo + +## 8.12.11 (2016-12-02) + +- No changes + +## 8.12.10 (2016-11-28) + +- Fix information disclosure in `Projects::BlobController#update` +- Fix missing access checks on issue lookup using IssuableFinder +- Replace issue access checks with use of IssuableFinder + ## 8.12.9 (2016-11-07) - Fix XSS issue in Markdown autolinker -- cgit v1.2.1 From 9190cea21524f936c0ae8a9e754c861f13583fc5 Mon Sep 17 00:00:00 2001 From: Adam Niedzielski <adamsunday@gmail.com> Date: Fri, 9 Dec 2016 11:12:38 +0100 Subject: Do not reload diff for merge request made from fork when target branch in fork is updated The target branch of a merge request has to be a branch in the project for which the merge request is submitted. When a branch changes in a fork, it does not make sense to reload diffs of merge requests in the upstream project that use the same branch name as the target branch. Please note that it does make sense to reload diffs when the source branch changes. --- app/models/merge_request.rb | 4 +++- app/services/merge_requests/refresh_service.rb | 10 ++++---- ...efresh-main-when-fork-target-branch-updated.yml | 4 ++++ .../merge_requests/refresh_service_spec.rb | 28 +++++++++++++++------- 4 files changed, 32 insertions(+), 14 deletions(-) create mode 100644 changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index dd9f1a7c85b..ea3cf1cdaac 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -101,7 +101,9 @@ class MergeRequest < ActiveRecord::Base validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?] validate :validate_fork, unless: :closed_without_fork? - scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } + scope :by_source_or_target_branch, ->(branch_name) do + where("source_branch = :branch OR target_branch = :branch", branch: branch_name) + end scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } scope :of_projects, ->(ids) { where(target_project_id: ids) } diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index e4056306bc4..0a9563ed7e7 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -55,8 +55,9 @@ 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 = @project.merge_requests.opened. + by_source_or_target_branch(@branch_name).to_a + merge_requests += fork_merge_requests merge_requests = filter_merge_requests(merge_requests) merge_requests.each do |merge_request| @@ -157,13 +158,14 @@ 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 filter_merge_requests(merge_requests) end end def fork_merge_requests - @fork_merge_requests ||= @project.fork_merge_requests.opened + @fork_merge_requests ||= @project.fork_merge_requests.opened. + where(source_branch: @branch_name).to_a end def branch_added? diff --git a/changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml b/changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml new file mode 100644 index 00000000000..12b1460f388 --- /dev/null +++ b/changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml @@ -0,0 +1,4 @@ +--- +title: Do not reload diff for merge request made from fork when target branch in fork is updated +merge_request: 7973 +author: diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index bc340ff9d3c..7e3705983fb 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -126,17 +126,27 @@ describe MergeRequests::RefreshService, services: true do end context 'push to fork repo target branch' do - before do - service.new(@fork_project, @user).execute(@oldrev, @newrev, 'refs/heads/feature') - reload_mrs + describe 'changes to merge requests' do + before do + service.new(@fork_project, @user).execute(@oldrev, @newrev, 'refs/heads/feature') + reload_mrs + end + + it { expect(@merge_request.notes).to be_empty } + it { expect(@merge_request).to be_open } + it { expect(@fork_merge_request.notes).to be_empty } + it { expect(@fork_merge_request).to be_open } + it { expect(@build_failed_todo).to be_pending } + it { expect(@fork_build_failed_todo).to be_pending } end - it { expect(@merge_request.notes).to be_empty } - it { expect(@merge_request).to be_open } - it { expect(@fork_merge_request.notes).to be_empty } - it { expect(@fork_merge_request).to be_open } - it { expect(@build_failed_todo).to be_pending } - it { expect(@fork_build_failed_todo).to be_pending } + describe 'merge request diff' do + it 'does not reload the diff of the merge request made from fork' do + expect do + service.new(@fork_project, @user).execute(@oldrev, @newrev, 'refs/heads/feature') + end.not_to change { @fork_merge_request.reload.merge_request_diff } + end + end end context 'push to origin repo target branch after fork project was removed' do -- cgit v1.2.1 From 1038d37671084beb9bbf9458347d4e02cb2c22c3 Mon Sep 17 00:00:00 2001 From: Sean Packham <seanpackham@gitlab.com> Date: Wed, 28 Sep 2016 14:45:28 +0100 Subject: Improvements to setting up ssh --- doc/ssh/README.md | 154 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 93 insertions(+), 61 deletions(-) diff --git a/doc/ssh/README.md b/doc/ssh/README.md index d6a0979f6ec..ab52231cd56 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -1,12 +1,21 @@ # SSH -## SSH keys - -An SSH key allows you to establish a secure connection between your -computer and GitLab. Before generating an SSH key in your shell, check if your system -already has one by running the following command: - -**Windows Command Line:** +Git is a distributed version control system, which means you can work locally +but you can also share or "push" your changes to other servers. +Before you can push your changes to a GitLab server +you need a secure communication channel for sharing information. +GitLab uses Public-key or asymmetric cryptography +which "encrypts" a communication channel by locking it with your "private key" +and allows trusted parties to unlock it with your "public key". +If someone does not have your public key they cannot access the unencrypted message. + +## Locating an existing SSH key pair + +Before generating a new SSH key check if your system already has one +at the default location by opening a shell, or Command Prompt on Windows, +and running the following command: + +**Windows Command Prompt:** ```bash type %userprofile%\.ssh\id_rsa.pub ``` @@ -15,40 +24,52 @@ type %userprofile%\.ssh\id_rsa.pub cat ~/.ssh/id_rsa.pub ``` -If you see a long string starting with `ssh-rsa`, you can skip the `ssh-keygen` step. +If you see a string starting with `ssh-rsa` you already have an SSH key pair +and you can skip the next step **Generating a new SSH key pair** +and continue onto **Copying your public SSH key to the clipboard**. +If you don't see the string or would like to generate a SSH key pair with a custom name +continue onto the next step. + +## Generating a new SSH key pair To generate a new SSH key, use the following command: + +**GNU/Linux/Mac/PowerShell:** ```bash ssh-keygen -t rsa -C "$your_email" ``` -This command will prompt you for a location and filename to store the key -pair and for a password. When prompted for the location and filename, just -press enter to use the default. If you use a different name, the key will not -be used automatically. -Note: It is a best practice to use a password for an SSH key, but it is not -required and you can skip creating a password by pressing enter. +**Windows:** +On Windows you will need to download +[PuttyGen](http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html) +and follow this documentation +[article](https://the.earth.li/~sgtatham/putty/0.67/htmldoc/Chapter8.html#pubkey-puttygen) +to generate a SSH key pair. -If you want to change the password of your key later, you can use the following -command: `ssh-keygen -p <keyname>` +### Provide a file path -Use the command below to show your public key: +You will be prompted to input a file path to save your key pair to. -**Windows Command Line:** -```bash -type %userprofile%\.ssh\id_rsa.pub -``` -**GNU/Linux/Mac/PowerShell:** -```bash -cat ~/.ssh/id_rsa.pub -``` +If you don't already have an SSH key pair use the suggested path by pressing enter. +Using the suggested path will allow your SSH client +to automatically use the key pair with no additional configuration. + +If you already have a key pair with the suggested file path you will need to input a new file path +and declare what host this key pair will be used for in your `.ssh/config` file, +see **Working with non-default SSH key pair paths** for more information. + +### Provide a password -Copy-paste the key to the 'My SSH Keys' section under the 'SSH' tab in your -user profile. Please copy the complete key starting with `ssh-rsa` and ending -with your username and host. +Once you have input a file path you will be prompted to input a password to secure your SSH key pair. +Note: It is a best practice to use a password for an SSH key pair, +but it is not required and you can skip creating a password by pressing enter. -To copy your public key to the clipboard, use the code below. Depending on your -OS you'll need to use a different command: +If you want to change the password of your key, you can use the following command: +`ssh-keygen -p <keyname>` + +## Copying your public SSH key to the clipboard + +To copy your public key to the clipboard, use the appropriate code for you operating system below: **Windows Command Line:** ```bash @@ -70,6 +91,48 @@ pbcopy < ~/.ssh/id_rsa.pub xclip -sel clip < ~/.ssh/id_rsa.pub ``` +## Adding your public SSH key to GitLab + +Navigate to the 'SSH Keys' tab in you 'Profile Settings'. +Paste your key in the 'Key' section and give it a relevant 'Title'. +Use an identifiable title like 'Work Laptop - Windows 7' or 'Home MacBook Pro 15'. + +If you manually copied your public SSH key make sure you copied the entire key +starting with `ssh-rsa` and ending with your email. + +## Working with non-default SSH key pair paths + +If you used a non-default file path for your GitLab SSH key pair, +you must configure your SSH client to find your GitLab SSH private key +for connections to your GitLab server (perhaps gitlab.com). + +For OpenSSH clients this is configured in the `~/.ssh/config` file. +Below are two example host configurations using their own key: + +``` +# GitLab.com server +Host gitlab.com +RSAAuthentication yes +IdentityFile ~/folder1/private-key-filename +User mygitlabusername + +# Private GitLab server +Host gitlab.company.com +RSAAuthentication yes +IdentityFile ~/folder2/private-key-filename +``` + +Note in the gitlab.com example above a username was specified +to override the default chosen by OpenSSH (your local username). +This is only required if your local and remote usernames differ. + +Due to the wide variety of SSH clients and their very large number of configuration options, +further explanation of these topics is beyond the scope of this document. + +Public SSH keys need to be unique, as they will bind to your account. +Your SSH key is the only identifier you'll have when pushing code via SSH. +That's why it needs to uniquely map to a single user. + ## Deploy keys Deploy keys allow read-only access to multiple projects with a single SSH @@ -99,34 +162,3 @@ Deploy keys can be shared between projects, you just need to add them to each pr ### Eclipse How to add your ssh key to Eclipse: https://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration - -## Tip: Non-default OpenSSH key file names or locations - -If, for whatever reason, you decide to specify a non-default location and filename for your GitLab SSH key pair, you must configure your SSH client to find your GitLab SSH private key for connections to your GitLab server (perhaps gitlab.com). For OpenSSH clients, this is handled in the `~/.ssh/config` file with a stanza similar to the following: - -``` -# -# Main gitlab.com server -# -Host gitlab.com -RSAAuthentication yes -IdentityFile ~/my-ssh-key-directory/my-gitlab-private-key-filename -User mygitlabusername -``` - -Another example -``` -# -# Our company's internal GitLab server -# -Host my-gitlab.company.com -RSAAuthentication yes -IdentityFile ~/my-ssh-key-directory/company-com-private-key-filename -``` - -Note in the gitlab.com example above a username was specified to override the default chosen by OpenSSH (your local username). This is only required if your local and remote usernames differ. - -Due to the wide variety of SSH clients and their very large number of configuration options, further explanation of these topics is beyond the scope of this document. - -Public SSH keys need to be unique, as they will bind to your account. Your SSH key is the only identifier you'll -have when pushing code via SSH. That's why it needs to uniquely map to a single user. \ No newline at end of file -- cgit v1.2.1 From 81bfa925edcd37bc007a2f39c3b01ddead77f1a0 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Fri, 9 Dec 2016 12:49:07 +0100 Subject: Refactor SSH keys docs [ci skip] --- doc/ssh/README.md | 135 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 70 insertions(+), 65 deletions(-) diff --git a/doc/ssh/README.md b/doc/ssh/README.md index ab52231cd56..9803937fcf9 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -5,7 +5,7 @@ but you can also share or "push" your changes to other servers. Before you can push your changes to a GitLab server you need a secure communication channel for sharing information. GitLab uses Public-key or asymmetric cryptography -which "encrypts" a communication channel by locking it with your "private key" +which encrypts a communication channel by locking it with your "private key" and allows trusted parties to unlock it with your "public key". If someone does not have your public key they cannot access the unencrypted message. @@ -16,10 +16,13 @@ at the default location by opening a shell, or Command Prompt on Windows, and running the following command: **Windows Command Prompt:** + ```bash type %userprofile%\.ssh\id_rsa.pub ``` -**GNU/Linux/Mac/PowerShell:** + +**GNU/Linux / macOS / PowerShell:** + ```bash cat ~/.ssh/id_rsa.pub ``` @@ -27,78 +30,82 @@ cat ~/.ssh/id_rsa.pub If you see a string starting with `ssh-rsa` you already have an SSH key pair and you can skip the next step **Generating a new SSH key pair** and continue onto **Copying your public SSH key to the clipboard**. -If you don't see the string or would like to generate a SSH key pair with a custom name -continue onto the next step. +If you don't see the string or would like to generate a SSH key pair with a +custom name continue onto the next step. ## Generating a new SSH key pair -To generate a new SSH key, use the following command: +1. To generate a new SSH key, use the following command: -**GNU/Linux/Mac/PowerShell:** -```bash -ssh-keygen -t rsa -C "$your_email" -``` + **GNU/Linux / macOS:** -**Windows:** -On Windows you will need to download -[PuttyGen](http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html) -and follow this documentation -[article](https://the.earth.li/~sgtatham/putty/0.67/htmldoc/Chapter8.html#pubkey-puttygen) -to generate a SSH key pair. + ```bash + ssh-keygen -t rsa -C "GitLab" -b 4096 + ``` -### Provide a file path + **Windows:** -You will be prompted to input a file path to save your key pair to. + On Windows you will need to download + [PuttyGen](http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html) + and follow this [documentation article][winputty] to generate a SSH key pair. -If you don't already have an SSH key pair use the suggested path by pressing enter. -Using the suggested path will allow your SSH client -to automatically use the key pair with no additional configuration. +1. Next, you will be prompted to input a file path to save your key pair to. -If you already have a key pair with the suggested file path you will need to input a new file path -and declare what host this key pair will be used for in your `.ssh/config` file, -see **Working with non-default SSH key pair paths** for more information. + If you don't already have an SSH key pair use the suggested path by pressing + enter. Using the suggested path will allow your SSH client + to automatically use the key pair with no additional configuration. -### Provide a password + If you already have a key pair with the suggested file path, you will need + to input a new file path and declare what host this key pair will be used + for in your `.ssh/config` file, see **Working with non-default SSH key pair paths** + for more information. -Once you have input a file path you will be prompted to input a password to secure your SSH key pair. -Note: It is a best practice to use a password for an SSH key pair, -but it is not required and you can skip creating a password by pressing enter. +1. Once you have input a file path you will be prompted to input a password to + secure your SSH key pair. It is a best practice to use a password for an SSH + key pair, but it is not required and you can skip creating a password by + pressing enter. -If you want to change the password of your key, you can use the following command: -`ssh-keygen -p <keyname>` + >**Note:** + If you want to change the password of your key, you can use `ssh-keygen -p <keyname>`. -## Copying your public SSH key to the clipboard +1. The next step is to copy the public key as we will need it afterwards. -To copy your public key to the clipboard, use the appropriate code for you operating system below: + To copy your public key to the clipboard, use the appropriate code for your + operating system below: -**Windows Command Line:** -```bash -type %userprofile%\.ssh\id_rsa.pub | clip -``` + **macOS:** -**Windows PowerShell:** -```bash -cat ~/.ssh/id_rsa.pub | clip -``` + ```bash + pbcopy < ~/.ssh/id_rsa.pub + ``` -**Mac:** -```bash -pbcopy < ~/.ssh/id_rsa.pub -``` + **GNU/Linux (requires the xclip package):** -**GNU/Linux (requires xclip):** -```bash -xclip -sel clip < ~/.ssh/id_rsa.pub -``` + ```bash + xclip -sel clip < ~/.ssh/id_rsa.pub + ``` + + **Windows Command Line:** + + ```bash + type %userprofile%\.ssh\id_rsa.pub | clip + ``` -## Adding your public SSH key to GitLab + **Windows PowerShell:** -Navigate to the 'SSH Keys' tab in you 'Profile Settings'. -Paste your key in the 'Key' section and give it a relevant 'Title'. -Use an identifiable title like 'Work Laptop - Windows 7' or 'Home MacBook Pro 15'. + ```bash + cat ~/.ssh/id_rsa.pub | clip + ``` -If you manually copied your public SSH key make sure you copied the entire key -starting with `ssh-rsa` and ending with your email. +1. The final step is to add your public SSH key to GitLab. + + Navigate to the 'SSH Keys' tab in you 'Profile Settings'. + Paste your key in the 'Key' section and give it a relevant 'Title'. + Use an identifiable title like 'Work Laptop - Windows 7' or + 'Home MacBook Pro 15'. + + If you manually copied your public SSH key make sure you copied the entire + key starting with `ssh-rsa` and ending with your email. ## Working with non-default SSH key pair paths @@ -113,21 +120,17 @@ Below are two example host configurations using their own key: # GitLab.com server Host gitlab.com RSAAuthentication yes -IdentityFile ~/folder1/private-key-filename -User mygitlabusername +IdentityFile ~/.ssh/config/private-key-filename-01 # Private GitLab server Host gitlab.company.com RSAAuthentication yes -IdentityFile ~/folder2/private-key-filename +IdentityFile ~/.ssh/config/private-key-filename ``` -Note in the gitlab.com example above a username was specified -to override the default chosen by OpenSSH (your local username). -This is only required if your local and remote usernames differ. - -Due to the wide variety of SSH clients and their very large number of configuration options, -further explanation of these topics is beyond the scope of this document. +Due to the wide variety of SSH clients and their very large number of +configuration options, further explanation of these topics is beyond the scope +of this document. Public SSH keys need to be unique, as they will bind to your account. Your SSH key is the only identifier you'll have when pushing code via SSH. @@ -152,13 +155,15 @@ If you want to add the same key to another project, please enable it in the list that says 'Deploy keys from projects available to you'. All the deploy keys of all the projects you have access to are available. This project access can happen through being a direct member of the project, or through -a group. See `def accessible_deploy_keys` in `app/models/user.rb` for more -information. +a group. -Deploy keys can be shared between projects, you just need to add them to each project. +Deploy keys can be shared between projects, you just need to add them to each +project. ## Applications ### Eclipse How to add your ssh key to Eclipse: https://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration + +[winputty]: https://the.earth.li/~sgtatham/putty/0.67/htmldoc/Chapter8.html#pubkey-puttygen -- cgit v1.2.1 From 383e6b552e4b3ae9b2fdf9c1401dca2aef8e98e7 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 9 Dec 2016 12:13:54 +0000 Subject: Fix broken test --- spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6 b/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6 index ad51367bb32..1c5bf8887f8 100644 --- a/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6 +++ b/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6 @@ -11,7 +11,7 @@ describe('Custom Event Polyfill', () => { expect(e.type).toEqual('foo'); expect(e.bubbles).toBe(false); expect(e.cancelable).toBe(false); - expect(e.detail).toBe(null); + expect(e.detail).toBe(undefined); }); it('should create a `CustomEvent` instance with a `details` object', () => { @@ -29,7 +29,7 @@ describe('Custom Event Polyfill', () => { expect(e.type).toEqual('bar'); expect(e.bubbles).toBe(true); expect(e.cancelable).toBe(false); - expect(e.detail).toBe(null); + expect(e.detail).toBe(undefined); }); it('should create a `CustomEvent` instance with a `cancelable` boolean', () => { @@ -38,6 +38,6 @@ describe('Custom Event Polyfill', () => { expect(e.type).toEqual('bar'); expect(e.bubbles).toBe(false); expect(e.cancelable).toBe(true); - expect(e.detail).toBe(null); + expect(e.detail).toBe(undefined); }); }); -- cgit v1.2.1 From 858ec6048223f2eec8c1386d33e3a6c5827233cb Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Fri, 9 Dec 2016 16:59:23 +0200 Subject: Handling OAuth2 errors --- app/controllers/import/bitbucket_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 12716d60e7d..b9cc6556140 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -2,7 +2,7 @@ class Import::BitbucketController < Import::BaseController before_action :verify_bitbucket_import_enabled before_action :bitbucket_auth, except: :callback - rescue_from OAuth::Error, with: :bitbucket_unauthorized + rescue_from OAuth2::Error, with: :bitbucket_unauthorized rescue_from Bitbucket::Error::Unauthorized, with: :bitbucket_unauthorized def callback -- cgit v1.2.1 From cc30a9f7ed436fd906c1e24a195414f2f84ee61c Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Fri, 9 Dec 2016 17:25:42 +0200 Subject: Fix rubocop[ci skip] --- lib/gitlab/bitbucket_import/importer.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 8852f5b0f3f..e00a90da980 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -1,10 +1,10 @@ module Gitlab module BitbucketImport class Importer - LABELS = [{ title: 'bug', color: '#FF0000'}, - { title: 'enhancement', color: '#428BCA'}, - { title: 'proposal', color: '#69D100'}, - { title: 'task', color: '#7F8C8D'}].freeze + LABELS = [{ title: 'bug', color: '#FF0000' }, + { title: 'enhancement', color: '#428BCA' }, + { title: 'proposal', color: '#69D100' }, + { title: 'task', color: '#7F8C8D' }].freeze attr_reader :project, :client -- cgit v1.2.1 From 4f264c793046307f67b2b3a365cb2a41f1fc0eda Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra <dimitriehoekstra@gmail.com> Date: Fri, 9 Dec 2016 16:46:03 +0100 Subject: Updates the font weight of button styles because of the change to system fonts --- app/assets/stylesheets/framework/buttons.scss | 2 +- changelogs/unreleased/update-button-font-weight.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/update-button-font-weight.yml diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 8da3da2ad08..1c7b2f4df7c 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -1,7 +1,7 @@ @mixin btn-default { border-radius: 3px; font-size: $gl-font-size; - font-weight: 500; + font-weight: 400; padding: $gl-vert-padding $gl-btn-padding; &:focus, diff --git a/changelogs/unreleased/update-button-font-weight.yml b/changelogs/unreleased/update-button-font-weight.yml new file mode 100644 index 00000000000..ddb3c1c8da4 --- /dev/null +++ b/changelogs/unreleased/update-button-font-weight.yml @@ -0,0 +1,4 @@ +--- +title: Updates the font weight of button styles because of the change to system fonts +merge_request: +author: -- cgit v1.2.1 From 593c912151eaef865d31fc2a8307ef6d337c2349 Mon Sep 17 00:00:00 2001 From: Robert Schilling <rschilling@student.tugraz.at> Date: Mon, 5 Dec 2016 15:40:53 +0100 Subject: Grapify the service API --- app/models/project_services/buildkite_service.rb | 3 +- app/models/project_services/drone_ci_service.rb | 3 +- .../project_services/emails_on_push_service.rb | 18 +- app/models/project_services/hipchat_service.rb | 6 +- app/models/project_services/irker_service.rb | 3 +- doc/api/services.md | 102 +++- lib/api/helpers.rb | 11 - lib/api/services.rb | 626 +++++++++++++++++++-- .../project_services/hipchat_service_spec.rb | 2 +- spec/requests/api/services_spec.rb | 5 +- spec/support/services_shared_context.rb | 6 + 11 files changed, 700 insertions(+), 85 deletions(-) diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb index 86a06321e21..fe6d7aabb22 100644 --- a/app/models/project_services/buildkite_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -3,7 +3,8 @@ require "addressable/uri" class BuildkiteService < CiService ENDPOINT = "https://buildkite.com" - prop_accessor :project_url, :token, :enable_ssl_verification + prop_accessor :project_url, :token + boolean_accessor :enable_ssl_verification validates :project_url, presence: true, url: true, if: :activated? validates :token, presence: true, if: :activated? diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index 5e4dd101c53..adc78a427ee 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -1,5 +1,6 @@ class DroneCiService < CiService - prop_accessor :drone_url, :token, :enable_ssl_verification + prop_accessor :drone_url, :token + boolean_accessor :enable_ssl_verification validates :drone_url, presence: true, url: true, if: :activated? validates :token, presence: true, if: :activated? diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index e0083c43adb..79285cbd26d 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -1,6 +1,6 @@ class EmailsOnPushService < Service - prop_accessor :send_from_committer_email - prop_accessor :disable_diffs + boolean_accessor :send_from_committer_email + boolean_accessor :disable_diffs prop_accessor :recipients validates :recipients, presence: true, if: :activated? @@ -24,20 +24,20 @@ class EmailsOnPushService < Service return unless supported_events.include?(push_data[:object_kind]) EmailsOnPushWorker.perform_async( - project_id, - recipients, - push_data, - send_from_committer_email: send_from_committer_email?, - disable_diffs: disable_diffs? + project_id, + recipients, + push_data, + send_from_committer_email: send_from_committer_email?, + disable_diffs: disable_diffs? ) end def send_from_committer_email? - self.send_from_committer_email == "1" + Gitlab::Utils.to_boolean(self.send_from_committer_email) end def disable_diffs? - self.disable_diffs == "1" + Gitlab::Utils.to_boolean(self.disable_diffs) end def fields diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 660a8ae3421..915f6fed74c 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -8,8 +8,8 @@ class HipchatService < Service ul ol li dl dt dd ] - prop_accessor :token, :room, :server, :notify, :color, :api_version - boolean_accessor :notify_only_broken_builds + prop_accessor :token, :room, :server, :color, :api_version + boolean_accessor :notify_only_broken_builds, :notify validates :token, presence: true, if: :activated? def initialize_properties @@ -75,7 +75,7 @@ class HipchatService < Service end def message_options(data = nil) - { notify: notify.present? && notify == '1', color: message_color(data) } + { notify: notify.present? && Gitlab::Utils.to_boolean(notify), color: message_color(data) } end def create_message(data) diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index ce7d1c5d5b1..7355918feab 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -2,7 +2,8 @@ require 'uri' class IrkerService < Service prop_accessor :server_host, :server_port, :default_irc_uri - prop_accessor :colorize_messages, :recipients, :channels + prop_accessor :recipients, :channels + boolean_accessor :colorize_messages validates :recipients, presence: true, if: :activated? before_validation :get_channels diff --git a/doc/api/services.md b/doc/api/services.md index a5d733fe6c7..acb54448664 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -139,6 +139,40 @@ Get Buildkite service settings for a project. GET /projects/:id/services/buildkite ``` +## Build-Emails + +Get emails for GitLab CI builds. + +### Create/Edit Build-Emails service + +Set Build-Emails service for a project. + +``` +PUT /projects/:id/services/builds-email +``` + +Parameters: + +- `recipients` (**required**) - Comma-separated list of recipient email addresses +- `add_pusher` (optional) - Add pusher to recipients list +- `notify_only_broken_builds` (optional) -Notify only broken builds + +### Delete Build-Emails service + +Delete Build-Emails service for a project. + +``` +DELETE /projects/:id/services/builds-email +``` + +### Get Build-Emails service settings + +Get Build-Emails service settings for a project. + +``` +GET /projects/:id/services/builds-email +``` + ## Campfire Simple web-based real-time group chat @@ -476,12 +510,11 @@ PUT /projects/:id/services/jira | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `active` | boolean| no | Enable/disable the JIRA service. | | `url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project, e.g., `https://jira.example.com`. | | `project_key` | string | yes | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. | | `username` | string | no | The username of the user created to be used with GitLab/JIRA. | | `password` | string | no | The password of the user created to be used with GitLab/JIRA. | -| `jira_issue_transition_id` | string | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. | +| `jira_issue_transition_id` | integer | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. | ### Delete JIRA service @@ -491,6 +524,71 @@ Remove all previously JIRA settings from a project. DELETE /projects/:id/services/jira ``` +## Mattermost Slash Commands + +Ability to receive slash commands from a Mattermost chat instance. + +### Create/Edit Mattermost Slash Command service + +Set Mattermost Slash Command for a project. + +``` +PUT /projects/:id/services/mattermost-slash-commands +``` + +Parameters: + +- `token` (**required**) - The Mattermost token + +### Delete Mattermost Slash Command service + +Delete Mattermost Slash Command service for a project. + +``` +DELETE /projects/:id/services/mattermost-slash-commands +``` + +### Get Mattermost Slash Command service settings + +Get Mattermost Slash Command service settings for a project. + +``` +GET /projects/:id/services/mattermost-slash-commands +``` + +## Pipeline-Emails + +Get emails for GitLab CI pipelines. + +### Create/Edit Pipeline-Emails service + +Set Pipeline-Emails service for a project. + +``` +PUT /projects/:id/services/pipelines-email +``` + +Parameters: + +- `recipients` (**required**) - Comma-separated list of recipient email addresses +- `notify_only_broken_builds` (optional) -Notify only broken pipelines + +### Delete Pipeline-Emails service + +Delete Pipeline-Emails service for a project. + +``` +DELETE /projects/:id/services/pipelines-email +``` + +### Get Pipeline-Emails service settings + +Get Pipeline-Emails service settings for a project. + +``` +GET /projects/:id/services/pipelines-email +``` + ## PivotalTracker Project Management Software (Source Commits Endpoint) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 7f94ede7940..16ac40b7142 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -93,17 +93,6 @@ module API end end - def project_service(project = user_project) - @project_service ||= project.find_or_initialize_service(params[:service_slug].underscore) - @project_service || not_found!("Service") - end - - def service_attributes - @service_attributes ||= project_service.fields.inject([]) do |arr, hash| - arr << hash[:name].to_sym - end - end - def find_group(id) if id =~ /^\d+$/ Group.find_by(id: id) diff --git a/lib/api/services.rb b/lib/api/services.rb index bc427705777..fde2e2746f1 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -1,84 +1,602 @@ module API - # Projects API class Services < Grape::API + services = { + 'asana' => [ + { + required: true, + name: :api_key, + type: String, + desc: 'User API token' + }, + { + required: false, + name: :restrict_to_branch, + type: String, + desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches' + } + ], + 'assembla' => [ + { + required: true, + name: :token, + type: String, + desc: 'The authentication token' + }, + { + required: false, + name: :subdomain, + type: String, + desc: 'Subdomain setting' + } + ], + 'bamboo' => [ + { + required: true, + name: :bamboo_url, + type: String, + desc: 'Bamboo root URL like https://bamboo.example.com' + }, + { + required: true, + name: :build_key, + type: String, + desc: 'Bamboo build plan key like' + }, + { + required: true, + name: :username, + type: String, + desc: 'A user with API access, if applicable' + }, + { + required: true, + name: :password, + type: String, + desc: 'Passord of the user' + } + ], + 'bugzilla' => [ + { + required: true, + name: :new_issue_url, + type: String, + desc: 'New issue URL' + }, + { + required: true, + name: :issues_url, + type: String, + desc: 'Issues URL' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'Project URL' + }, + { + required: false, + name: :description, + type: String, + desc: 'Description' + }, + { + required: false, + name: :title, + type: String, + desc: 'Title' + } + ], + 'buildkite' => [ + { + required: true, + name: :token, + type: String, + desc: 'Buildkite project GitLab token' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'The buildkite project URL' + }, + { + required: false, + name: :enable_ssl_verification, + type: Boolean, + desc: 'Enable SSL verification for communication' + } + ], + 'builds-email' => [ + { + required: true, + name: :recipients, + type: String, + desc: 'Comma-separated list of recipient email addresses' + }, + { + required: false, + name: :add_pusher, + type: Boolean, + desc: 'Add pusher to recipients list' + }, + { + required: false, + name: :notify_only_broken_builds, + type: Boolean, + desc: 'Notify only broken builds' + } + ], + 'campfire' => [ + { + required: true, + name: :token, + type: String, + desc: 'Campfire token' + }, + { + required: false, + name: :subdomain, + type: String, + desc: 'Campfire subdomain' + }, + { + required: false, + name: :room, + type: String, + desc: 'Campfire room' + }, + ], + 'custom-issue-tracker' => [ + { + required: true, + name: :new_issue_url, + type: String, + desc: 'New issue URL' + }, + { + required: true, + name: :issues_url, + type: String, + desc: 'Issues URL' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'Project URL' + }, + { + required: false, + name: :description, + type: String, + desc: 'Description' + }, + { + required: false, + name: :title, + type: String, + desc: 'Title' + } + ], + 'drone-ci' => [ + { + required: true, + name: :token, + type: String, + desc: 'Drone CI token' + }, + { + required: true, + name: :drone_url, + type: String, + desc: 'Drone CI URL' + }, + { + required: false, + name: :enable_ssl_verification, + type: Boolean, + desc: 'Enable SSL verification for communication' + } + ], + 'emails-on-push' => [ + { + required: true, + name: :recipients, + type: String, + desc: 'Comma-separated list of recipient email addresses' + }, + { + required: false, + name: :disable_diffs, + type: Boolean, + desc: 'Disable code diffs' + }, + { + required: false, + name: :send_from_committer_email, + type: Boolean, + desc: 'Send from committer' + } + ], + 'external-wiki' => [ + { + required: true, + name: :external_wiki_url, + type: String, + desc: 'The URL of the external Wiki' + } + ], + 'flowdock' => [ + { + required: true, + name: :token, + type: String, + desc: 'Flowdock token' + } + ], + 'gemnasium' => [ + { + required: true, + name: :api_key, + type: String, + desc: 'Your personal API key on gemnasium.com' + }, + { + required: true, + name: :token, + type: String, + desc: "The project's slug on gemnasium.com" + } + ], + 'hipchat' => [ + { + required: true, + name: :token, + type: String, + desc: 'The room token' + }, + { + required: false, + name: :room, + type: String, + desc: 'The room name or ID' + }, + { + required: false, + name: :color, + type: String, + desc: 'The room color' + }, + { + required: false, + name: :notify, + type: Boolean, + desc: 'Enable notifications' + }, + { + required: false, + name: :api_version, + type: String, + desc: 'Leave blank for default (v2)' + }, + { + required: false, + name: :server, + type: String, + desc: 'Leave blank for default. https://hipchat.example.com' + } + ], + 'irker' => [ + { + required: true, + name: :recipients, + type: String, + desc: 'Recipients/channels separated by whitespaces' + }, + { + required: false, + name: :default_irc_uri, + type: String, + desc: 'Default: irc://irc.network.net:6697' + }, + { + required: false, + name: :server_host, + type: String, + desc: 'Server host. Default localhost' + }, + { + required: false, + name: :server_port, + type: Integer, + desc: 'Server port. Default 6659' + }, + { + required: false, + name: :colorize_messages, + type: Boolean, + desc: 'Colorize messages' + } + ], + 'jira' => [ + { + required: true, + name: :url, + type: String, + desc: 'The URL to the JIRA project which is being linked to this GitLab project, e.g., https://jira.example.com' + }, + { + required: true, + name: :project_key, + type: String, + desc: 'The short identifier for your JIRA project, all uppercase, e.g., PROJ' + }, + { + required: false, + name: :username, + type: String, + desc: 'The username of the user created to be used with GitLab/JIRA' + }, + { + required: false, + name: :password, + type: String, + desc: 'The password of the user created to be used with GitLab/JIRA' + }, + { + required: false, + name: :jira_issue_transition_id, + type: Integer, + desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`' + } + ], + 'mattermost-slash-commands' => [ + { + required: true, + name: :token, + type: String, + desc: 'The Mattermost token' + } + ], + 'pipelines-email' => [ + { + required: true, + name: :recipients, + type: String, + desc: 'Comma-separated list of recipient email addresses' + }, + { + required: false, + name: :notify_only_broken_builds, + type: Boolean, + desc: 'Notify only broken builds' + } + ], + 'pivotaltracker' => [ + { + required: true, + name: :token, + type: String, + desc: 'The Pivotaltracker token' + }, + { + required: false, + name: :restrict_to_branch, + type: String, + desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.' + } + ], + 'pushover' => [ + { + required: true, + name: :api_key, + type: String, + desc: 'The application key' + }, + { + required: true, + name: :user_key, + type: String, + desc: 'The user key' + }, + { + required: true, + name: :priority, + type: String, + desc: 'The priority' + }, + { + required: true, + name: :device, + type: String, + desc: 'Leave blank for all active devices' + }, + { + required: true, + name: :sound, + type: String, + desc: 'The sound of the notification' + } + ], + 'redmine' => [ + { + required: true, + name: :new_issue_url, + type: String, + desc: 'The new issue URL' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'The project URL' + }, + { + required: true, + name: :issues_url, + type: String, + desc: 'The issues URL' + }, + { + required: false, + name: :description, + type: String, + desc: 'The description of the tracker' + } + ], + 'slack' => [ + { + required: true, + name: :webhook, + type: String, + desc: 'The Slack webhook. e.g. https://hooks.slack.com/services/...' + }, + { + required: false, + name: :new_issue_url, + type: String, + desc: 'The user name' + }, + { + required: false, + name: :channel, + type: String, + desc: 'The channel name' + } + ], + 'teamcity' => [ + { + required: true, + name: :teamcity_url, + type: String, + desc: 'TeamCity root URL like https://teamcity.example.com' + }, + { + required: true, + name: :build_type, + type: String, + desc: 'Build configuration ID' + }, + { + required: true, + name: :username, + type: String, + desc: 'A user with permissions to trigger a manual build' + }, + { + required: true, + name: :password, + type: String, + desc: 'The password of the user' + } + ] + }.freeze + + trigger_services = { + 'mattermost-slash-commands' => [ + { + name: :token, + type: String, + desc: 'The Mattermost token' + } + ] + }.freeze + resource :projects do before { authenticate! } before { authorize_admin_project } - # Set <service_slug> service for project - # - # Example Request: - # - # PUT /projects/:id/services/gitlab-ci - # - put ':id/services/:service_slug' do - if project_service - validators = project_service.class.validators.select do |s| - s.class == ActiveRecord::Validations::PresenceValidator && - s.attributes != [:project_id] + helpers do + def service_attributes(service) + service.fields.inject([]) do |arr, hash| + arr << hash[:name].to_sym end + end + end - required_attributes! validators.map(&:attributes).flatten.uniq - attrs = attributes_for_keys service_attributes + services.each do |service_slug, settings| + desc "Set #{service_slug} service for project" + params do + settings.each do |setting| + if setting[:required] + requires setting[:name], type: setting[:type], desc: setting[:desc] + else + optional setting[:name], type: setting[:type], desc: setting[:desc] + end + end + end + put ":id/services/#{service_slug}" do + service = user_project.find_or_initialize_service(service_slug.underscore) + service_params = declared_params(include_missing: false).merge(active: true) - if project_service.update_attributes(attrs.merge(active: true)) + if service.update_attributes(service_params) true else - not_found! + render_api_error!('400 Bad Request', 400) end end end - # Delete <service_slug> service for project - # - # Example Request: - # - # DELETE /project/:id/services/gitlab-ci - # - delete ':id/services/:service_slug' do - if project_service - attrs = service_attributes.inject({}) do |hash, key| - hash.merge!(key => nil) - end + desc "Delete a service for project" + params do + requires :service_slug, type: String, values: services.keys, desc: 'The name of the service' + end + delete ":id/services/:service_slug" do + service = user_project.find_or_initialize_service(params[:service_slug].underscore) - if project_service.update_attributes(attrs.merge(active: false)) - true - else - not_found! - end + attrs = service_attributes(service).inject({}) do |hash, key| + hash.merge!(key => nil) + end + + if service.update_attributes(attrs.merge(active: false)) + true + else + render_api_error!('400 Bad Request', 400) end end - # Get <service_slug> service settings for project - # - # Example Request: - # - # GET /project/:id/services/gitlab-ci - # - get ':id/services/:service_slug' do - present project_service, with: Entities::ProjectService, include_passwords: current_user.is_admin? + desc 'Get the service settings for project' do + success Entities::ProjectService + end + params do + requires :service_slug, type: String, values: services.keys, desc: 'The name of the service' + end + get ":id/services/:service_slug" do + service = user_project.find_or_initialize_service(params[:service_slug].underscore) + present service, with: Entities::ProjectService, include_passwords: current_user.is_admin? end end - resource :projects do - desc 'Trigger a slash command' do - detail 'Added in GitLab 8.13' + trigger_services.each do |service_slug, settings| + params do + requires :id, type: String, desc: 'The ID of a project' end - post ':id/services/:service_slug/trigger' do - project = find_project(params[:id]) + resource :projects do + desc "Trigger a slash command for #{service_slug}" do + detail 'Added in GitLab 8.13' + end + params do + settings.each do |setting| + requires setting[:name], type: setting[:type], desc: setting[:desc] + end + end + post ":id/services/#{service_slug.underscore}/trigger" do + project = find_project(params[:id]) - # This is not accurate, but done to prevent leakage of the project names - not_found!('Service') unless project + # This is not accurate, but done to prevent leakage of the project names + not_found!('Service') unless project - service = project_service(project) + service = project.find_or_initialize_service(service_slug.underscore) - result = service.try(:active?) && service.try(:trigger, params) + result = service.try(:active?) && service.try(:trigger, params) - if result - status result[:status] || 200 - present result - else - not_found!('Service') + if result + status result[:status] || 200 + present result + else + not_found!('Service') + end end end end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 2da3a9cb09f..564e49d5459 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -358,7 +358,7 @@ describe HipchatService, models: true do context 'with a failed build' do it 'uses the red color' do build_data = { object_kind: 'build', commit: { status: 'failed' } } - + expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'red' }) end end diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index d30361f53d4..668e39f9dba 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -2,6 +2,7 @@ require "spec_helper" describe API::Services, api: true do include ApiHelpers + let(:user) { create(:user) } let(:admin) { create(:admin) } let(:user2) { create(:user) } @@ -98,7 +99,7 @@ describe API::Services, api: true do post api("/projects/#{project.id}/services/idonotexist/trigger") expect(response).to have_http_status(404) - expect(json_response["message"]).to eq("404 Service Not Found") + expect(json_response["error"]).to eq("404 Not Found") end end @@ -114,7 +115,7 @@ describe API::Services, api: true do end it 'when the service is inactive' do - post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger") + post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger"), params expect(response).to have_http_status(404) end diff --git a/spec/support/services_shared_context.rb b/spec/support/services_shared_context.rb index d1c999cad4d..66c93890e31 100644 --- a/spec/support/services_shared_context.rb +++ b/spec/support/services_shared_context.rb @@ -16,8 +16,14 @@ Service.available_services_names.each do |service| hash.merge!(k => 'secrettoken') elsif k =~ /^(.*_url|url|webhook)/ hash.merge!(k => "http://example.com") + elsif service_klass.method_defined?("#{k}?") + hash.merge!(k => true) elsif service == 'irker' && k == :recipients hash.merge!(k => 'irc://irc.network.net:666/#channel') + elsif service == 'irker' && k == :server_port + hash.merge!(k => 1234) + elsif service == 'jira' && k == :jira_issue_transition_id + hash.merge!(k => 1234) else hash.merge!(k => "someword") end -- cgit v1.2.1 From 41dd01b1276d11ceec6d63e2ead52ddaf1852041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Fri, 9 Dec 2016 17:16:04 +0100 Subject: Stop replacing `$your_email` with the user's email MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `$your_email` was removed from the SSH doc. Signed-off-by: Rémy Coutable <remy@rymai.me> --- app/views/help/show.html.haml | 2 +- spec/features/help_pages_spec.rb | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/app/views/help/show.html.haml b/app/views/help/show.html.haml index be257b51b9e..f6ebd76af9d 100644 --- a/app/views/help/show.html.haml +++ b/app/views/help/show.html.haml @@ -1,3 +1,3 @@ - page_title @path.split("/").reverse.map(&:humanize) .documentation.wiki - = markdown @markdown.gsub('$your_email', current_user.try(:email) || "email@example.com") + = markdown @markdown diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb index 4319d6db0d2..40a1fced8d8 100644 --- a/spec/features/help_pages_spec.rb +++ b/spec/features/help_pages_spec.rb @@ -1,16 +1,6 @@ require 'spec_helper' describe 'Help Pages', feature: true do - describe 'Show SSH page' do - before do - login_as :user - end - it 'replaces the variable $your_email with the email of the user' do - visit help_page_path('ssh/README') - expect(page).to have_content("ssh-keygen -t rsa -C \"#{@user.email}\"") - end - end - describe 'Get the main help page' do shared_examples_for 'help page' do |prefix: ''| it 'prefixes links correctly' do -- cgit v1.2.1 From fcf332a4fa544d4a5eba200878a7b9298e5b6f6b Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 9 Dec 2016 11:35:15 +0000 Subject: Group links spec update --- features/steps/project/team_management.rb | 2 +- spec/features/projects/members/group_links_spec.rb | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb index 99b6397ba74..22d971fadfb 100644 --- a/features/steps/project/team_management.rb +++ b/features/steps/project/team_management.rb @@ -148,7 +148,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps step 'I should see "Opensource" group user listing' do page.within '.project-members-groups' do expect(page).to have_content('OpenSource') - expect(find('select').value).to eq('40') + expect(first('.group_member')).to have_content('Master') end end end diff --git a/spec/features/projects/members/group_links_spec.rb b/spec/features/projects/members/group_links_spec.rb index cc2f695211c..94995f7cf95 100644 --- a/spec/features/projects/members/group_links_spec.rb +++ b/spec/features/projects/members/group_links_spec.rb @@ -16,12 +16,17 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t end it 'updates group access level' do - select 'Guest', from: "member_access_level_#{group.id}" + click_button @group_link.human_access + + page.within '.dropdown-menu' do + click_link 'Guest' + end + wait_for_ajax visit namespace_project_project_members_path(project.namespace, project) - expect(page).to have_select("member_access_level_#{group.id}", selected: 'Guest') + expect(first('.group_member')).to have_content('Guest') end it 'updates expiry date' do -- cgit v1.2.1 From 1e8271b60bcd16b6fcc20d574c9583d593084eef Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 9 Dec 2016 17:24:21 +0000 Subject: Adds new partial for graph icons. Fix tests --- app/views/ci/status/_graph_icon_with_name.html.haml | 12 ++++++++++++ .../ci/status/_graph_icon_with_name_and_action.html.haml | 8 ++++++++ app/views/ci/status/_icon_with_name.html.haml | 3 +-- app/views/ci/status/_icon_with_name_and_action.html.haml | 2 +- app/views/projects/stage/_graph.html.haml | 2 +- app/views/projects/stage/_in_stage_group.html.haml | 2 +- spec/features/projects/pipelines/pipeline_spec.rb | 6 +++--- 7 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 app/views/ci/status/_graph_icon_with_name.html.haml create mode 100644 app/views/ci/status/_graph_icon_with_name_and_action.html.haml diff --git a/app/views/ci/status/_graph_icon_with_name.html.haml b/app/views/ci/status/_graph_icon_with_name.html.haml new file mode 100644 index 00000000000..51037a3bd20 --- /dev/null +++ b/app/views/ci/status/_graph_icon_with_name.html.haml @@ -0,0 +1,12 @@ +- detailed_status = subject.detailed_status(current_user) +- details_path = detailed_status.details_path if detailed_status.has_details? +- klass = "ci-status-icon ci-status-icon-#{detailed_status}" +- graph_status_icon = "#{detailed_status.icon}_graph" + +- if details_path + = link_to details_path, class: klass, data: { toggle: 'tooltip', title: "#{subject.name} - #{detailed_status}" } do + %span{ class: klass }= custom_icon(graph_status_icon) + .ci-status-text= subject.name +- else + %span{ class: klass }= custom_icon(graph_status_icon) + .ci-status-text= subject.name diff --git a/app/views/ci/status/_graph_icon_with_name_and_action.html.haml b/app/views/ci/status/_graph_icon_with_name_and_action.html.haml new file mode 100644 index 00000000000..525075ced70 --- /dev/null +++ b/app/views/ci/status/_graph_icon_with_name_and_action.html.haml @@ -0,0 +1,8 @@ += render "ci/status/graph_icon_with_name", subject: subject + +- detailed_status = subject.detailed_status(current_user) +- if detailed_status.has_action? + = link_to detailed_status.action_path, method: detailed_status.action_method, + title: "#{subject.name}: #{detailed_status.action_title}", class: 'ci-action-icon-container' do + %i.ci-action-icon-wrapper + = icon(detailed_status.action_icon, class: detailed_status.action_class) diff --git a/app/views/ci/status/_icon_with_name.html.haml b/app/views/ci/status/_icon_with_name.html.haml index a467316ef47..028e1fe9402 100644 --- a/app/views/ci/status/_icon_with_name.html.haml +++ b/app/views/ci/status/_icon_with_name.html.haml @@ -1,11 +1,10 @@ - detailed_status = subject.detailed_status(current_user) - details_path = detailed_status.details_path if detailed_status.has_details? - klass = "ci-status-icon ci-status-icon-#{detailed_status}" -- status_icon = graph ? "#{detailed_status.icon}_graph" : detailed_status.icon - if details_path = link_to details_path, class: klass, data: { toggle: 'tooltip', title: "#{subject.name} - #{detailed_status}" } do - %span{ class: klass }= custom_icon(status_icon) + %span{ class: klass }= custom_icon(detailed_status.icon) .ci-status-text= subject.name - else %span{ class: klass }= custom_icon(detailed_status.icon) diff --git a/app/views/ci/status/_icon_with_name_and_action.html.haml b/app/views/ci/status/_icon_with_name_and_action.html.haml index b912c212534..76db3b7f38a 100644 --- a/app/views/ci/status/_icon_with_name_and_action.html.haml +++ b/app/views/ci/status/_icon_with_name_and_action.html.haml @@ -1,4 +1,4 @@ -= render "ci/status/icon_with_name", subject: subject, graph: true += render "ci/status/icon_with_name", subject: subject - detailed_status = subject.detailed_status(current_user) - if detailed_status.has_action? diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml index 745b6d143f4..255091cbfe8 100644 --- a/app/views/projects/stage/_graph.html.haml +++ b/app/views/projects/stage/_graph.html.haml @@ -14,7 +14,7 @@ %li.build{ class: ("playable" if is_playable) } .curve .build-content - = render 'ci/status/icon_with_name_and_action', subject: status + = render 'ci/status/graph_icon_with_name_and_action', subject: status - else %li.build .curve diff --git a/app/views/projects/stage/_in_stage_group.html.haml b/app/views/projects/stage/_in_stage_group.html.haml index 5c9b6549b37..70101ccf806 100644 --- a/app/views/projects/stage/_in_stage_group.html.haml +++ b/app/views/projects/stage/_in_stage_group.html.haml @@ -10,4 +10,4 @@ %ul - subject.each do |status| %li.dropdown-build - = render 'ci/status/icon_with_name_and_action', subject: status + = render 'ci/status/graph_icon_with_name_and_action', subject: status diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index e21de05ac64..7358931b9f0 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -40,7 +40,7 @@ describe "Pipelines", feature: true, js: true do context 'pipeline graph' do it 'shows a running icon and a cancel action for the running build' do - page.within('.stage-column:first-child .build:first-child') do + page.within('.stage-column:nth-child(2) .build:first-child') do expect(page).to have_selector('.ci-status-icon-running') expect(page).to have_content('deploy') expect(page).to have_selector('.ci-action-icon-container .fa-ban') @@ -56,7 +56,7 @@ describe "Pipelines", feature: true, js: true do end it 'shows the failed icon and a retry action for the failed build' do - page.within('.stage-column:nth-child(2) .build') do + page.within('.stage-column:first-child .build') do expect(page).to have_selector('.ci-status-icon-failed') expect(page).to have_content('test') expect(page).to have_selector('.ci-action-icon-container .fa-refresh') @@ -64,7 +64,7 @@ describe "Pipelines", feature: true, js: true do end it 'shows the skipped icon and a play action for the manual build' do - page.within('.stage-column:first-child .build:nth-child(2)') do + page.within('.stage-column:nth-child(2) .build:nth-child(2)') do expect(page).to have_selector('.ci-status-icon-skipped') expect(page).to have_content('manual') expect(page).to have_selector('.ci-action-icon-container .ci-play-icon') -- cgit v1.2.1 From e01e9f8b01e530ea5aa43180abbbb6490684eb8c Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Fri, 9 Dec 2016 15:28:41 -0200 Subject: [ci skip] Update "Installation from source" guide for 8.15.0 --- doc/install/installation.md | 4 ++-- doc/update/8.14-to-8.15.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index 9c6a9656557..2740b2982b9 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -271,9 +271,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-14-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-15-stable gitlab -**Note:** You can change `8-14-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `8-15-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It diff --git a/doc/update/8.14-to-8.15.md b/doc/update/8.14-to-8.15.md index 3f58493fa63..5556dae2551 100644 --- a/doc/update/8.14-to-8.15.md +++ b/doc/update/8.14-to-8.15.md @@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-15-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch --all --tags -sudo -u git -H git checkout v4.0.0 +sudo -u git -H git checkout v4.0.3 ``` ### 6. Update gitlab-workhorse -- cgit v1.2.1 From ff2193a3db558214fab90bb644be6967a03176a0 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Fri, 9 Dec 2016 19:40:22 +0200 Subject: Fix specs --- lib/gitlab/bitbucket_import/importer.rb | 6 +----- spec/lib/gitlab/bitbucket_import/importer_spec.rb | 14 +++++++++++++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index e00a90da980..a0a17333185 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -58,7 +58,7 @@ module Gitlab updated_at: issue.updated_at ) - assign_label(issue, label_name) + issue.labels << @labels[label_name] if issue.persisted? client.issue_comments(repo, issue.iid).each do |comment| @@ -92,10 +92,6 @@ module Gitlab end end - def assign_label(issue, label_name) - issue.labels << @labels[label_name] - end - def import_pull_requests pull_requests = client.pull_requests(repo) diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index ef4fc9fd08e..353312675d6 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -18,6 +18,7 @@ describe Gitlab::BitbucketImport::Importer, lib: true do "closed" # undocumented status ] end + let(:sample_issues_statuses) do issues = [] @@ -26,6 +27,7 @@ describe Gitlab::BitbucketImport::Importer, lib: true do id: index, state: status, title: "Issue #{index}", + kind: 'bug', content: { raw: "Some content to issue #{index}", markup: "markdown", @@ -38,6 +40,7 @@ describe Gitlab::BitbucketImport::Importer, lib: true do end let(:project_identifier) { 'namespace/repo' } + let(:data) do { 'bb_session' => { @@ -46,6 +49,7 @@ describe Gitlab::BitbucketImport::Importer, lib: true do } } end + let(:project) do create( :project, @@ -53,7 +57,9 @@ describe Gitlab::BitbucketImport::Importer, lib: true do import_data: ProjectImportData.new(credentials: data) ) end + let(:importer) { Gitlab::BitbucketImport::Importer.new(project) } + let(:issues_statuses_sample_data) do { count: sample_issues_statuses.count, @@ -77,6 +83,12 @@ describe Gitlab::BitbucketImport::Importer, lib: true do headers: { "Content-Type" => "application/json" }, body: issues_statuses_sample_data.to_json) + stub_request(:get, "https://api.bitbucket.org/2.0/repositories/namespace/repo?pagelen=50&sort=created_on"). + with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization'=>'Bearer', 'User-Agent'=>'Faraday v0.9.2'}). + to_return(:status => 200, + :body => "", + :headers => {}) + sample_issues_statuses.each_with_index do |issue, index| stub_request( :get, @@ -97,7 +109,7 @@ describe Gitlab::BitbucketImport::Importer, lib: true do end it 'map statuses to open or closed' do - # HACK: Bitbucket::Representation.const_get('Issue') seems to return Issue without this + # HACK: Bitbucket::Representation.const_get('Issue') seems to return ::Issue without this Bitbucket::Representation::Issue.new({}) importer.execute -- cgit v1.2.1 From ee10a1cc5157e78384bac16f9ee2cf43d66513df Mon Sep 17 00:00:00 2001 From: tauriedavis <taurie@gitlab.com> Date: Wed, 30 Nov 2016 12:37:54 -0800 Subject: 20916 Shorten line length of issues and mrs --- app/assets/javascripts/issue.js | 6 ++++++ app/assets/javascripts/merge_request.js | 6 ++++++ app/assets/stylesheets/framework/layout.scss | 4 ++++ app/assets/stylesheets/framework/variables.scss | 3 +++ app/assets/stylesheets/pages/issuable.scss | 13 +++++++++++++ 5 files changed, 32 insertions(+) diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 8540b199aba..56a2b6a5295 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -11,6 +11,7 @@ function Issue() { this.submitNoteForm = bind(this.submitNoteForm, this); // Prevent duplicate event bindings + this.limitContainerWidth(); this.disableTaskList(); if ($('a.btn-close').length) { this.initTaskList(); @@ -21,6 +22,11 @@ this.initCanCreateBranch(); } + Issue.prototype.limitContainerWidth = function() { + var $wrapper = $('.content-wrapper .container-fluid'); + $wrapper.addClass('limit-container-width') + }; + Issue.prototype.initTaskList = function() { $('.detail-page-description .js-task-list-container').taskList('enable'); return $(document).on('tasklist:changed', '.detail-page-description .js-task-list-container', this.updateTaskList); diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 88c3636be6c..b3a1ec39c13 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -14,6 +14,7 @@ // Options: // action - String, current controller action // + this.limitContainerWidth(); this.opts = opts != null ? opts : {}; this.submitNoteForm = bind(this.submitNoteForm, this); this.$el = $('.merge-request'); @@ -31,6 +32,11 @@ } } + MergeRequest.prototype.limitContainerWidth = function() { + var $wrapper = $('.content-wrapper .container-fluid'); + $wrapper.addClass('limit-container-width') + }; + // Local jQuery finder MergeRequest.prototype.$ = function(selector) { return this.$el.find(selector); diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index dfaf2f7f1d3..66711aa1804 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -26,6 +26,10 @@ body { .container-limited { max-width: $fixed-layout-width; + + &.limit-container-width { + max-width: $limited-layout-width; + } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 18716813c48..55d97b9219c 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -166,6 +166,9 @@ $row-hover-border: #b2d7ff; $progress-color: #c0392b; $header-height: 50px; $fixed-layout-width: 1280px; +$limited-layout-width: 958px; +$line-length-width: 700px; +$gl-avatar-size: 40px; $error-exclamation-point: #e62958; $border-radius-default: 2px; $btn-transparent-color: #8f8f8f; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 90587b9425b..6c35496b846 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -1,3 +1,16 @@ +.container-limited.limit-container-width { + .issue-details { + .description, + .note-body { + p, + ul, + ol, + .code { + max-width: $line-length-width; + } + } + } +} .issuable-details { section { .issuable-discussion { -- cgit v1.2.1 From 258ab29435d4a02ab5ded6c19de597fdf847ef80 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Fri, 2 Dec 2016 10:13:04 -0600 Subject: Add content_class for limited width --- app/assets/javascripts/issue.js | 6 ------ app/assets/javascripts/merge_request.js | 6 ------ app/assets/stylesheets/pages/issuable.scss | 1 + app/views/projects/issues/show.html.haml | 1 + app/views/projects/merge_requests/_show.html.haml | 1 + 5 files changed, 3 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 56a2b6a5295..8540b199aba 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -11,7 +11,6 @@ function Issue() { this.submitNoteForm = bind(this.submitNoteForm, this); // Prevent duplicate event bindings - this.limitContainerWidth(); this.disableTaskList(); if ($('a.btn-close').length) { this.initTaskList(); @@ -22,11 +21,6 @@ this.initCanCreateBranch(); } - Issue.prototype.limitContainerWidth = function() { - var $wrapper = $('.content-wrapper .container-fluid'); - $wrapper.addClass('limit-container-width') - }; - Issue.prototype.initTaskList = function() { $('.detail-page-description .js-task-list-container').taskList('enable'); return $(document).on('tasklist:changed', '.detail-page-description .js-task-list-container', this.updateTaskList); diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index b3a1ec39c13..88c3636be6c 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -14,7 +14,6 @@ // Options: // action - String, current controller action // - this.limitContainerWidth(); this.opts = opts != null ? opts : {}; this.submitNoteForm = bind(this.submitNoteForm, this); this.$el = $('.merge-request'); @@ -32,11 +31,6 @@ } } - MergeRequest.prototype.limitContainerWidth = function() { - var $wrapper = $('.content-wrapper .container-fluid'); - $wrapper.addClass('limit-container-width') - }; - // Local jQuery finder MergeRequest.prototype.$ = function(selector) { return this.$el.find(selector); diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 6c35496b846..2357dd2fe6f 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -11,6 +11,7 @@ } } } + .issuable-details { section { .issuable-discussion { diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index bd629b5c519..981bf640a6b 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,3 +1,4 @@ +- @content_class = "limit-container-width" - page_title "#{@issue.title} (#{@issue.to_reference})", "Issues" - page_description @issue.description - page_card_attributes @issue.card_attributes diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 896f10104fa..0db5548d36e 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -1,3 +1,4 @@ +- @content_class = "limit-container-width" - page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes -- cgit v1.2.1 From 091970208e0c8e7aefb6e7dcfafb4c81188c27cf Mon Sep 17 00:00:00 2001 From: Stan Hu <stanhu@gmail.com> Date: Fri, 9 Dec 2016 15:17:13 -0800 Subject: Return repositories to which user is a member, not just owner --- lib/bitbucket/client.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bitbucket/client.rb b/lib/bitbucket/client.rb index e23da4556aa..a9f405e659b 100644 --- a/lib/bitbucket/client.rb +++ b/lib/bitbucket/client.rb @@ -35,7 +35,7 @@ module Bitbucket end def repos - path = "/repositories/#{user.username}" + path = "/repositories/#{user.username}?role=member" get_collection(path, :repo) end -- cgit v1.2.1 From 1d7f85aeef624a83f0b225217a23c8f5189cde54 Mon Sep 17 00:00:00 2001 From: Stan Hu <stanhu@gmail.com> Date: Fri, 9 Dec 2016 15:28:49 -0800 Subject: Fix query for importing all projects for member --- lib/bitbucket/client.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bitbucket/client.rb b/lib/bitbucket/client.rb index a9f405e659b..5c2ef2a4509 100644 --- a/lib/bitbucket/client.rb +++ b/lib/bitbucket/client.rb @@ -35,7 +35,7 @@ module Bitbucket end def repos - path = "/repositories/#{user.username}?role=member" + path = "/repositories?role=member" get_collection(path, :repo) end -- cgit v1.2.1 From 793a717bad5491c523d4551ef72287f8be9c1062 Mon Sep 17 00:00:00 2001 From: Ryan Harris <harrisryan1@gmail.com> Date: Fri, 9 Dec 2016 23:59:20 -0500 Subject: Change cursor type only for inactive stage-nav-items on Cycle Analytics page --- app/assets/stylesheets/pages/cycle_analytics.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index ce708106490..21a5bd047f7 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -215,7 +215,7 @@ border-bottom: 1px solid transparent; border-right: 1px solid $border-color; background-color: $gray-light; - cursor: pointer; + cursor: default; &.active { background-color: transparent; @@ -232,6 +232,7 @@ &:hover:not(.active) { background-color: $gray-lightest; box-shadow: inset 2px 0 0 0 $border-color; + cursor: pointer; } &:first-child { -- cgit v1.2.1 From 69ffa81424e16a337b4c39d2cb9c0dff8b92f71b Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Fri, 9 Dec 2016 23:30:54 -0600 Subject: fix alignment for notification settings ajax response --- app/controllers/notification_settings_controller.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/controllers/notification_settings_controller.rb b/app/controllers/notification_settings_controller.rb index 8ec4bb1233f..160259626d6 100644 --- a/app/controllers/notification_settings_controller.rb +++ b/app/controllers/notification_settings_controller.rb @@ -37,7 +37,11 @@ class NotificationSettingsController < ApplicationController def render_response render json: { - html: view_to_html_string("shared/notifications/_button", notification_setting: @notification_setting), + html: view_to_html_string( + "shared/notifications/_button", + notification_setting: @notification_setting, + left_align: @notification_setting.source.nil? + ), saved: @saved } end -- cgit v1.2.1 From 097c283ac186bccb647143fd022bfe6a320379b0 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Sat, 10 Dec 2016 00:21:47 -0600 Subject: remove left_align setting from notification setting dropdown in favor of a pure css solution --- app/assets/stylesheets/pages/notifications.scss | 4 ++++ app/assets/stylesheets/pages/projects.scss | 4 ++++ app/controllers/notification_settings_controller.rb | 6 +----- app/views/profiles/notifications/show.html.haml | 2 +- app/views/shared/notifications/_button.html.haml | 3 +-- app/views/shared/notifications/_notification_dropdown.html.haml | 3 +-- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss index 94fbbef3c77..7d61390a439 100644 --- a/app/assets/stylesheets/pages/notifications.scss +++ b/app/assets/stylesheets/pages/notifications.scss @@ -1,5 +1,9 @@ .notification-list-item { line-height: 34px; + + .dropdown-menu { + @extend .dropdown-menu-align-right; + } } .notification { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 72b6685d940..6e0f6b1cd81 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -188,6 +188,10 @@ margin-left: 10px; } + .notification-dropdown .dropdown-menu { + @extend .dropdown-menu-align-right; + } + .download-button { @media (max-width: $screen-md-max) { margin-left: 0; diff --git a/app/controllers/notification_settings_controller.rb b/app/controllers/notification_settings_controller.rb index 160259626d6..8ec4bb1233f 100644 --- a/app/controllers/notification_settings_controller.rb +++ b/app/controllers/notification_settings_controller.rb @@ -37,11 +37,7 @@ class NotificationSettingsController < ApplicationController def render_response render json: { - html: view_to_html_string( - "shared/notifications/_button", - notification_setting: @notification_setting, - left_align: @notification_setting.source.nil? - ), + html: view_to_html_string("shared/notifications/_button", notification_setting: @notification_setting), saved: @saved } end diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 844fce59704..d79a1a9f368 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -30,7 +30,7 @@ %br .clearfix .form-group.pull-left.global-notification-setting - = render 'shared/notifications/button', notification_setting: @global_notification_setting, left_align: true + = render 'shared/notifications/button', notification_setting: @global_notification_setting .clearfix diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml index 1f7df0bcd19..fbad0d05de3 100644 --- a/app/views/shared/notifications/_button.html.haml +++ b/app/views/shared/notifications/_button.html.haml @@ -1,4 +1,3 @@ -- left_align = local_assigns[:left_align] - if notification_setting .dropdown.notification-dropdown = form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f| @@ -19,7 +18,7 @@ = notification_title(notification_setting.level) = icon("caret-down") - = render "shared/notifications/notification_dropdown", notification_setting: notification_setting, left_align: left_align + = render "shared/notifications/notification_dropdown", notification_setting: notification_setting = content_for :scripts_body do = render "shared/notifications/custom_notifications", notification_setting: notification_setting diff --git a/app/views/shared/notifications/_notification_dropdown.html.haml b/app/views/shared/notifications/_notification_dropdown.html.haml index d3258ee64cb..85ad74f9a39 100644 --- a/app/views/shared/notifications/_notification_dropdown.html.haml +++ b/app/views/shared/notifications/_notification_dropdown.html.haml @@ -1,5 +1,4 @@ -- left_align = local_assigns[:left_align] -%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting), ("dropdown-menu-align-right" unless left_align)] } +%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting)] } - NotificationSetting.levels.each_key do |level| - next if level == "custom" - next if level == "global" && notification_setting.source.nil? -- cgit v1.2.1 From 5934698fc3b7aaf6623959a9740e62ed3d341a42 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Mon, 3 Oct 2016 16:14:17 -0500 Subject: fix broken string interpolation --- app/views/projects/commit/_change.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index f6e3d5e76f5..59af45ee1ed 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -13,7 +13,7 @@ %a.close{href: "#", "data-dismiss" => "modal"} × %h3.page-title== #{label} this #{commit.change_type_title(current_user)} .modal-body - = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-#{type}-form js-requires-input' do + = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do .form-group.branch = label_tag 'target_branch', target_label, class: 'control-label' .col-sm-10 -- cgit v1.2.1 From 7876e83d0c91e75b91e21ddb9993fc39fde96731 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Mon, 3 Oct 2016 17:04:23 -0500 Subject: remove unnecessary nonce id --- app/views/projects/commit/_change.html.haml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index 59af45ee1ed..e38ee6a2afa 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -23,9 +23,8 @@ - if can?(current_user, :push_code, @project) .js-create-merge-request-container .checkbox - - nonce = SecureRandom.hex - = label_tag "create_merge_request-#{nonce}" do - = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}" + = label_tag do + = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request' Start a <strong>new merge request</strong> with these changes - else = hidden_field_tag 'create_merge_request', 1 -- cgit v1.2.1 From 79aad815272fdebca30080cb33dd3fa77ef72350 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 5 Oct 2016 10:12:46 -0500 Subject: fix awkward verb conjugation in cherry-pick and revert errors --- app/services/commits/change_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index db5f2bf9b2e..4d410f66c55 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -35,7 +35,7 @@ module Commits success else error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically. - It may have already been #{action.to_s.dasherize}, or a more recent commit may have updated some of its content." + A #{action.to_s.dasherize} may have already been performed with this #{@commit.change_type_title(current_user)}, or a more recent commit may have updated some of its content." raise ChangeError, error_msg end end -- cgit v1.2.1 From 130cbc979aff4ce746cabc7d319a34904b1fd611 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Sat, 10 Dec 2016 00:44:04 -0600 Subject: prevent create_merge_request form field helpers from generating an id value --- app/views/projects/commit/_change.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index e38ee6a2afa..782f558e8b0 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -24,10 +24,10 @@ .js-create-merge-request-container .checkbox = label_tag do - = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request' + = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: nil Start a <strong>new merge request</strong> with these changes - else - = hidden_field_tag 'create_merge_request', 1 + = hidden_field_tag 'create_merge_request', 1, id: nil .form-actions = submit_tag label, class: 'btn btn-create' = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" -- cgit v1.2.1 From 758b3055c5b25e2b5dcdb12c169d23303d3bfe0d Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 7 Dec 2016 14:58:27 -0600 Subject: update action button order for snippets page --- app/views/projects/snippets/_actions.html.haml | 12 ++++++------ app/views/snippets/_actions.html.haml | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index 32e1f8a21b0..346badb8f25 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -1,13 +1,13 @@ .hidden-xs - - if can?(current_user, :create_project_snippet, @project) - = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New snippet" do - New snippet - - if can?(current_user, :update_project_snippet, @snippet) - = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do - 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 Edit + - if can?(current_user, :update_project_snippet, @snippet) + = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do + Delete + - if can?(current_user, :create_project_snippet, @project) + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New snippet" do + New snippet - if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet) .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index 1d0e549ed3d..2ed5894d312 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -1,13 +1,13 @@ .hidden-xs - - if current_user - = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New snippet" do - 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-danger", title: 'Delete Snippet' do - Delete - if can?(current_user, :update_personal_snippet, @snippet) = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do Edit + - if can?(current_user, :admin_personal_snippet, @snippet) + = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do + Delete + - if current_user + = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New snippet" do + New snippet - if current_user .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } -- cgit v1.2.1 From 12a8095cc7a30469c4546af1ed801c785b0ebace Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 7 Dec 2016 15:08:19 -0600 Subject: remove unused class name --- app/views/projects/snippets/_actions.html.haml | 4 ++-- app/views/snippets/_actions.html.haml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index 346badb8f25..d2a97476957 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -1,12 +1,12 @@ .hidden-xs - 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 + = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped" do Edit - if can?(current_user, :update_project_snippet, @snippet) = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do Delete - if can?(current_user, :create_project_snippet, @project) - = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New snippet" do + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create', title: "New snippet" do New snippet - if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet) .visible-xs-block.dropdown diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index 2ed5894d312..ebb3dfe5a34 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -1,12 +1,12 @@ .hidden-xs - if can?(current_user, :update_personal_snippet, @snippet) - = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do + = link_to edit_snippet_path(@snippet), class: "btn btn-grouped" do Edit - if can?(current_user, :admin_personal_snippet, @snippet) = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do Delete - if current_user - = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New snippet" do + = link_to new_snippet_path, class: "btn btn-grouped btn-create", title: "New snippet" do New snippet - if current_user .visible-xs-block.dropdown -- cgit v1.2.1 From c41b7e8a2f4df9a3a320cbf1b5b60a43709ca9b6 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 7 Dec 2016 15:22:16 -0600 Subject: invert snippet action buttons --- app/views/projects/snippets/_actions.html.haml | 4 ++-- app/views/snippets/_actions.html.haml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index d2a97476957..068a6610350 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -3,10 +3,10 @@ = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped" do Edit - if can?(current_user, :update_project_snippet, @snippet) - = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do + = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do Delete - if can?(current_user, :create_project_snippet, @project) - = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create', title: "New snippet" do + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do New snippet - if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet) .visible-xs-block.dropdown diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index ebb3dfe5a34..95fc7198104 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -3,10 +3,10 @@ = link_to edit_snippet_path(@snippet), class: "btn btn-grouped" do Edit - if can?(current_user, :admin_personal_snippet, @snippet) - = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do + = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do Delete - if current_user - = link_to new_snippet_path, class: "btn btn-grouped btn-create", title: "New snippet" do + = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do New snippet - if current_user .visible-xs-block.dropdown -- cgit v1.2.1 From 6b20ad3646694ae90e8375f92cb5df13e2fd9fad Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 7 Dec 2016 15:41:39 -0600 Subject: remove plus icon in "new snippet" button --- app/views/dashboard/snippets/index.html.haml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index b2af438ea57..62618625454 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -6,7 +6,6 @@ .nav-block .controls.hidden-xs = link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do - = icon('plus') New snippet .nav-links.snippet-scope-menu @@ -36,7 +35,6 @@ .visible-xs = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do - = icon('plus') New snippet = render 'snippets/snippets' -- cgit v1.2.1 From b65d3e1132762b9e1a39ce908b3b481f92d9a10c Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 7 Dec 2016 16:03:00 -0600 Subject: move new snippet button to main snippet navigation block --- app/views/dashboard/_snippets_head.html.haml | 20 +++++---- app/views/dashboard/snippets/index.html.haml | 62 +++++++++++++--------------- app/views/explore/snippets/index.html.haml | 8 ---- 3 files changed, 42 insertions(+), 48 deletions(-) diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml index b25e8ea1f0c..02e90bbfa55 100644 --- a/app/views/dashboard/_snippets_head.html.haml +++ b/app/views/dashboard/_snippets_head.html.haml @@ -1,7 +1,13 @@ -%ul.nav-links - = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do - = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do - Your Snippets - = nav_link(page: explore_snippets_path) do - = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do - Explore Snippets +.top-area + %ul.nav-links + = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do + = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do + Your Snippets + = nav_link(page: explore_snippets_path) do + = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do + Explore Snippets + + - if current_user + .nav-controls.hidden-xs + = link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do + New snippet diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index 62618625454..13cba7ca224 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -3,38 +3,34 @@ = render 'dashboard/snippets_head' -.nav-block - .controls.hidden-xs - = link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do - New snippet - - .nav-links.snippet-scope-menu - %li{ class: ("active" unless params[:scope]) } - = link_to dashboard_snippets_path do - All - %span.badge - = current_user.snippets.count - - %li{ class: ("active" if params[:scope] == "are_private") } - = link_to dashboard_snippets_path(scope: 'are_private') do - Private - %span.badge - = current_user.snippets.are_private.count - - %li{ class: ("active" if params[:scope] == "are_internal") } - = link_to dashboard_snippets_path(scope: 'are_internal') do - Internal - %span.badge - = current_user.snippets.are_internal.count - - %li{ class: ("active" if params[:scope] == "are_public") } - = link_to dashboard_snippets_path(scope: 'are_public') do - Public - %span.badge - = current_user.snippets.are_public.count - - .visible-xs - = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do - New snippet +.nav-links.snippet-scope-menu + %li{ class: ("active" unless params[:scope]) } + = link_to dashboard_snippets_path do + All + %span.badge + = current_user.snippets.count + + %li{ class: ("active" if params[:scope] == "are_private") } + = link_to dashboard_snippets_path(scope: 'are_private') do + Private + %span.badge + = current_user.snippets.are_private.count + + %li{ class: ("active" if params[:scope] == "are_internal") } + = link_to dashboard_snippets_path(scope: 'are_internal') do + Internal + %span.badge + = current_user.snippets.are_internal.count + + %li{ class: ("active" if params[:scope] == "are_public") } + = link_to dashboard_snippets_path(scope: 'are_public') do + Public + %span.badge + = current_user.snippets.are_public.count + +.visible-xs +   + = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do + New snippet = render 'snippets/snippets' diff --git a/app/views/explore/snippets/index.html.haml b/app/views/explore/snippets/index.html.haml index 7def9eacdc9..9b5ea13ca29 100644 --- a/app/views/explore/snippets/index.html.haml +++ b/app/views/explore/snippets/index.html.haml @@ -6,12 +6,4 @@ - else = render 'explore/head' -.row-content-block - - if current_user - = link_to new_snippet_path, class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do - New snippet - - .oneline - Public snippets created by you and other users are listed here - = render 'snippets/snippets' -- cgit v1.2.1 From 54a1193d790ae40fea5db1d8596c12fbc7a93576 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 7 Dec 2016 16:35:53 -0600 Subject: add scope filters to project snippets page --- app/controllers/projects/snippets_controller.rb | 8 +++++--- app/finders/snippets_finder.rb | 24 ++++++++++++++++++---- app/views/projects/snippets/index.html.haml | 27 +++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index e290a0eadda..0720be2e55d 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -19,10 +19,12 @@ class Projects::SnippetsController < Projects::ApplicationController respond_to :html def index - @snippets = SnippetsFinder.new.execute(current_user, { + @snippets = SnippetsFinder.new.execute( + current_user, filter: :by_project, - project: @project - }) + project: @project, + scope: params[:scope] + ) @snippets = @snippets.page(params[:page]) end diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index 00ff1611039..99f1e73c800 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -8,7 +8,7 @@ class SnippetsFinder when :by_user then by_user(current_user, params[:user], params[:scope]) when :by_project - by_project(current_user, params[:project]) + by_project(current_user, params[:project], params[:scope]) end end @@ -47,14 +47,30 @@ class SnippetsFinder end end - def by_project(current_user, project) + def by_project(current_user, project, scope) snippets = project.snippets.fresh if current_user if project.team.member?(current_user) || current_user.admin? - snippets + case scope + when 'are_internal' then + snippets.are_internal + when 'are_private' then + snippets.are_private + when 'are_public' then + snippets.are_public + else + snippets + end else - snippets.public_and_internal + case scope + when 'are_internal' then + snippets.are_internal + when 'are_public' then + snippets.are_public + else + snippets.public_and_internal + end end else snippets.are_public diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index e77e1b026f6..76792fb5326 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -1,5 +1,32 @@ - page_title "Snippets" +- if current_user + .nav-links.snippet-scope-menu + %li{ class: ("active" unless params[:scope]) } + = link_to namespace_project_snippets_path(@project.namespace, @project) do + All + %span.badge + = @project.snippets.count + + - if @project.team.member?(current_user) || current_user.admin? + %li{ class: ("active" if params[:scope] == "are_private") } + = link_to namespace_project_snippets_path(@project.namespace, @project, scope: 'are_private') do + Private + %span.badge + = @project.snippets.are_private.count + + %li{ class: ("active" if params[:scope] == "are_internal") } + = link_to namespace_project_snippets_path(@project.namespace, @project, scope: 'are_internal') do + Internal + %span.badge + = @project.snippets.are_internal.count + + %li{ class: ("active" if params[:scope] == "are_public") } + = link_to namespace_project_snippets_path(@project.namespace, @project, scope: 'are_public') do + Public + %span.badge + = @project.snippets.are_public.count + .sub-header-block - if can?(current_user, :create_project_snippet, @project) = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do -- cgit v1.2.1 From 68bb459b160419004ef2110b2824c8b2ab4c9739 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 7 Dec 2016 16:48:26 -0600 Subject: move project new snippet button into snippet scope navigation header --- app/views/projects/snippets/index.html.haml | 58 +++++++++++++++-------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index 76792fb5326..35c4e9d85ad 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -1,38 +1,42 @@ - page_title "Snippets" - if current_user - .nav-links.snippet-scope-menu - %li{ class: ("active" unless params[:scope]) } - = link_to namespace_project_snippets_path(@project.namespace, @project) do - All - %span.badge - = @project.snippets.count + .top-area + .nav-links.snippet-scope-menu + %li{ class: ("active" unless params[:scope]) } + = link_to namespace_project_snippets_path(@project.namespace, @project) do + All + %span.badge + = @project.snippets.count + + - if @project.team.member?(current_user) || current_user.admin? + %li{ class: ("active" if params[:scope] == "are_private") } + = link_to namespace_project_snippets_path(@project.namespace, @project, scope: 'are_private') do + Private + %span.badge + = @project.snippets.are_private.count - - if @project.team.member?(current_user) || current_user.admin? - %li{ class: ("active" if params[:scope] == "are_private") } - = link_to namespace_project_snippets_path(@project.namespace, @project, scope: 'are_private') do - Private + %li{ class: ("active" if params[:scope] == "are_internal") } + = link_to namespace_project_snippets_path(@project.namespace, @project, scope: 'are_internal') do + Internal %span.badge - = @project.snippets.are_private.count + = @project.snippets.are_internal.count - %li{ class: ("active" if params[:scope] == "are_internal") } - = link_to namespace_project_snippets_path(@project.namespace, @project, scope: 'are_internal') do - Internal - %span.badge - = @project.snippets.are_internal.count + %li{ class: ("active" if params[:scope] == "are_public") } + = link_to namespace_project_snippets_path(@project.namespace, @project, scope: 'are_public') do + Public + %span.badge + = @project.snippets.are_public.count - %li{ class: ("active" if params[:scope] == "are_public") } - = link_to namespace_project_snippets_path(@project.namespace, @project, scope: 'are_public') do - Public - %span.badge - = @project.snippets.are_public.count + .nav-controls.hidden-xs + - if can?(current_user, :create_project_snippet, @project) + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New snippet" do + New snippet -.sub-header-block - - if can?(current_user, :create_project_snippet, @project) - = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do +- if can?(current_user, :create_project_snippet, @project) + .visible-xs +   + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-block", title: "New snippet" do New snippet - .oneline - Share code pastes with others out of git repository - = render 'snippets/snippets' -- cgit v1.2.1 From 1ea478476404d6696d65269fd2ffe6ca29740035 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 7 Dec 2016 16:49:06 -0600 Subject: ensure all snippets count badge is accurate for non team members --- app/views/projects/snippets/index.html.haml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index 35c4e9d85ad..978f4b87564 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -7,7 +7,10 @@ = link_to namespace_project_snippets_path(@project.namespace, @project) do All %span.badge - = @project.snippets.count + - if @project.team.member?(current_user) || current_user.admin? + = @project.snippets.count + - else + = @project.snippets.public_and_internal.count - if @project.team.member?(current_user) || current_user.admin? %li{ class: ("active" if params[:scope] == "are_private") } -- cgit v1.2.1 From 68c1e3a1568d9f61bf1e01d6ff55ce59c5c3eaaf Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 8 Dec 2016 14:29:15 -0600 Subject: update snippets list design --- app/assets/stylesheets/pages/snippets.scss | 10 ++++++++++ app/views/shared/snippets/_snippet.html.haml | 23 ++++++++++------------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss index 857eb76131a..e6e86556695 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/pages/snippets.scss @@ -1,3 +1,13 @@ +.snippet-row { + .title { + margin-bottom: 2px; + } + + .snippet-filename { + padding: 0 2px; + } +} + .snippet-form-holder .file-holder .file-title { padding: 2px; } diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml index ea17bec8677..95985ad6afb 100644 --- a/app/views/shared/snippets/_snippet.html.haml +++ b/app/views/shared/snippets/_snippet.html.haml @@ -4,14 +4,11 @@ .title = link_to reliable_snippet_path(snippet) do = snippet.title - - if snippet.private? - %span.label.label-gray.hidden-xs - = icon('lock') - private - %span.monospace.pull-right.hidden-xs - = snippet.file_name + - if snippet.file_name + %span.snippet-filename.monospace.hidden-xs + = snippet.file_name - %ul.controls.visible-xs + %ul.controls %li - note_count = snippet.notes.user.count = link_to reliable_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do @@ -22,11 +19,11 @@ = visibility_level_label(snippet.visibility_level) = visibility_level_icon(snippet.visibility_level, fw: false) - %small.pull-right.cgray.hidden-xs - - if snippet.project_id? - = link_to snippet.project.name_with_namespace, namespace_project_path(snippet.project.namespace, snippet.project) - - .snippet-info.hidden-xs + .snippet-info + #{snippet.to_reference} · + authored #{time_ago_with_tooltip(snippet.created_at, placement: 'bottom')} by = link_to user_snippets_path(snippet.author) do = snippet.author_name - authored #{time_ago_with_tooltip(snippet.created_at)} + + .pull-right.snippet-updated-at + %span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom', html_class: 'snippet_update_ago')} -- cgit v1.2.1 From 7f3fc26ec98193fa0c3bfeb7b78c81176bb9c689 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 8 Dec 2016 16:13:23 -0600 Subject: fix failing tests --- app/views/shared/snippets/_snippet.html.haml | 4 ++-- features/steps/project/snippets.rb | 2 +- spec/features/dashboard/datetime_on_tooltips_spec.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml index 95985ad6afb..65946910529 100644 --- a/app/views/shared/snippets/_snippet.html.haml +++ b/app/views/shared/snippets/_snippet.html.haml @@ -21,9 +21,9 @@ .snippet-info #{snippet.to_reference} · - authored #{time_ago_with_tooltip(snippet.created_at, placement: 'bottom')} by + authored #{time_ago_with_tooltip(snippet.created_at, placement: 'bottom', html_class: 'snippet-created-ago')} by = link_to user_snippets_path(snippet.author) do = snippet.author_name .pull-right.snippet-updated-at - %span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom', html_class: 'snippet_update_ago')} + %span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom')} diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb index 5e7d539add6..a3bebfa4b71 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 "New snippet" + first(:link, "New snippet").click end step 'I click link "Snippet one"' do diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb index 365cb445df1..44dfc2dff45 100644 --- a/spec/features/dashboard/datetime_on_tooltips_spec.rb +++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb @@ -36,7 +36,7 @@ feature 'Tooltips on .timeago dates', feature: true, js: true do visit user_snippets_path(user) wait_for_ajax() - page.find('.js-timeago').hover + page.find('.js-timeago.snippet-created-ago').hover end it 'has the datetime formated correctly' do -- cgit v1.2.1 From 0608ecbc69c991efcf56f7872cf1b06416d4bd10 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Fri, 9 Dec 2016 10:54:13 -0600 Subject: conditionally display assoc project info in snippets index --- app/views/dashboard/snippets/index.html.haml | 2 +- app/views/explore/snippets/index.html.haml | 2 +- app/views/shared/snippets/_snippet.html.haml | 10 +++++++++- app/views/snippets/_snippets.html.haml | 3 ++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index 13cba7ca224..81bfa44a665 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -33,4 +33,4 @@ = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do New snippet -= render 'snippets/snippets' += render partial: 'snippets/snippets', locals: { link_project: true } diff --git a/app/views/explore/snippets/index.html.haml b/app/views/explore/snippets/index.html.haml index 9b5ea13ca29..e5706d04736 100644 --- a/app/views/explore/snippets/index.html.haml +++ b/app/views/explore/snippets/index.html.haml @@ -6,4 +6,4 @@ - else = render 'explore/head' -= render 'snippets/snippets' += render partial: 'snippets/snippets', locals: { link_project: true } diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml index 65946910529..5d2d2317f22 100644 --- a/app/views/shared/snippets/_snippet.html.haml +++ b/app/views/shared/snippets/_snippet.html.haml @@ -1,3 +1,5 @@ +- link_project = local_assigns.fetch(:link_project, false) + %li.snippet-row = image_tag avatar_icon(snippet.author_email), class: "avatar s40 hidden-xs", alt: '' @@ -21,9 +23,15 @@ .snippet-info #{snippet.to_reference} · - authored #{time_ago_with_tooltip(snippet.created_at, placement: 'bottom', html_class: 'snippet-created-ago')} by + authored #{time_ago_with_tooltip(snippet.created_at, placement: 'bottom', html_class: 'snippet-created-ago')} + by = link_to user_snippets_path(snippet.author) do = snippet.author_name + - if link_project && snippet.project_id? + %span.hidden-xs + in + = link_to namespace_project_path(snippet.project.namespace, snippet.project) do + = snippet.project.name_with_namespace .pull-right.snippet-updated-at %span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom')} diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml index 77b66ca74b6..ac3701233ad 100644 --- a/app/views/snippets/_snippets.html.haml +++ b/app/views/snippets/_snippets.html.haml @@ -1,8 +1,9 @@ - remote = local_assigns.fetch(:remote, false) +- link_project = local_assigns.fetch(:link_project, false) .snippets-list-holder %ul.content-list - = render partial: 'shared/snippets/snippet', collection: @snippets + = render partial: 'shared/snippets/snippet', collection: @snippets, locals: { link_project: link_project } - if @snippets.empty? %li .nothing-here-block Nothing here. -- cgit v1.2.1 From 6dc4007ad6c775a8d82bb1e3e2e15ec6e8143b14 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Fri, 9 Dec 2016 11:45:40 -0600 Subject: fix snippets reference id in search results (should be $ not #) --- app/views/search/results/_snippet_title.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml index c414acb6a11..027d42396b4 100644 --- a/app/views/search/results/_snippet_title.html.haml +++ b/app/views/search/results/_snippet_title.html.haml @@ -14,7 +14,7 @@ = link_to snippet_title.project.name_with_namespace, namespace_project_path(snippet_title.project.namespace, snippet_title.project) .snippet-info - = "##{snippet_title.id}" + = snippet_title.to_reference %span by = link_to user_snippets_path(snippet_title.author) do -- cgit v1.2.1 From eaf92daa2ce2601426c991c794aab57c4d0da420 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Fri, 9 Dec 2016 11:48:53 -0600 Subject: move snippet edited timeago under the snippet title --- app/assets/stylesheets/pages/snippets.scss | 10 ++++++++-- app/views/shared/snippets/_header.html.haml | 12 ++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss index e6e86556695..ff13b86acf0 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/pages/snippets.scss @@ -34,11 +34,17 @@ padding-bottom: $gl-padding; } +.snippet-header { + padding: $gl-padding 0; +} + .snippet-title { font-size: 24px; font-weight: 600; - padding: $gl-padding; - padding-left: 0; +} + +.snippet-edited-ago { + color: $gray-darkest; } .snippet-actions { diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index d7506e07ff6..d084f5e9684 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -8,10 +8,6 @@ %span.creator authored = 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') by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")} .snippet-actions @@ -20,5 +16,9 @@ - else = render "snippets/actions" -%h2.snippet-title.prepend-top-0.append-bottom-0 - = markdown_field(@snippet, :title) +.snippet-header + %h2.snippet-title.prepend-top-0.append-bottom-0 + = markdown_field(@snippet, :title) + + - if @snippet.updated_at != @snippet.created_at + = edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago') -- cgit v1.2.1 From adbc37804e49e1d3ba02bf61122696e135666ff3 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Fri, 9 Dec 2016 14:40:48 -0600 Subject: refactor duplicate code into a by_scope method --- app/finders/snippets_finder.rb | 54 +++++++++++++++--------------------------- 1 file changed, 19 insertions(+), 35 deletions(-) diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index 99f1e73c800..31f039b5a70 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -29,21 +29,11 @@ class SnippetsFinder def by_user(current_user, user, scope) snippets = user.snippets.fresh - return snippets.are_public unless current_user - - if user == current_user - case scope - when 'are_internal' then - snippets.are_internal - when 'are_private' then - snippets.are_private - when 'are_public' then - snippets.are_public - else - snippets - end + if current_user + include_private = user == current_user + by_scope(snippets, scope, include_private) else - snippets.public_and_internal + snippets.are_public end end @@ -51,29 +41,23 @@ class SnippetsFinder snippets = project.snippets.fresh if current_user - if project.team.member?(current_user) || current_user.admin? - case scope - when 'are_internal' then - snippets.are_internal - when 'are_private' then - snippets.are_private - when 'are_public' then - snippets.are_public - else - snippets - end - else - case scope - when 'are_internal' then - snippets.are_internal - when 'are_public' then - snippets.are_public - else - snippets.public_and_internal - end - end + include_private = project.team.member?(current_user) || current_user.admin? + by_scope(snippets, scope, include_private) else snippets.are_public end end + + def by_scope(snippets, scope = nil, include_private = false) + case scope.to_s + when 'are_private' + include_private ? snippets.are_private : nil + when 'are_internal' + snippets.are_internal + when 'are_public' + snippets.are_public + else + include_private ? snippets : snippets.public_and_internal + end + end end -- cgit v1.2.1 From 687872978100c168ce381448c0a9536fb53542ce Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Fri, 9 Dec 2016 15:38:10 -0600 Subject: implement snippets_scope_menu partial to reduce code duplication --- app/helpers/snippets_helper.rb | 11 ++++++++ app/views/dashboard/snippets/index.html.haml | 26 +------------------ app/views/projects/snippets/index.html.haml | 30 ++-------------------- app/views/snippets/_snippets_scope_menu.html.haml | 31 +++++++++++++++++++++++ 4 files changed, 45 insertions(+), 53 deletions(-) create mode 100644 app/views/snippets/_snippets_scope_menu.html.haml diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 7e33a562077..fc7febd3385 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -8,6 +8,17 @@ module SnippetsHelper end end + # Return the path of a snippets index for a user or for a project + # + # @returns String, path to snippet index + def snippets_path(subject = nil, opts = nil) + if subject.is_a?(Project) + namespace_project_snippets_path(subject.namespace, subject, opts) + else # assume subject === User + dashboard_snippets_path(opts) + end + end + # Get an array of line numbers surrounding a matching # line, bounded by min/max. # diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index 81bfa44a665..85cbe0bf0e6 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -2,31 +2,7 @@ - header_title "Snippets", dashboard_snippets_path = render 'dashboard/snippets_head' - -.nav-links.snippet-scope-menu - %li{ class: ("active" unless params[:scope]) } - = link_to dashboard_snippets_path do - All - %span.badge - = current_user.snippets.count - - %li{ class: ("active" if params[:scope] == "are_private") } - = link_to dashboard_snippets_path(scope: 'are_private') do - Private - %span.badge - = current_user.snippets.are_private.count - - %li{ class: ("active" if params[:scope] == "are_internal") } - = link_to dashboard_snippets_path(scope: 'are_internal') do - Internal - %span.badge - = current_user.snippets.are_internal.count - - %li{ class: ("active" if params[:scope] == "are_public") } - = link_to dashboard_snippets_path(scope: 'are_public') do - Public - %span.badge - = current_user.snippets.are_public.count += render partial: 'snippets/snippets_scope_menu', locals: { include_private: true } .visible-xs   diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index 978f4b87564..84e05cd6d88 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -2,34 +2,8 @@ - if current_user .top-area - .nav-links.snippet-scope-menu - %li{ class: ("active" unless params[:scope]) } - = link_to namespace_project_snippets_path(@project.namespace, @project) do - All - %span.badge - - if @project.team.member?(current_user) || current_user.admin? - = @project.snippets.count - - else - = @project.snippets.public_and_internal.count - - - if @project.team.member?(current_user) || current_user.admin? - %li{ class: ("active" if params[:scope] == "are_private") } - = link_to namespace_project_snippets_path(@project.namespace, @project, scope: 'are_private') do - Private - %span.badge - = @project.snippets.are_private.count - - %li{ class: ("active" if params[:scope] == "are_internal") } - = link_to namespace_project_snippets_path(@project.namespace, @project, scope: 'are_internal') do - Internal - %span.badge - = @project.snippets.are_internal.count - - %li{ class: ("active" if params[:scope] == "are_public") } - = link_to namespace_project_snippets_path(@project.namespace, @project, scope: 'are_public') do - Public - %span.badge - = @project.snippets.are_public.count + - include_private = @project.team.member?(current_user) || current_user.admin? + = render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private } .nav-controls.hidden-xs - if can?(current_user, :create_project_snippet, @project) diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml new file mode 100644 index 00000000000..cb837f1fac1 --- /dev/null +++ b/app/views/snippets/_snippets_scope_menu.html.haml @@ -0,0 +1,31 @@ +- subject = local_assigns.fetch(:subject, current_user) +- include_private = local_assigns.fetch(:include_private, false) + +.nav-links.snippet-scope-menu + %li{ class: ("active" unless params[:scope]) } + = link_to snippets_path(subject) do + All + %span.badge + - if include_private + = subject.snippets.count + - else + = subject.snippets.public_and_internal.count + + - if include_private + %li{ class: ("active" if params[:scope] == "are_private") } + = link_to snippets_path(subject, scope: 'are_private') do + Private + %span.badge + = subject.snippets.are_private.count + + %li{ class: ("active" if params[:scope] == "are_internal") } + = link_to snippets_path(subject, scope: 'are_internal') do + Internal + %span.badge + = subject.snippets.are_internal.count + + %li{ class: ("active" if params[:scope] == "are_public") } + = link_to snippets_path(subject, scope: 'are_public') do + Public + %span.badge + = subject.snippets.are_public.count -- cgit v1.2.1 From a68735d4985bf5ffaeaf5a051b40f8aed0c0a6e0 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Fri, 9 Dec 2016 15:43:02 -0600 Subject: use Snippet.none in favor of nil to allow chaining --- app/finders/snippets_finder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index 31f039b5a70..78a2f8840ed 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -51,7 +51,7 @@ class SnippetsFinder def by_scope(snippets, scope = nil, include_private = false) case scope.to_s when 'are_private' - include_private ? snippets.are_private : nil + include_private ? snippets.are_private : Snippet.none when 'are_internal' snippets.are_internal when 'are_public' -- cgit v1.2.1 From dccd53e1ce1903f2df8dd20023e2bafd96d850f3 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Fri, 9 Dec 2016 16:01:08 -0600 Subject: add new tests for snippets_finder.rb --- spec/finders/snippets_finder_spec.rb | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index 28bdc18e840..21ccdaa6c38 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -84,16 +84,39 @@ describe SnippetsFinder do expect(snippets).not_to include(@snippet1, @snippet2) end - it "returns public and internal snippets for none project members" do + it "returns public and internal snippets for non project members" do snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) expect(snippets).to include(@snippet2, @snippet3) expect(snippets).not_to include(@snippet1) end + it "returns public snippets for non project members" do + snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_public") + expect(snippets).to include(@snippet3) + expect(snippets).not_to include(@snippet1, @snippet2) + end + + it "returns internal snippets for non project members" do + snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_internal") + expect(snippets).to include(@snippet2) + expect(snippets).not_to include(@snippet1, @snippet3) + end + + it "does not return private snippets for non project members" do + snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_private") + expect(snippets).not_to include(@snippet1, @snippet2, @snippet3) + end + it "returns all snippets for project members" do project1.team << [user, :developer] snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) expect(snippets).to include(@snippet1, @snippet2, @snippet3) end + + it "returns private snippets for project members" do + project1.team << [user, :developer] + snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_private") + expect(snippets).to include(@snippet1) + end end end -- cgit v1.2.1 From 730ff2e50b600f3e3c79e69fd4978faf6a06ce1b Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Fri, 9 Dec 2016 17:33:23 -0600 Subject: rename snippets_path helper due to conflict --- app/helpers/snippets_helper.rb | 2 +- app/views/snippets/_snippets_scope_menu.html.haml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index fc7febd3385..8c02b4061ca 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -11,7 +11,7 @@ module SnippetsHelper # Return the path of a snippets index for a user or for a project # # @returns String, path to snippet index - def snippets_path(subject = nil, opts = nil) + def subject_snippets_path(subject = nil, opts = nil) if subject.is_a?(Project) namespace_project_snippets_path(subject.namespace, subject, opts) else # assume subject === User diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml index cb837f1fac1..2dda5fed647 100644 --- a/app/views/snippets/_snippets_scope_menu.html.haml +++ b/app/views/snippets/_snippets_scope_menu.html.haml @@ -3,7 +3,7 @@ .nav-links.snippet-scope-menu %li{ class: ("active" unless params[:scope]) } - = link_to snippets_path(subject) do + = link_to subject_snippets_path(subject) do All %span.badge - if include_private @@ -13,19 +13,19 @@ - if include_private %li{ class: ("active" if params[:scope] == "are_private") } - = link_to snippets_path(subject, scope: 'are_private') do + = link_to subject_snippets_path(subject, scope: 'are_private') do Private %span.badge = subject.snippets.are_private.count %li{ class: ("active" if params[:scope] == "are_internal") } - = link_to snippets_path(subject, scope: 'are_internal') do + = link_to subject_snippets_path(subject, scope: 'are_internal') do Internal %span.badge = subject.snippets.are_internal.count %li{ class: ("active" if params[:scope] == "are_public") } - = link_to snippets_path(subject, scope: 'are_public') do + = link_to subject_snippets_path(subject, scope: 'are_public') do Public %span.badge = subject.snippets.are_public.count -- cgit v1.2.1 From 9355ba238b7ffde61888fb21a1ed1b33414a7d9a Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Fri, 9 Dec 2016 16:36:05 -0600 Subject: normalize author email so we can treat it as case insensitive --- app/assets/javascripts/graphs/stat_graph_contributors_util.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js index 051ff98c774..1982f4af939 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js @@ -2,7 +2,7 @@ (function() { window.ContributorsStatGraphUtil = { parse_log: function(log) { - var by_author, by_email, data, entry, i, len, total; + var by_author, by_email, data, entry, i, len, total, normalized_email; total = {}; by_author = {}; by_email = {}; @@ -11,7 +11,8 @@ if (total[entry.date] == null) { this.add_date(entry.date, total); } - data = by_author[entry.author_name] || by_email[entry.author_email]; + normalized_email = entry.author_email.toLowerCase(); + data = by_author[entry.author_name] || by_email[normalized_email]; if (data == null) { data = this.add_author(entry, by_author, by_email); } @@ -32,12 +33,14 @@ return collection[date].date = date; }, add_author: function(author, by_author, by_email) { - var data; + var data, normalized_email; data = {}; data.author_name = author.author_name; data.author_email = author.author_email; + normalized_email = author.author_email.toLowerCase(); by_author[author.author_name] = data; - return by_email[author.author_email] = data; + by_email[normalized_email] = data; + return data; }, store_data: function(entry, total, by_author) { this.store_commits(total, by_author); -- cgit v1.2.1 From 9e3b17faab915a0f422ed88f1014878856c188b7 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Fri, 9 Dec 2016 16:45:10 -0600 Subject: add CHANGELOG entry for !8021 --- changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml diff --git a/changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml b/changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml new file mode 100644 index 00000000000..742b10e72aa --- /dev/null +++ b/changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml @@ -0,0 +1,4 @@ +--- +title: group authors in contribution graph with case insensitive email handle comparison +merge_request: 8021 +author: -- cgit v1.2.1 From bc31040ea9344e8bc4d3c90fa49a6e30ce9c3698 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" <lbennett@gitlab.com> Date: Sat, 10 Dec 2016 12:04:54 +0000 Subject: Fixed lint warning and propose fail or warning --- package.json | 2 +- spec/javascripts/project_title_spec.js | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 961989f8012..49b8210e427 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "scripts": { - "eslint": "eslint --ext .js,.js.es6 .", + "eslint": "eslint --max-warnings 0 --ext .js,.js.es6 .", "eslint-fix": "npm run eslint -- --fix", "eslint-report": "npm run eslint -- --format html --output-file ./eslint-report.html" }, diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index 49211a6b852..65de1756201 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -21,16 +21,18 @@ return this.project = new Project(); }); return describe('project list', function() { + var fakeAjaxResponse = function fakeAjaxResponse(req) { + var d; + expect(req.url).toBe('/api/v3/projects.json?simple=true'); + d = $.Deferred(); + d.resolve(this.projects_data); + return d.promise(); + }; + beforeEach((function(_this) { return function() { _this.projects_data = fixture.load('projects.json')[0]; - return spyOn(jQuery, 'ajax').and.callFake(function(req) { - var d; - expect(req.url).toBe('/api/v3/projects.json?simple=true'); - d = $.Deferred(); - d.resolve(_this.projects_data); - return d.promise(); - }); + return spyOn(jQuery, 'ajax').and.callFake(fakeAjaxResponse.bind(_this)); }; })(this)); it('to show on toggle click', (function(_this) { -- cgit v1.2.1 From d8b7df3cbcfd4fcdf204fdcba01720e60fb598bf Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Thu, 8 Dec 2016 20:59:41 +0200 Subject: Add support for nested groups to admin routing Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- app/views/admin/dashboard/_head.html.haml | 2 +- app/views/admin/dashboard/index.html.haml | 2 +- app/views/admin/groups/_group.html.haml | 2 +- app/views/admin/groups/show.html.haml | 4 ++-- app/views/admin/projects/index.html.haml | 12 +++++------ app/views/repository_check_mailer/notify.html.haml | 2 +- app/views/repository_check_mailer/notify.text.haml | 2 +- config/routes/admin.rb | 23 +++++++++++++++------- features/steps/shared/paths.rb | 2 +- spec/features/admin/admin_groups_spec.rb | 2 +- spec/features/admin/admin_projects_spec.rb | 6 +++--- spec/features/security/admin_access_spec.rb | 2 +- spec/routing/admin_routing_spec.rb | 14 ++++++++++++- 13 files changed, 48 insertions(+), 27 deletions(-) diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml index ec40391a3e3..b5f96363230 100644 --- a/app/views/admin/dashboard/_head.html.haml +++ b/app/views/admin/dashboard/_head.html.haml @@ -8,7 +8,7 @@ %span Overview = nav_link(controller: [:admin, :projects]) do - = link_to admin_namespaces_projects_path, title: 'Projects' do + = link_to admin_projects_path, title: 'Projects' do %span Projects = nav_link(controller: :users) do diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index e51f4ac1d93..5238623e936 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -116,7 +116,7 @@ .light-well.well-centered %h4 Projects .data - = link_to admin_namespaces_projects_path do + = link_to admin_projects_path do %h1= number_with_delimiter(Project.cached_count) %hr = link_to('New Project', new_project_path, class: "btn btn-new") diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml index 664bb417c6a..4efeec0ea4e 100644 --- a/app/views/admin/groups/_group.html.haml +++ b/app/views/admin/groups/_group.html.haml @@ -2,7 +2,7 @@ %li.group-row{ class: css_class } .controls - = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn' + = link_to 'Edit', admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn' = link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove' .stats %span diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 40871e32913..71a605f33b1 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -2,7 +2,7 @@ %h3.page-title Group: #{@group.name} - = link_to edit_admin_group_path(@group), class: "btn pull-right" do + = link_to admin_group_edit_path(@group), class: "btn pull-right" do %i.fa.fa-pencil-square-o Edit %hr @@ -88,7 +88,7 @@ Read more about project permissions %strong= link_to "here", help_page_path("user/permissions"), class: "vlink" - = form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do + = form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do %div = users_select_tag(:user_ids, multiple: true, email_user: true, scope: :all) %div.prepend-top-10 diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index b37b8d4fee7..8bc7dc7dd51 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -7,7 +7,7 @@ %div{ class: container_class } .top-area .prepend-top-default - = form_tag admin_namespaces_projects_path, method: :get do |f| + = form_tag admin_projects_path, method: :get do |f| .search-holder .search-field-holder = search_field_tag :name, params[:name], class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false, placeholder: 'Search by name' @@ -41,19 +41,19 @@ = button_tag "Search", class: "btn btn-primary btn-search" %ul.nav-links - - opts = params[:visibility_level].present? ? {} : { page: admin_namespaces_projects_path } + - opts = params[:visibility_level].present? ? {} : { page: admin_projects_path } = nav_link(opts) do - = link_to admin_namespaces_projects_path do + = link_to admin_projects_path do All = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s ? 'active' : '' }) do - = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do + = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do Private = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s ? 'active' : '' }) do - = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do + = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do Internal = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s ? 'active' : '' }) do - = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do + = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do Public .nav-controls diff --git a/app/views/repository_check_mailer/notify.html.haml b/app/views/repository_check_mailer/notify.html.haml index a585147ddd1..94e5a5d9709 100644 --- a/app/views/repository_check_mailer/notify.html.haml +++ b/app/views/repository_check_mailer/notify.html.haml @@ -2,7 +2,7 @@ #{@message}. %p - = link_to "See the affected projects in the GitLab admin panel", admin_namespaces_projects_url(last_repository_check_failed: 1) + = link_to "See the affected projects in the GitLab admin panel", admin_projects_url(last_repository_check_failed: 1) %p You are receiving this message because you are a GitLab administrator for #{Gitlab.config.gitlab.url}. diff --git a/app/views/repository_check_mailer/notify.text.haml b/app/views/repository_check_mailer/notify.text.haml index 93db151329e..0902c50d052 100644 --- a/app/views/repository_check_mailer/notify.text.haml +++ b/app/views/repository_check_mailer/notify.text.haml @@ -1,6 +1,6 @@ #{@message}. \ -View details: #{admin_namespaces_projects_url(last_repository_check_failed: 1)} +View details: #{admin_projects_url(last_repository_check_failed: 1)} You are receiving this message because you are a GitLab administrator for #{Gitlab.config.gitlab.url}. diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 5ae985da561..0dd2c8f7aef 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -28,9 +28,19 @@ namespace :admin do resources :applications - resources :groups, constraints: { id: /[^\/]+/ } do - member do + resources :groups, only: [:index, :new, :create] + + scope(path: 'groups/*id', + controller: :groups, + constraints: { id: Gitlab::Regex.namespace_route_regex }) do + + scope(as: :group) do put :members_update + get :edit, action: :edit + get '/', action: :show + patch '/', action: :update + put '/', action: :update + delete '/', action: :destroy end end @@ -50,14 +60,13 @@ namespace :admin do resource :system_info, controller: 'system_info', only: [:show] resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ } - resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do - root to: 'projects#index', as: :projects + resources :projects, only: [:index] + scope(path: 'projects/*namespace_id', as: :namespace) do resources(:projects, path: '/', - constraints: { id: /[a-zA-Z.0-9_\-]+/ }, - only: [:index, :show]) do - root to: 'projects#show' + constraints: { id: Gitlab::Regex.project_route_regex }, + only: [:show]) do member do put :transfer diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index 2bd8ea745e4..398160449ca 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -168,7 +168,7 @@ module SharedPaths end step 'I visit admin projects page' do - visit admin_namespaces_projects_path + visit admin_projects_path end step 'I visit admin users page' do diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb index f6d625fa7f6..0aa01fc499a 100644 --- a/spec/features/admin/admin_groups_spec.rb +++ b/spec/features/admin/admin_groups_spec.rb @@ -21,7 +21,7 @@ feature 'Admin Groups', feature: true do scenario 'shows the visibility level radio populated with the group visibility_level value' do group = create(:group, :private) - visit edit_admin_group_path(group) + visit admin_group_edit_path(group) expect_selected_visibility(group.visibility_level) end diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index 30ded9202a4..a36bfd574cb 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -8,11 +8,11 @@ describe "Admin::Projects", feature: true do describe "GET /admin/projects" do before do - visit admin_namespaces_projects_path + visit admin_projects_path end it "is ok" do - expect(current_path).to eq(admin_namespaces_projects_path) + expect(current_path).to eq(admin_projects_path) end it "has projects list" do @@ -22,7 +22,7 @@ describe "Admin::Projects", feature: true do describe "GET /admin/projects/:id" do before do - visit admin_namespaces_projects_path + visit admin_projects_path click_link "#{@project.name}" end diff --git a/spec/features/security/admin_access_spec.rb b/spec/features/security/admin_access_spec.rb index fe8cd7b7602..e180ca53eb5 100644 --- a/spec/features/security/admin_access_spec.rb +++ b/spec/features/security/admin_access_spec.rb @@ -4,7 +4,7 @@ describe "Admin::Projects", feature: true do include AccessMatchers describe "GET /admin/projects" do - subject { admin_namespaces_projects_path } + subject { admin_projects_path } it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for :user } diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb index 69eeb45ed71..661b671301e 100644 --- a/spec/routing/admin_routing_spec.rb +++ b/spec/routing/admin_routing_spec.rb @@ -66,7 +66,8 @@ describe Admin::ProjectsController, "routing" do end it "to #show" do - expect(get("/admin/projects/gitlab")).to route_to('admin/projects#show', namespace_id: 'gitlab') + expect(get("/admin/projects/gitlab/gitlab-ce")).to route_to('admin/projects#show', namespace_id: 'gitlab', id: 'gitlab-ce') + expect(get("/admin/projects/gitlab/subgroup/gitlab-ce")).to route_to('admin/projects#show', namespace_id: 'gitlab/subgroup', id: 'gitlab-ce') end end @@ -119,3 +120,14 @@ describe Admin::HealthCheckController, "routing" do expect(get("/admin/health_check")).to route_to('admin/health_check#show') end end + +describe Admin::GroupsController, "routing" do + it "to #index" do + expect(get("/admin/groups")).to route_to('admin/groups#index') + end + + it "to #show" do + expect(get("/admin/groups/gitlab")).to route_to('admin/groups#show', id: 'gitlab') + expect(get("/admin/groups/gitlab/subgroup")).to route_to('admin/groups#show', id: 'gitlab/subgroup') + end +end -- cgit v1.2.1 From b552a4eb18ad1f3e8b9c0a4e56898a36d3d9d8de Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Wed, 7 Dec 2016 19:16:02 +0200 Subject: Validate presence of route by Routable concern Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- app/models/concerns/routable.rb | 1 + spec/models/concerns/routable_spec.rb | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index d36bb9da296..8d377484473 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -7,6 +7,7 @@ module Routable has_one :route, as: :source, autosave: true, dependent: :destroy validates_associated :route + validates :route, presence: true before_validation :update_route_path, if: :full_path_changed? end diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb index 0acefc0c1d5..d6e93b4bf7b 100644 --- a/spec/models/concerns/routable_spec.rb +++ b/spec/models/concerns/routable_spec.rb @@ -3,6 +3,10 @@ require 'spec_helper' describe Group, 'Routable' do let!(:group) { create(:group) } + describe 'Validations' do + it { is_expected.to validate_presence_of(:route) } + end + describe 'Associations' do it { is_expected.to have_one(:route).dependent(:destroy) } end -- cgit v1.2.1 From d7010fdcd7c73c32c71cc4c635472014597cee81 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Wed, 7 Dec 2016 19:16:17 +0200 Subject: Create nested group factory Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- spec/factories/groups.rb | 4 ++++ spec/models/group_spec.rb | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb index ebd3595ea64..ece6beb9fa9 100644 --- a/spec/factories/groups.rb +++ b/spec/factories/groups.rb @@ -19,5 +19,9 @@ FactoryGirl.define do trait :access_requestable do request_access_enabled true end + + trait :nested do + parent factory: :group + end end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 1613a586a2c..850b1a3cf1e 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -271,4 +271,11 @@ describe Group, models: true do expect(group.web_url).to include("groups/#{group.name}") end end + + describe 'nested group' do + subject { create(:group, :nested) } + + it { is_expected.to be_valid } + it { expect(subject.parent).to be_kind_of(Group) } + end end -- cgit v1.2.1 From 3e0b8e000f088c24493e993139f8af1be2e14de1 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Fri, 9 Dec 2016 17:27:11 +0200 Subject: Rename Routable.where_paths_in to Routable.where_full_path_in Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- app/models/concerns/routable.rb | 6 +++--- lib/banzai/filter/abstract_reference_filter.rb | 2 +- spec/models/concerns/routable_spec.rb | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index 8d377484473..1108a64c59e 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -29,17 +29,17 @@ module Routable order_sql = "(CASE WHEN #{binary} routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)" - where_paths_in([path]).reorder(order_sql).take + where_full_path_in([path]).reorder(order_sql).take end # Builds a relation to find multiple objects by their full paths. # # Usage: # - # Klass.where_paths_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee}) + # Klass.where_full_path_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee}) # # Returns an ActiveRecord::Relation. - def where_paths_in(paths) + def where_full_path_in(paths) wheres = [] cast_lower = Gitlab::Database.postgresql? diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index d904a8bd4ae..fd74eeaebe7 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -248,7 +248,7 @@ module Banzai end def projects_relation_for_paths(paths) - Project.where_paths_in(paths).includes(:namespace) + Project.where_full_path_in(paths).includes(:namespace) end # Returns projects for the given paths. diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb index d6e93b4bf7b..b556135532f 100644 --- a/spec/models/concerns/routable_spec.rb +++ b/spec/models/concerns/routable_spec.rb @@ -39,16 +39,16 @@ describe Group, 'Routable' do it { expect(described_class.find_by_full_path('unknown')).to eq(nil) } end - describe '.where_paths_in' do + describe '.where_full_path_in' do context 'without any paths' do it 'returns an empty relation' do - expect(described_class.where_paths_in([])).to eq([]) + expect(described_class.where_full_path_in([])).to eq([]) end end context 'without any valid paths' do it 'returns an empty relation' do - expect(described_class.where_paths_in(%w[unknown])).to eq([]) + expect(described_class.where_full_path_in(%w[unknown])).to eq([]) end end @@ -56,13 +56,13 @@ describe Group, 'Routable' do let!(:nested_group) { create(:group, parent: group) } it 'returns the projects matching the paths' do - result = described_class.where_paths_in([group.to_param, nested_group.to_param]) + result = described_class.where_full_path_in([group.to_param, nested_group.to_param]) expect(result).to contain_exactly(group, nested_group) end it 'returns projects regardless of the casing of paths' do - result = described_class.where_paths_in([group.to_param.upcase, nested_group.to_param.upcase]) + result = described_class.where_full_path_in([group.to_param.upcase, nested_group.to_param.upcase]) expect(result).to contain_exactly(group, nested_group) end -- cgit v1.2.1 From e71ed01bfbaef088e8f7927b01d5c470b4cfc5f2 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Sun, 11 Dec 2016 10:18:44 +0100 Subject: Change docs title to represent the edition --- doc/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/README.md b/doc/README.md index 66c8c26e4f0..eba1e9845b1 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,4 +1,4 @@ -# Documentation +# GitLab Community Edition documentation ## User documentation -- cgit v1.2.1 From d6a1a3e6bee6417d02a2095bce479f494460ce17 Mon Sep 17 00:00:00 2001 From: Ismail S <astroman3d@gmail.com> Date: Sun, 11 Dec 2016 14:14:59 +0000 Subject: Fix typo --- doc/ci/review_apps/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md index a66165dc973..c679ea4e298 100644 --- a/doc/ci/review_apps/index.md +++ b/doc/ci/review_apps/index.md @@ -33,7 +33,7 @@ built and deployed under a dynamic environment and can be previewed with an also dynamically URL. The details of the Review Apps implementation depend widely on your real -technology stack and on your deployment process. The simplest case it to +technology stack and on your deployment process. The simplest case is to deploy a simple static HTML website, but it will not be that straightforward when your app is using a database for example. To make a branch be deployed on a temporary instance and booting up this instance with all required software -- cgit v1.2.1 From 36c24de5da7feb6392e8250e2d9e11e602d3e527 Mon Sep 17 00:00:00 2001 From: Georg Hartmann <mail@georg-hartmann.com> Date: Sun, 11 Dec 2016 19:36:01 +0000 Subject: Fix typo in curl example request --- doc/api/merge_requests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 81df55ab4ab..662cc9da733 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -429,7 +429,7 @@ DELETE /projects/:id/merge_requests/:merge_request_id | `merge_request_id` | integer | yes | The ID of a project's merge request | ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_request/85 +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_requests/85 ``` ## Accept MR -- cgit v1.2.1 From e117cb3a1965c15d3d5617addb695f337dcf069f Mon Sep 17 00:00:00 2001 From: Connor Shea <connor.james.shea@gmail.com> Date: Sun, 11 Dec 2016 23:22:18 -0700 Subject: Update Sidekiq from 4.2.1 to 4.2.7. Includes various bug fixes, mostly for Rails 5. Changelog: https://github.com/mperham/sidekiq/blob/fc168fe393bee3ad1fcbb52cff2d84af86c38cc4/Changes.md --- Gemfile | 2 +- Gemfile.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index f27d6363e3d..2cc7764e6b8 100644 --- a/Gemfile +++ b/Gemfile @@ -132,7 +132,7 @@ gem 'after_commit_queue', '~> 1.3.0' gem 'acts-as-taggable-on', '~> 4.0' # Background jobs -gem 'sidekiq', '~> 4.2' +gem 'sidekiq', '~> 4.2.7' gem 'sidekiq-cron', '~> 0.4.4' gem 'redis-namespace', '~> 1.5.2' gem 'sidekiq-limit_fetch', '~> 3.4' diff --git a/Gemfile.lock b/Gemfile.lock index c464ff70587..3de1a7cbf26 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -126,7 +126,7 @@ GEM coffee-script-source (1.10.0) colorize (0.7.7) concurrent-ruby (1.0.2) - connection_pool (2.2.0) + connection_pool (2.2.1) crack (0.4.3) safe_yaml (~> 1.0.0) creole (0.5.0) @@ -648,10 +648,10 @@ GEM rack shoulda-matchers (2.8.0) activesupport (>= 3.0.0) - sidekiq (4.2.1) + sidekiq (4.2.7) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) - rack-protection (~> 1.5) + rack-protection (>= 1.5.0) redis (~> 3.2, >= 3.2.1) sidekiq-cron (0.4.4) redis-namespace (>= 1.5.2) @@ -928,7 +928,7 @@ DEPENDENCIES settingslogic (~> 2.0.9) sham_rack (~> 1.3.6) shoulda-matchers (~> 2.8.0) - sidekiq (~> 4.2) + sidekiq (~> 4.2.7) sidekiq-cron (~> 0.4.4) sidekiq-limit_fetch (~> 3.4) simplecov (= 0.12.0) -- cgit v1.2.1 From 8d5f7e26cd63e60fbcd1cbe6f5e6b77f90119481 Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Fri, 2 Dec 2016 21:46:50 +0600 Subject: adds check for logged in user in group issues --- app/views/groups/issues.html.haml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 324a116a50e..872d70d17d9 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -6,13 +6,14 @@ - if group_issues(@group).exists? .top-area = render 'shared/issuable/nav', type: :issues - .nav-controls - - if current_user - = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do - = icon('rss') - %span.icon-label - Subscribe - = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" + - if current_user + .nav-controls + - if current_user + = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do + = icon('rss') + %span.icon-label + Subscribe + = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" = render 'shared/issuable/filter', type: :issues -- cgit v1.2.1 From fd20d0b19e17603c886189148e9f59caab50ae19 Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Fri, 2 Dec 2016 21:55:56 +0600 Subject: adds changelog --- changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml diff --git a/changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml b/changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml new file mode 100644 index 00000000000..62030d3fc45 --- /dev/null +++ b/changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml @@ -0,0 +1,4 @@ +--- +title: Prevent user creating issue or MR without signing in for a group +merge_request: 7902 +author: -- cgit v1.2.1 From 0bde5c946ab31f822e1146b0e242fdbd1553733e Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Mon, 5 Dec 2016 13:51:57 +0600 Subject: hides new MR button from not loggedin user --- app/views/groups/merge_requests.html.haml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index e6953d94531..dbbdb583a24 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -2,8 +2,9 @@ .top-area = render 'shared/issuable/nav', type: :merge_requests - .nav-controls - = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request" + - if current_user + .nav-controls + = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request" = render 'shared/issuable/filter', type: :merge_requests -- cgit v1.2.1 From c877b152d1354946d0a1299e672c1be0aaafdfdf Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Mon, 12 Dec 2016 13:02:08 +0600 Subject: removes extra if check --- app/views/groups/issues.html.haml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 872d70d17d9..b4aa4f24d9e 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -8,11 +8,10 @@ = render 'shared/issuable/nav', type: :issues - if current_user .nav-controls - - if current_user - = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do - = icon('rss') - %span.icon-label - Subscribe + = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do + = icon('rss') + %span.icon-label + Subscribe = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" = render 'shared/issuable/filter', type: :issues -- cgit v1.2.1 From 3c36d9dc9b3c2db45df6dce19357e9c4bdde366f Mon Sep 17 00:00:00 2001 From: jnoortheen <jnoortheen@gmail.com> Date: Tue, 6 Dec 2016 22:32:30 +0530 Subject: fix: removed signed_out notification test: replaced signed_out message check with check for sign_in button fixes #25294 --- app/controllers/sessions_controller.rb | 6 ++++++ changelogs/unreleased/25294-remove-signed-out-msg.yml | 4 ++++ spec/support/login_helpers.rb | 3 ++- 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/25294-remove-signed-out-msg.yml diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 38e7c6f4a48..8c698695202 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -37,6 +37,12 @@ class SessionsController < Devise::SessionsController end end + def destroy + super + # hide the signed_out notice + flash[:notice] = nil + end + private # Handle an "initial setup" state, where there's only one user, it's an admin, diff --git a/changelogs/unreleased/25294-remove-signed-out-msg.yml b/changelogs/unreleased/25294-remove-signed-out-msg.yml new file mode 100644 index 00000000000..567294fe5f7 --- /dev/null +++ b/changelogs/unreleased/25294-remove-signed-out-msg.yml @@ -0,0 +1,4 @@ +--- +title: 'fix: removed signed_out notification' +merge_request: 7958 +author: jnoortheen diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb index c0b3e83244d..ad1eed5b369 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/login_helpers.rb @@ -75,7 +75,8 @@ module LoginHelpers def logout find(".header-user-dropdown-toggle").click click_link "Sign out" - expect(page).to have_content('Signed out successfully') + # check the sign_in button + expect(page).to have_button('Sign in') end # Logout without JavaScript driver -- cgit v1.2.1 From e1f563109c19a08405170d281a7ac537b7c881ff Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Mon, 12 Dec 2016 09:54:27 +0000 Subject: Fixed GFM autocomplete to disallow non-word characters in string Closes #25540 --- app/assets/javascripts/gfm_auto_complete.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 6f9d6283071..076475d3ed4 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -69,7 +69,7 @@ _a = decodeURI("%C3%80"); _y = decodeURI("%C3%BF"); - regexp = new RegExp("(?:\\B|\\W|\\s)" + flag + "(?!\\W)([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)|([^\\x00-\\xff]*)$", 'gi'); + regexp = new RegExp("(?:\\B|\\W|\\s)" + flag + "(?!\\W)([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)$", 'gi'); match = regexp.exec(subtext); -- cgit v1.2.1 From 4bb3c4644734a8f23bcfbe97018431f3a2bafedb Mon Sep 17 00:00:00 2001 From: Semyon Pupkov <mail@semyonpupkov.com> Date: Mon, 12 Dec 2016 14:57:46 +0500 Subject: Move admin active tab spinach tests to rspec https://gitlab.com/gitlab-org/gitlab-ce/issues/23036 --- ...ove-admin-active-tab-spinach-tests-to-rspec.yml | 4 + features/admin/active_tab.feature | 54 ------------- features/steps/admin/active_tab.rb | 41 ---------- spec/features/admin/admin_active_tab_spec.rb | 90 ++++++++++++++++++++++ 4 files changed, 94 insertions(+), 95 deletions(-) create mode 100644 changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml delete mode 100644 features/admin/active_tab.feature delete mode 100644 features/steps/admin/active_tab.rb create mode 100644 spec/features/admin/admin_active_tab_spec.rb diff --git a/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml b/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml new file mode 100644 index 00000000000..11250643a23 --- /dev/null +++ b/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml @@ -0,0 +1,4 @@ +--- +title: Move admin active tab spinach tests to rspec +merge_request: 8037 +author: Semyon Pupkov diff --git a/features/admin/active_tab.feature b/features/admin/active_tab.feature deleted file mode 100644 index f5bb06dea7d..00000000000 --- a/features/admin/active_tab.feature +++ /dev/null @@ -1,54 +0,0 @@ -@admin -Feature: Admin Active Tab - Background: - Given I sign in as an admin - - Scenario: On Admin Home - Given I visit admin page - Then the active main tab should be Overview - And no other main tabs should be active - - Scenario: On Admin Projects - Given I visit admin projects page - Then the active main tab should be Overview - And the active sub tab should be Projects - And no other main tabs should be active - And no other sub tabs should be active - - Scenario: On Admin Groups - Given I visit admin groups page - Then the active main tab should be Overview - And the active sub tab should be Groups - And no other main tabs should be active - And no other sub tabs should be active - - Scenario: On Admin Users - Given I visit admin users page - Then the active main tab should be Overview - And the active sub tab should be Users - And no other main tabs should be active - And no other sub tabs should be active - - Scenario: On Admin Logs - Given I visit admin logs page - Then the active main tab should be Monitoring - And the active sub tab should be Logs - And no other main tabs should be active - And no other sub tabs should be active - - Scenario: On Admin Messages - Given I visit admin messages page - Then the active main tab should be Messages - And no other main tabs should be active - - Scenario: On Admin Hooks - Given I visit admin hooks page - Then the active main tab should be Hooks - And no other main tabs should be active - - Scenario: On Admin Resque - Given I visit admin Resque page - Then the active main tab should be Monitoring - And the active sub tab should be Resque - And no other main tabs should be active - And no other sub tabs should be active diff --git a/features/steps/admin/active_tab.rb b/features/steps/admin/active_tab.rb deleted file mode 100644 index 9b1689a8198..00000000000 --- a/features/steps/admin/active_tab.rb +++ /dev/null @@ -1,41 +0,0 @@ -class Spinach::Features::AdminActiveTab < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedActiveTab - - step 'the active main tab should be Overview' do - ensure_active_main_tab('Overview') - end - - step 'the active sub tab should be Projects' do - ensure_active_sub_tab('Projects') - end - - step 'the active sub tab should be Groups' do - ensure_active_sub_tab('Groups') - end - - step 'the active sub tab should be Users' do - ensure_active_sub_tab('Users') - end - - step 'the active main tab should be Hooks' do - ensure_active_main_tab('Hooks') - end - - step 'the active main tab should be Monitoring' do - ensure_active_main_tab('Monitoring') - end - - step 'the active sub tab should be Resque' do - ensure_active_sub_tab('Background Jobs') - end - - step 'the active sub tab should be Logs' do - ensure_active_sub_tab('Logs') - end - - step 'the active main tab should be Messages' do - ensure_active_main_tab('Messages') - end -end diff --git a/spec/features/admin/admin_active_tab_spec.rb b/spec/features/admin/admin_active_tab_spec.rb new file mode 100644 index 00000000000..f2eecc5b552 --- /dev/null +++ b/spec/features/admin/admin_active_tab_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +RSpec.describe 'admin active tab' do + before do + login_as :admin + end + + shared_examples 'page has active tab' do |title| + it "activates #{title} tab" do + expect(page).to have_selector('.layout-nav .nav-links > li.active', count: 1) + expect(page.find('.layout-nav li.active')).to have_content(title) + end + end + + shared_examples 'page has active sub tab' do |title| + it "activates #{title} sub tab" do + expect(page).to have_selector('.sub-nav li.active', count: 1) + expect(page.find('.sub-nav li.active')).to have_content(title) + end + end + + context 'on home page' do + before do + visit admin_root_path + end + + it_behaves_like 'page has active tab', 'Overview' + end + + context 'on projects' do + before do + visit admin_namespaces_projects_path + end + + it_behaves_like 'page has active tab', 'Overview' + it_behaves_like 'page has active sub tab', 'Projects' + end + + context 'on groups' do + before do + visit admin_groups_path + end + + it_behaves_like 'page has active tab', 'Overview' + it_behaves_like 'page has active sub tab', 'Groups' + end + + context 'on users' do + before do + visit admin_users_path + end + + it_behaves_like 'page has active tab', 'Overview' + it_behaves_like 'page has active sub tab', 'Users' + end + + context 'on logs' do + before do + visit admin_logs_path + end + + it_behaves_like 'page has active tab', 'Monitoring' + it_behaves_like 'page has active sub tab', 'Logs' + end + + context 'on messages' do + before do + visit admin_broadcast_messages_path + end + + it_behaves_like 'page has active tab', 'Messages' + end + + context 'on hooks' do + before do + visit admin_hooks_path + end + + it_behaves_like 'page has active tab', 'Hooks' + end + + context 'on background jobs' do + before do + visit admin_background_jobs_path + end + + it_behaves_like 'page has active tab', 'Monitoring' + it_behaves_like 'page has active sub tab', 'Background Jobs' + end +end -- cgit v1.2.1 From 633e64382d30674fecb1aaf138d18c73827c9a40 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 8 Dec 2016 09:51:36 +0100 Subject: Added Ci::Status::Build --- app/models/ci/build.rb | 4 +++ app/models/commit_status.rb | 4 +++ lib/gitlab/ci/status/build/common.rb | 54 +++++++++++++++++++++++++++++++++++ lib/gitlab/ci/status/build/factory.rb | 15 ++++++++++ lib/gitlab/ci/status/core.rb | 10 +++++-- 5 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 lib/gitlab/ci/status/build/common.rb create mode 100644 lib/gitlab/ci/status/build/factory.rb diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 88c46076df6..0f4c498c266 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -100,6 +100,10 @@ module Ci end end + def detailed_status + Gitlab::Ci::Status::Build::Factory.new(self).fabricate! + end + def manual? self.when == 'manual' end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index cf90475f4d4..fce16174e22 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -131,4 +131,8 @@ class CommitStatus < ActiveRecord::Base def has_trace? false end + + def detailed_status + Gitlab::Ci::Status::Factory.new(self).fabricate! + end end diff --git a/lib/gitlab/ci/status/build/common.rb b/lib/gitlab/ci/status/build/common.rb new file mode 100644 index 00000000000..d3d7e03ee3f --- /dev/null +++ b/lib/gitlab/ci/status/build/common.rb @@ -0,0 +1,54 @@ +module Gitlab + module Ci + module Status + module Build + module Common + def has_details? + true + end + + def details_path + namespace_project_build_path(@subject.project.namespace, + @subject.project, + @subject.pipeline) + end + + def action_type + case + when @subject.playable? then :playable + when @subject.active? then :cancel + when @subject.retryable? then :retry + end + end + + def has_action?(current_user) + action_type && can?(current_user, :update_build, @subject) + end + + def action_icon + case action_type + when :playable then 'remove' + when :cancel then 'icon_play' + when :retry then 'repeat' + end + end + + def action_path + case action_type + when :playable + play_namespace_project_build_path(subject.project.namespace, subject.project, subject) + when :cancel + cancel_namespace_project_build_path(subject.project.namespace, subject.project, subject) + when :retry + retry_namespace_project_build_path(subject.project.namespace, subject.project, subject) + end + end + + def action_method + :post + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb new file mode 100644 index 00000000000..dd38e1418b6 --- /dev/null +++ b/lib/gitlab/ci/status/build/factory.rb @@ -0,0 +1,15 @@ +module Gitlab + module Ci + module Status + module Build + class Factory < Status::Factory + private + + def core_status + super.extend(Status::Build::Common) + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb index ce4108fdcf2..60c559248aa 100644 --- a/lib/gitlab/ci/status/core.rb +++ b/lib/gitlab/ci/status/core.rb @@ -34,15 +34,15 @@ module Gitlab end def has_details? - raise NotImplementedError + false end def details_path raise NotImplementedError end - def has_action? - raise NotImplementedError + def has_action?(_user = nil) + false end def action_icon @@ -52,6 +52,10 @@ module Gitlab def action_path raise NotImplementedError end + + def action_method + raise NotImplementedError + end end end end -- cgit v1.2.1 From 516dc7a5be3624f1866fa46f19f6472e2f9fae22 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 8 Dec 2016 10:40:56 +0100 Subject: Improve actions --- app/models/ci/build.rb | 4 +++ lib/gitlab/ci/status/build/common.rb | 26 ++++++------------- lib/gitlab/ci/status/build/factory.rb | 4 +++ lib/gitlab/ci/status/build/play.rb | 47 +++++++++++++++++++++++++++++++++++ lib/gitlab/ci/status/build/stop.rb | 47 +++++++++++++++++++++++++++++++++++ 5 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 lib/gitlab/ci/status/build/play.rb create mode 100644 lib/gitlab/ci/status/build/stop.rb diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 0f4c498c266..73564dd2aa0 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -127,6 +127,10 @@ module Ci end end + def cancelable? + active? + end + def retryable? project.builds_enabled? && commands.present? && complete? end diff --git a/lib/gitlab/ci/status/build/common.rb b/lib/gitlab/ci/status/build/common.rb index d3d7e03ee3f..2bed68d1a11 100644 --- a/lib/gitlab/ci/status/build/common.rb +++ b/lib/gitlab/ci/status/build/common.rb @@ -13,33 +13,23 @@ module Gitlab @subject.pipeline) end - def action_type - case - when @subject.playable? then :playable - when @subject.active? then :cancel - when @subject.retryable? then :retry - end - end - def has_action?(current_user) - action_type && can?(current_user, :update_build, @subject) + (subject.cancelable? || subject.retryable?) && + can?(current_user, :update_build, @subject) end def action_icon - case action_type - when :playable then 'remove' - when :cancel then 'icon_play' - when :retry then 'repeat' + case + when subject.cancelable? then 'icon_play' + when subject.retryable? then 'repeat' end end def action_path - case action_type - when :playable - play_namespace_project_build_path(subject.project.namespace, subject.project, subject) - when :cancel + case + when subject.cancelable? cancel_namespace_project_build_path(subject.project.namespace, subject.project, subject) - when :retry + when subject.retryable? retry_namespace_project_build_path(subject.project.namespace, subject.project, subject) end end diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb index dd38e1418b6..d8a9f53f236 100644 --- a/lib/gitlab/ci/status/build/factory.rb +++ b/lib/gitlab/ci/status/build/factory.rb @@ -5,6 +5,10 @@ module Gitlab class Factory < Status::Factory private + def extended_statuses + [Stop, Play] + end + def core_status super.extend(Status::Build::Common) end diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb new file mode 100644 index 00000000000..581c81d0175 --- /dev/null +++ b/lib/gitlab/ci/status/build/play.rb @@ -0,0 +1,47 @@ +module Gitlab + module Ci + module Status + module Status + class Play < SimpleDelegator + extend Status::Extended + + def text + 'play' + end + + def label + 'play' + end + + def icon + 'icon_status_skipped' + end + + def to_s + 'play' + end + + def has_action?(current_user) + can?(current_user, :update_build, subject) + end + + def action_icon + :play + end + + def action_path + play_namespace_project_build_path(subject.project.namespace, subject.project, subject) + end + + def action_method + :post + end + + def self.matches?(build) + build.playable? && !build.stops_environment? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb new file mode 100644 index 00000000000..261de9695c5 --- /dev/null +++ b/lib/gitlab/ci/status/build/stop.rb @@ -0,0 +1,47 @@ +module Gitlab + module Ci + module Status + module Status + class Play < SimpleDelegator + extend Status::Extended + + def text + 'stop' + end + + def label + 'stop' + end + + def icon + 'icon_status_skipped' + end + + def to_s + 'stop' + end + + def has_action?(current_user) + can?(current_user, :update_build, subject) + end + + def action_icon + :play + end + + def action_path + play_namespace_project_build_path(subject.project.namespace, subject.project, subject) + end + + def action_method + :post + end + + def self.matches?(build) + build.playable? && build.stops_environment? + end + end + end + end + end +end -- cgit v1.2.1 From 1b6c2c3c0a38bed733d861902eb8c9397ab76cd3 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 8 Dec 2016 10:48:08 +0100 Subject: Introduce `cancelable` and `returnable` [ci skip] --- lib/gitlab/ci/status/build/cancelable.rb | 31 +++++++++++++++++++++++++++++++ lib/gitlab/ci/status/build/common.rb | 25 ------------------------- lib/gitlab/ci/status/build/factory.rb | 2 +- lib/gitlab/ci/status/build/play.rb | 10 +--------- lib/gitlab/ci/status/build/retryable.rb | 31 +++++++++++++++++++++++++++++++ 5 files changed, 64 insertions(+), 35 deletions(-) create mode 100644 lib/gitlab/ci/status/build/cancelable.rb create mode 100644 lib/gitlab/ci/status/build/retryable.rb diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb new file mode 100644 index 00000000000..bff0464ef0c --- /dev/null +++ b/lib/gitlab/ci/status/build/cancelable.rb @@ -0,0 +1,31 @@ +module Gitlab + module Ci + module Status + module Status + class Cancelable < SimpleDelegator + extend Status::Extended + + def has_action?(current_user) + can?(current_user, :update_build, subject) + end + + def action_icon + 'remove' + end + + def action_path + cancel_namespace_project_build_path(subject.project.namespace, subject.project, subject) + end + + def action_method + :post + end + + def self.matches?(build) + build.cancelable? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/build/common.rb b/lib/gitlab/ci/status/build/common.rb index 2bed68d1a11..3e47d7dfd20 100644 --- a/lib/gitlab/ci/status/build/common.rb +++ b/lib/gitlab/ci/status/build/common.rb @@ -12,31 +12,6 @@ module Gitlab @subject.project, @subject.pipeline) end - - def has_action?(current_user) - (subject.cancelable? || subject.retryable?) && - can?(current_user, :update_build, @subject) - end - - def action_icon - case - when subject.cancelable? then 'icon_play' - when subject.retryable? then 'repeat' - end - end - - def action_path - case - when subject.cancelable? - cancel_namespace_project_build_path(subject.project.namespace, subject.project, subject) - when subject.retryable? - retry_namespace_project_build_path(subject.project.namespace, subject.project, subject) - end - end - - def action_method - :post - end end end end diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb index d8a9f53f236..8f420a93954 100644 --- a/lib/gitlab/ci/status/build/factory.rb +++ b/lib/gitlab/ci/status/build/factory.rb @@ -6,7 +6,7 @@ module Gitlab private def extended_statuses - [Stop, Play] + [Stop, Play, Cancelable, Retryable] end def core_status diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb index 581c81d0175..d295850137b 100644 --- a/lib/gitlab/ci/status/build/play.rb +++ b/lib/gitlab/ci/status/build/play.rb @@ -13,20 +13,12 @@ module Gitlab 'play' end - def icon - 'icon_status_skipped' - end - - def to_s - 'play' - end - def has_action?(current_user) can?(current_user, :update_build, subject) end def action_icon - :play + 'play' end def action_path diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb new file mode 100644 index 00000000000..b3c0eedadf8 --- /dev/null +++ b/lib/gitlab/ci/status/build/retryable.rb @@ -0,0 +1,31 @@ +module Gitlab + module Ci + module Status + module Status + class Retryable < SimpleDelegator + extend Status::Extended + + def has_action?(current_user) + can?(current_user, :update_build, subject) + end + + def action_icon + 'repeat' + end + + def action_path + retry_namespace_project_build_path(subject.project.namespace, subject.project, subject) + end + + def action_method + :post + end + + def self.matches?(build) + build.retryable? + end + end + end + end + end +end -- cgit v1.2.1 From a83a80edb368d9b5697493123c2f13d8b7c6531e Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 8 Dec 2016 10:52:44 +0100 Subject: Check permission of details --- lib/gitlab/ci/status/build/common.rb | 4 ++-- lib/gitlab/ci/status/core.rb | 2 +- lib/gitlab/ci/status/pipeline/common.rb | 4 ++-- lib/gitlab/ci/status/stage/common.rb | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/gitlab/ci/status/build/common.rb b/lib/gitlab/ci/status/build/common.rb index 3e47d7dfd20..2fb79afa3d3 100644 --- a/lib/gitlab/ci/status/build/common.rb +++ b/lib/gitlab/ci/status/build/common.rb @@ -3,8 +3,8 @@ module Gitlab module Status module Build module Common - def has_details? - true + def has_details?(current_user) + can?(current_user, :read_build, subject) end def details_path diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb index 60c559248aa..6b47096f811 100644 --- a/lib/gitlab/ci/status/core.rb +++ b/lib/gitlab/ci/status/core.rb @@ -33,7 +33,7 @@ module Gitlab self.class.name.demodulize.downcase.underscore end - def has_details? + def has_details?(_user = nil) false end diff --git a/lib/gitlab/ci/status/pipeline/common.rb b/lib/gitlab/ci/status/pipeline/common.rb index 25e52bec3da..5f79044a496 100644 --- a/lib/gitlab/ci/status/pipeline/common.rb +++ b/lib/gitlab/ci/status/pipeline/common.rb @@ -3,8 +3,8 @@ module Gitlab module Status module Pipeline module Common - def has_details? - true + def has_details?(current_user) + can?(current_user, :read_pipeline, subject) end def details_path diff --git a/lib/gitlab/ci/status/stage/common.rb b/lib/gitlab/ci/status/stage/common.rb index 14c437d2b98..e6ee2f92341 100644 --- a/lib/gitlab/ci/status/stage/common.rb +++ b/lib/gitlab/ci/status/stage/common.rb @@ -3,8 +3,8 @@ module Gitlab module Status module Stage module Common - def has_details? - true + def has_details?(current_user) + can?(current_user, :read_pipeline, subject) end def details_path -- cgit v1.2.1 From feaf01802c092be8f55994c910f2975376cbd20f Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 8 Dec 2016 11:03:01 +0100 Subject: Remove ci_status_with_icon helper and replace it with partial [ci skip] --- app/helpers/ci_status_helper.rb | 20 +------------------- app/views/admin/runners/show.html.haml | 2 +- app/views/ci/status/_icon_with_label.html.haml | 10 ++++++++++ app/views/projects/builds/_header.html.haml | 2 +- app/views/projects/ci/builds/_build.html.haml | 5 +---- app/views/projects/ci/pipelines/_pipeline.html.haml | 5 +---- .../_generic_commit_status.html.haml | 5 +---- app/views/projects/pipelines/_info.html.haml | 2 +- 8 files changed, 17 insertions(+), 34 deletions(-) create mode 100644 app/views/ci/status/_icon_with_label.html.haml diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 8e19752a8a1..d9f5e01f0dc 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -4,25 +4,7 @@ module CiStatusHelper builds_namespace_project_commit_path(project.namespace, project, pipeline.sha) end - def ci_status_with_icon(status, target = nil) - content = ci_icon_for_status(status) + ci_text_for_status(status) - klass = "ci-status ci-#{status}" - - if target - link_to content, target, class: klass - else - content_tag :span, content, class: klass - end - end - - def ci_text_for_status(status) - if detailed_status?(status) - status.text - else - status - end - end - + # Is used by Commit and Merge Request Widget def ci_label_for_status(status) if detailed_status?(status) return status.label diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 73038164056..fa8be25ffa8 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -91,7 +91,7 @@ %strong ##{build.id} %td.status - = ci_status_with_icon(build.status) + = render "ci/status/icon_with_label", subject: build %td.status - if project diff --git a/app/views/ci/status/_icon_with_label.html.haml b/app/views/ci/status/_icon_with_label.html.haml new file mode 100644 index 00000000000..65a74e88444 --- /dev/null +++ b/app/views/ci/status/_icon_with_label.html.haml @@ -0,0 +1,10 @@ +- details_path = subject.details_path if subject.has_details?(current_user) +- klass = "ci-status ci-#{subject.status}" +- if details_path + = link_to details_path, class: klass do + = custom_icon(status.icon) + = status.text +- else + %span{ class: klass } + = custom_icon(status.icon) + = status.text diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml index f6aa20c4579..5e4e30f08d5 100644 --- a/app/views/projects/builds/_header.html.haml +++ b/app/views/projects/builds/_header.html.haml @@ -1,6 +1,6 @@ .content-block.build-header .header-content - = ci_status_with_icon(@build.status) + = render "ci/status/icon_with_label", subject: build Build %strong ##{@build.id} in pipeline diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 18b3b04154f..6b0cd3e49a0 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -9,10 +9,7 @@ %tr.build.commit{class: ('retried' if retried)} %td.status - - if can?(current_user, :read_build, build) - = ci_status_with_icon(build.status, namespace_project_build_url(build.project.namespace, build.project, build)) - - else - = ci_status_with_icon(build.status) + = render "ci/status/icon_with_label", subject: build %td.branch-commit - if can?(current_user, :read_build, build) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index b58dceb58c9..84243e4306d 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -1,13 +1,10 @@ - status = pipeline.status -- detailed_status = pipeline.detailed_status - show_commit = local_assigns.fetch(:show_commit, true) - show_branch = local_assigns.fetch(:show_branch, true) %tr.commit %td.commit-link - = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "ci-status ci-#{detailed_status}" do - = ci_icon_for_status(detailed_status) - = ci_text_for_status(detailed_status) + = render "ci/status/icon_with_label", subject: pipeline %td = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index 7f751d9ae2e..69cb1631ee8 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -8,10 +8,7 @@ %tr.generic_commit_status{class: ('retried' if retried)} %td.status - - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url - = ci_status_with_icon(generic_commit_status.status, generic_commit_status.target_url) - - else - = ci_status_with_icon(generic_commit_status.status) + = render "ci/status/icon_with_label", subject: generic_commit_status %td.generic_commit_status-link - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 229bdfb0e8d..f7385184a2b 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -1,6 +1,6 @@ .page-content-header .header-main-content - = ci_status_with_icon(@pipeline.detailed_status) + = render "ci/status/icon_with_label", subject: @pipeline %strong Pipeline ##{@commit.pipelines.last.id} triggered #{time_ago_with_tooltip(@commit.authored_date)} by = author_avatar(@commit, size: 24) -- cgit v1.2.1 From e0ce97fb7d7d995fa76df57bfaac6d3601800190 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Thu, 8 Dec 2016 13:51:06 +0100 Subject: Refactor ci status factories to DRY code a little --- lib/gitlab/ci/status/build/factory.rb | 11 +++++------ lib/gitlab/ci/status/extended.rb | 2 +- lib/gitlab/ci/status/factory.rb | 30 +++++++++++++++++------------- lib/gitlab/ci/status/pipeline/factory.rb | 8 +++----- lib/gitlab/ci/status/stage/factory.rb | 6 ++---- 5 files changed, 28 insertions(+), 29 deletions(-) diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb index 8f420a93954..eee9a64120b 100644 --- a/lib/gitlab/ci/status/build/factory.rb +++ b/lib/gitlab/ci/status/build/factory.rb @@ -3,14 +3,13 @@ module Gitlab module Status module Build class Factory < Status::Factory - private - - def extended_statuses - [Stop, Play, Cancelable, Retryable] + def self.extended_statuses + [Status::Build::Stop, Status::Build::Play, + Status::Build::Cancelable, Status::Build::Retryable] end - def core_status - super.extend(Status::Build::Common) + def self.common_helpers + Status::Build::Common end end end diff --git a/lib/gitlab/ci/status/extended.rb b/lib/gitlab/ci/status/extended.rb index 6bfb5d38c1f..93e6eff1c94 100644 --- a/lib/gitlab/ci/status/extended.rb +++ b/lib/gitlab/ci/status/extended.rb @@ -2,7 +2,7 @@ module Gitlab module Ci module Status module Extended - def matches?(_subject) + def matches?(_subject, _user) raise NotImplementedError end end diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb index b2f896f2211..944e0fdde2d 100644 --- a/lib/gitlab/ci/status/factory.rb +++ b/lib/gitlab/ci/status/factory.rb @@ -2,10 +2,9 @@ module Gitlab module Ci module Status class Factory - attr_reader :subject - - def initialize(subject) + def initialize(subject, user = nil) @subject = subject + @user = user end def fabricate! @@ -16,27 +15,32 @@ module Gitlab end end + def self.extended_statuses + [] + end + + def self.common_helpers + Module.new + end + private - def subject_status - @subject_status ||= subject.status + def simple_status + @simple_status ||= @subject.status || :created end def core_status Gitlab::Ci::Status - .const_get(subject_status.capitalize) - .new(subject) + .const_get(simple_status.capitalize) + .new(@subject) + .extend(self.class.common_helpers) end def extended_status - @extended ||= extended_statuses.find do |status| - status.matches?(subject) + @extended ||= self.class.extended_statuses.find do |status| + status.matches?(@subject, @user) end end - - def extended_statuses - [] - end end end end diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb index 4ac4ec671d0..16dcb326be9 100644 --- a/lib/gitlab/ci/status/pipeline/factory.rb +++ b/lib/gitlab/ci/status/pipeline/factory.rb @@ -3,14 +3,12 @@ module Gitlab module Status module Pipeline class Factory < Status::Factory - private - - def extended_statuses + def self.extended_statuses [Pipeline::SuccessWithWarnings] end - def core_status - super.extend(Status::Pipeline::Common) + def self.common_helpers + Status::Pipeline::Common end end end diff --git a/lib/gitlab/ci/status/stage/factory.rb b/lib/gitlab/ci/status/stage/factory.rb index c6522d5ada1..689a5dd45bc 100644 --- a/lib/gitlab/ci/status/stage/factory.rb +++ b/lib/gitlab/ci/status/stage/factory.rb @@ -3,10 +3,8 @@ module Gitlab module Status module Stage class Factory < Status::Factory - private - - def core_status - super.extend(Status::Stage::Common) + def self.common_helpers + Status::Stage::Common end end end -- cgit v1.2.1 From 5059d0b834eeea22ada4b6ac98cfddc2123691e9 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Thu, 8 Dec 2016 14:28:49 +0100 Subject: Incorporate permission checks into new CI statuses [ci skip] --- lib/gitlab/ci/status/build/cancelable.rb | 12 +++++++----- lib/gitlab/ci/status/build/common.rb | 10 +++++----- lib/gitlab/ci/status/build/play.rb | 12 +++++++----- lib/gitlab/ci/status/build/retryable.rb | 12 +++++++----- lib/gitlab/ci/status/build/stop.rb | 12 +++++++----- lib/gitlab/ci/status/core.rb | 13 ++++++------- lib/gitlab/ci/status/extended.rb | 8 ++++++-- lib/gitlab/ci/status/factory.rb | 4 ++-- lib/gitlab/ci/status/pipeline/common.rb | 10 +++++----- lib/gitlab/ci/status/pipeline/success_with_warnings.rb | 4 ++-- lib/gitlab/ci/status/stage/common.rb | 12 ++++++------ 11 files changed, 60 insertions(+), 49 deletions(-) diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb index bff0464ef0c..a8830b04715 100644 --- a/lib/gitlab/ci/status/build/cancelable.rb +++ b/lib/gitlab/ci/status/build/cancelable.rb @@ -3,10 +3,10 @@ module Gitlab module Status module Status class Cancelable < SimpleDelegator - extend Status::Extended + include Status::Extended - def has_action?(current_user) - can?(current_user, :update_build, subject) + def has_action? + can?(user, :update_build, subject) end def action_icon @@ -14,14 +14,16 @@ module Gitlab end def action_path - cancel_namespace_project_build_path(subject.project.namespace, subject.project, subject) + cancel_namespace_project_build_path(subject.project.namespace, + subject.project, + subject) end def action_method :post end - def self.matches?(build) + def self.matches?(build, user) build.cancelable? end end diff --git a/lib/gitlab/ci/status/build/common.rb b/lib/gitlab/ci/status/build/common.rb index 2fb79afa3d3..2b602f1e247 100644 --- a/lib/gitlab/ci/status/build/common.rb +++ b/lib/gitlab/ci/status/build/common.rb @@ -3,14 +3,14 @@ module Gitlab module Status module Build module Common - def has_details?(current_user) - can?(current_user, :read_build, subject) + def has_details? + can?(user, :read_build, subject) end def details_path - namespace_project_build_path(@subject.project.namespace, - @subject.project, - @subject.pipeline) + namespace_project_build_path(subject.project.namespace, + subject.project, + subject.pipeline) end end end diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb index d295850137b..70c08197ea1 100644 --- a/lib/gitlab/ci/status/build/play.rb +++ b/lib/gitlab/ci/status/build/play.rb @@ -3,7 +3,7 @@ module Gitlab module Status module Status class Play < SimpleDelegator - extend Status::Extended + include Status::Extended def text 'play' @@ -13,8 +13,8 @@ module Gitlab 'play' end - def has_action?(current_user) - can?(current_user, :update_build, subject) + def has_action? + can?(user, :update_build, subject) end def action_icon @@ -22,14 +22,16 @@ module Gitlab end def action_path - play_namespace_project_build_path(subject.project.namespace, subject.project, subject) + play_namespace_project_build_path(subject.project.namespace, + subject.project, + subject) end def action_method :post end - def self.matches?(build) + def self.matches?(build, user) build.playable? && !build.stops_environment? end end diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb index b3c0eedadf8..3309e8808e1 100644 --- a/lib/gitlab/ci/status/build/retryable.rb +++ b/lib/gitlab/ci/status/build/retryable.rb @@ -3,10 +3,10 @@ module Gitlab module Status module Status class Retryable < SimpleDelegator - extend Status::Extended + include Status::Extended - def has_action?(current_user) - can?(current_user, :update_build, subject) + def has_action? + can?(user, :update_build, subject) end def action_icon @@ -14,14 +14,16 @@ module Gitlab end def action_path - retry_namespace_project_build_path(subject.project.namespace, subject.project, subject) + retry_namespace_project_build_path(subject.project.namespace, + subject.project, + subject) end def action_method :post end - def self.matches?(build) + def self.matches?(build, user) build.retryable? end end diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb index 261de9695c5..6fb51890bec 100644 --- a/lib/gitlab/ci/status/build/stop.rb +++ b/lib/gitlab/ci/status/build/stop.rb @@ -3,7 +3,7 @@ module Gitlab module Status module Status class Play < SimpleDelegator - extend Status::Extended + include Status::Extended def text 'stop' @@ -21,8 +21,8 @@ module Gitlab 'stop' end - def has_action?(current_user) - can?(current_user, :update_build, subject) + def has_action? + can?(user, :update_build, subject) end def action_icon @@ -30,14 +30,16 @@ module Gitlab end def action_path - play_namespace_project_build_path(subject.project.namespace, subject.project, subject) + play_namespace_project_build_path(subject.project.namespace, + subject.project, + subject) end def action_method :post end - def self.matches?(build) + def self.matches?(build, user) build.playable? && build.stops_environment? end end diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb index 6b47096f811..df371363736 100644 --- a/lib/gitlab/ci/status/core.rb +++ b/lib/gitlab/ci/status/core.rb @@ -6,8 +6,11 @@ module Gitlab class Core include Gitlab::Routing.url_helpers - def initialize(subject) + attr_reader :subject, :user + + def initialize(subject, user) @subject = subject + @user = user end def icon @@ -18,10 +21,6 @@ module Gitlab raise NotImplementedError end - def title - "#{@subject.class.name.demodulize}: #{label}" - end - # Deprecation warning: this method is here because we need to maintain # backwards compatibility with legacy statuses. We often do something # like "ci-status ci-status-#{status}" to set CSS class. @@ -33,7 +32,7 @@ module Gitlab self.class.name.demodulize.downcase.underscore end - def has_details?(_user = nil) + def has_details? false end @@ -41,7 +40,7 @@ module Gitlab raise NotImplementedError end - def has_action?(_user = nil) + def has_action? false end diff --git a/lib/gitlab/ci/status/extended.rb b/lib/gitlab/ci/status/extended.rb index 93e6eff1c94..d367c9bda69 100644 --- a/lib/gitlab/ci/status/extended.rb +++ b/lib/gitlab/ci/status/extended.rb @@ -2,8 +2,12 @@ module Gitlab module Ci module Status module Extended - def matches?(_subject, _user) - raise NotImplementedError + extend ActiveSupport::Concern + + class_methods do + def matches?(_subject, _user) + raise NotImplementedError + end end end end diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb index 944e0fdde2d..ae9ef895df4 100644 --- a/lib/gitlab/ci/status/factory.rb +++ b/lib/gitlab/ci/status/factory.rb @@ -2,7 +2,7 @@ module Gitlab module Ci module Status class Factory - def initialize(subject, user = nil) + def initialize(subject, user) @subject = subject @user = user end @@ -32,7 +32,7 @@ module Gitlab def core_status Gitlab::Ci::Status .const_get(simple_status.capitalize) - .new(@subject) + .new(@subject, @user) .extend(self.class.common_helpers) end diff --git a/lib/gitlab/ci/status/pipeline/common.rb b/lib/gitlab/ci/status/pipeline/common.rb index 5f79044a496..76bfd18bf40 100644 --- a/lib/gitlab/ci/status/pipeline/common.rb +++ b/lib/gitlab/ci/status/pipeline/common.rb @@ -3,14 +3,14 @@ module Gitlab module Status module Pipeline module Common - def has_details?(current_user) - can?(current_user, :read_pipeline, subject) + def has_details? + can?(user, :read_pipeline, subject) end def details_path - namespace_project_pipeline_path(@subject.project.namespace, - @subject.project, - @subject) + namespace_project_pipeline_path(subject.project.namespace, + subject.project, + subject) end def has_action? diff --git a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb index 4b040d60df8..a7c98f9e909 100644 --- a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb +++ b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb @@ -3,7 +3,7 @@ module Gitlab module Status module Pipeline class SuccessWithWarnings < SimpleDelegator - extend Status::Extended + include Status::Extended def text 'passed' @@ -21,7 +21,7 @@ module Gitlab 'success_with_warnings' end - def self.matches?(pipeline) + def self.matches?(pipeline, user) pipeline.success? && pipeline.has_warnings? end end diff --git a/lib/gitlab/ci/status/stage/common.rb b/lib/gitlab/ci/status/stage/common.rb index e6ee2f92341..6851ceda317 100644 --- a/lib/gitlab/ci/status/stage/common.rb +++ b/lib/gitlab/ci/status/stage/common.rb @@ -3,15 +3,15 @@ module Gitlab module Status module Stage module Common - def has_details?(current_user) - can?(current_user, :read_pipeline, subject) + def has_details? + can?(user, :read_pipeline, subject) end def details_path - namespace_project_pipeline_path(@subject.project.namespace, - @subject.project, - @subject.pipeline, - anchor: @subject.name) + namespace_project_pipeline_path(subject.project.namespace, + subject.project, + subject.pipeline, + anchor: subject.name) end def has_action? -- cgit v1.2.1 From 23feb6a773a49123c3ece0ff2ed675fd294d8817 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Thu, 8 Dec 2016 14:40:21 +0100 Subject: Fix tests related to detailed statuses and permissions [ci skip] --- spec/lib/gitlab/ci/status/canceled_spec.rb | 4 +++- spec/lib/gitlab/ci/status/created_spec.rb | 8 +++----- spec/lib/gitlab/ci/status/extended_spec.rb | 2 +- spec/lib/gitlab/ci/status/factory_spec.rb | 6 ++++-- spec/lib/gitlab/ci/status/failed_spec.rb | 8 +++----- spec/lib/gitlab/ci/status/pending_spec.rb | 8 +++----- spec/lib/gitlab/ci/status/pipeline/common_spec.rb | 4 +++- spec/lib/gitlab/ci/status/pipeline/factory_spec.rb | 4 +++- spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb | 2 +- spec/lib/gitlab/ci/status/running_spec.rb | 8 +++----- spec/lib/gitlab/ci/status/skipped_spec.rb | 8 +++----- spec/lib/gitlab/ci/status/stage/common_spec.rb | 8 ++++++-- spec/lib/gitlab/ci/status/stage/factory_spec.rb | 8 ++++++-- spec/lib/gitlab/ci/status/success_spec.rb | 8 +++----- 14 files changed, 45 insertions(+), 41 deletions(-) diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb index 619ecbcba67..eaf974bb953 100644 --- a/spec/lib/gitlab/ci/status/canceled_spec.rb +++ b/spec/lib/gitlab/ci/status/canceled_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Canceled do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'canceled' } diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb index 157302c65a8..2ce176a29d6 100644 --- a/spec/lib/gitlab/ci/status/created_spec.rb +++ b/spec/lib/gitlab/ci/status/created_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Created do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'created' } @@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Created do describe '#icon' do it { expect(subject.icon).to eq 'icon_status_created' } end - - describe '#title' do - it { expect(subject.title).to eq 'Double: created' } - end end diff --git a/spec/lib/gitlab/ci/status/extended_spec.rb b/spec/lib/gitlab/ci/status/extended_spec.rb index 120e121aae5..864121dec4b 100644 --- a/spec/lib/gitlab/ci/status/extended_spec.rb +++ b/spec/lib/gitlab/ci/status/extended_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Extended do end it 'requires subclass to implement matcher' do - expect { subject.matches?(double) } + expect { subject.matches?(double, double) } .to raise_error(NotImplementedError) end end diff --git a/spec/lib/gitlab/ci/status/factory_spec.rb b/spec/lib/gitlab/ci/status/factory_spec.rb index d5bd7f7102b..f92a1c149bf 100644 --- a/spec/lib/gitlab/ci/status/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/factory_spec.rb @@ -2,15 +2,17 @@ require 'spec_helper' describe Gitlab::Ci::Status::Factory do subject do - described_class.new(object) + described_class.new(resource, user) end + let(:user) { create(:user) } + let(:status) { subject.fabricate! } context 'when object has a core status' do HasStatus::AVAILABLE_STATUSES.each do |core_status| context "when core status is #{core_status}" do - let(:object) { double(status: core_status) } + let(:resource) { double(status: core_status) } it "fabricates a core status #{core_status}" do expect(status).to be_a( diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb index 0b3cb8168e6..9d527e6a7ef 100644 --- a/spec/lib/gitlab/ci/status/failed_spec.rb +++ b/spec/lib/gitlab/ci/status/failed_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Failed do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'failed' } @@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Failed do describe '#icon' do it { expect(subject.icon).to eq 'icon_status_failed' } end - - describe '#title' do - it { expect(subject.title).to eq 'Double: failed' } - end end diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb index 57c901c1202..d03f595d3c7 100644 --- a/spec/lib/gitlab/ci/status/pending_spec.rb +++ b/spec/lib/gitlab/ci/status/pending_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Pending do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'pending' } @@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Pending do describe '#icon' do it { expect(subject.icon).to eq 'icon_status_pending' } end - - describe '#title' do - it { expect(subject.title).to eq 'Double: pending' } - end end diff --git a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb index 21adee3f8e7..4f32ae5d809 100644 --- a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb @@ -1,11 +1,13 @@ require 'spec_helper' describe Gitlab::Ci::Status::Pipeline::Common do + let(:user) { create(:user) } let(:pipeline) { create(:ci_pipeline) } subject do Class.new(Gitlab::Ci::Status::Core) - .new(pipeline).extend(described_class) + .new(pipeline, user) + .extend(described_class) end it 'does not have action' do diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb index d6243940f2e..c6b2582652d 100644 --- a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb @@ -1,8 +1,10 @@ require 'spec_helper' describe Gitlab::Ci::Status::Pipeline::Factory do + let(:user) { create(:user) } + subject do - described_class.new(pipeline) + described_class.new(pipeline, user) end let(:status) do diff --git a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb index 02e526e3de2..634f80088d5 100644 --- a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do subject do - described_class.new(double('status')) + described_class.new(double('status'), double('user')) end describe '#test' do diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb index c023f1872cc..9f47090d396 100644 --- a/spec/lib/gitlab/ci/status/running_spec.rb +++ b/spec/lib/gitlab/ci/status/running_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Running do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'running' } @@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Running do describe '#icon' do it { expect(subject.icon).to eq 'icon_status_running' } end - - describe '#title' do - it { expect(subject.title).to eq 'Double: running' } - end end diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb index d4f7f4b3b70..94601648a8d 100644 --- a/spec/lib/gitlab/ci/status/skipped_spec.rb +++ b/spec/lib/gitlab/ci/status/skipped_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Skipped do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'skipped' } @@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Skipped do describe '#icon' do it { expect(subject.icon).to eq 'icon_status_skipped' } end - - describe '#title' do - it { expect(subject.title).to eq 'Double: skipped' } - end end diff --git a/spec/lib/gitlab/ci/status/stage/common_spec.rb b/spec/lib/gitlab/ci/status/stage/common_spec.rb index f3259c6f23e..9b7e6777dc1 100644 --- a/spec/lib/gitlab/ci/status/stage/common_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/common_spec.rb @@ -1,12 +1,16 @@ require 'spec_helper' describe Gitlab::Ci::Status::Stage::Common do + let(:user) { create(:user) } let(:pipeline) { create(:ci_empty_pipeline) } - let(:stage) { build(:ci_stage, pipeline: pipeline, name: 'test') } + + let(:stage) do + build(:ci_stage, pipeline: pipeline, name: 'test') + end subject do Class.new(Gitlab::Ci::Status::Core) - .new(stage).extend(described_class) + .new(stage, user).extend(described_class) end it 'does not have action' do diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb index 17929665c83..5a281564415 100644 --- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb @@ -1,11 +1,15 @@ require 'spec_helper' describe Gitlab::Ci::Status::Stage::Factory do + let(:user) { create(:user) } let(:pipeline) { create(:ci_empty_pipeline) } - let(:stage) { build(:ci_stage, pipeline: pipeline, name: 'test') } + + let(:stage) do + build(:ci_stage, pipeline: pipeline, name: 'test') + end subject do - described_class.new(stage) + described_class.new(stage, user) end let(:status) do diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb index 9e261a3aa5f..90f9f615e0d 100644 --- a/spec/lib/gitlab/ci/status/success_spec.rb +++ b/spec/lib/gitlab/ci/status/success_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Success do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'passed' } @@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Success do describe '#icon' do it { expect(subject.icon).to eq 'icon_status_success' } end - - describe '#title' do - it { expect(subject.title).to eq 'Double: passed' } - end end -- cgit v1.2.1 From f0cd73bfadbe9fa27b25473dab61d8c566292392 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Thu, 8 Dec 2016 14:51:38 +0100 Subject: Fix some detailed statuses specs related to abilities --- app/models/ability.rb | 6 ++++++ lib/gitlab/ci/status/core.rb | 1 + lib/gitlab/ci/status/stage/common.rb | 2 +- spec/lib/gitlab/ci/status/canceled_spec.rb | 4 ---- spec/lib/gitlab/ci/status/extended_spec.rb | 2 +- spec/lib/gitlab/ci/status/pipeline/common_spec.rb | 7 ++++++- spec/lib/gitlab/ci/status/pipeline/factory_spec.rb | 5 +++++ .../status/pipeline/success_with_warnings_spec.rb | 10 +++++----- spec/lib/gitlab/ci/status/stage/common_spec.rb | 23 +++++++++++++++++----- spec/lib/gitlab/ci/status/stage/factory_spec.rb | 7 ++++++- 10 files changed, 49 insertions(+), 18 deletions(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index fa8f8bc3a5f..ce461caf686 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -1,4 +1,10 @@ class Ability + module Allowable + def can?(user, action, subject) + Ability.allowed?(user, action, subject) + end + end + class << self # Given a list of users and a project this method returns the users that can # read the given project. diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb index df371363736..7e9f6e35012 100644 --- a/lib/gitlab/ci/status/core.rb +++ b/lib/gitlab/ci/status/core.rb @@ -5,6 +5,7 @@ module Gitlab # class Core include Gitlab::Routing.url_helpers + include Ability::Allowable attr_reader :subject, :user diff --git a/lib/gitlab/ci/status/stage/common.rb b/lib/gitlab/ci/status/stage/common.rb index 6851ceda317..7852f492e1d 100644 --- a/lib/gitlab/ci/status/stage/common.rb +++ b/lib/gitlab/ci/status/stage/common.rb @@ -4,7 +4,7 @@ module Gitlab module Stage module Common def has_details? - can?(user, :read_pipeline, subject) + can?(user, :read_pipeline, subject.pipeline) end def details_path diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb index eaf974bb953..4639278ad45 100644 --- a/spec/lib/gitlab/ci/status/canceled_spec.rb +++ b/spec/lib/gitlab/ci/status/canceled_spec.rb @@ -16,8 +16,4 @@ describe Gitlab::Ci::Status::Canceled do describe '#icon' do it { expect(subject.icon).to eq 'icon_status_canceled' } end - - describe '#title' do - it { expect(subject.title).to eq 'Double: canceled' } - end end diff --git a/spec/lib/gitlab/ci/status/extended_spec.rb b/spec/lib/gitlab/ci/status/extended_spec.rb index 864121dec4b..c2d74ca5cde 100644 --- a/spec/lib/gitlab/ci/status/extended_spec.rb +++ b/spec/lib/gitlab/ci/status/extended_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::Ci::Status::Extended do subject do - Class.new.extend(described_class) + Class.new.include(described_class) end it 'requires subclass to implement matcher' do diff --git a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb index 4f32ae5d809..2df9d574677 100644 --- a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' describe Gitlab::Ci::Status::Pipeline::Common do let(:user) { create(:user) } - let(:pipeline) { create(:ci_pipeline) } + let(:project) { create(:empty_project) } + let(:pipeline) { create(:ci_pipeline, project: project) } subject do Class.new(Gitlab::Ci::Status::Core) @@ -10,6 +11,10 @@ describe Gitlab::Ci::Status::Pipeline::Common do .extend(described_class) end + before do + project.team << [user, :developer] + end + it 'does not have action' do expect(subject).not_to have_action end diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb index c6b2582652d..d4a2dc7fcc1 100644 --- a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe Gitlab::Ci::Status::Pipeline::Factory do let(:user) { create(:user) } + let(:project) { pipeline.project } subject do described_class.new(pipeline, user) @@ -11,6 +12,10 @@ describe Gitlab::Ci::Status::Pipeline::Factory do subject.fabricate! end + before do + project.team << [user, :developer] + end + context 'when pipeline has a core status' do HasStatus::AVAILABLE_STATUSES.each do |core_status| context "when core status is #{core_status}" do diff --git a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb index 634f80088d5..7e3383c307f 100644 --- a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do subject do - described_class.new(double('status'), double('user')) + described_class.new(double('status')) end describe '#test' do @@ -29,13 +29,13 @@ describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do end it 'is a correct match' do - expect(described_class.matches?(pipeline)).to eq true + expect(described_class.matches?(pipeline, double)).to eq true end end context 'when pipeline does not have warnings' do it 'does not match' do - expect(described_class.matches?(pipeline)).to eq false + expect(described_class.matches?(pipeline, double)).to eq false end end end @@ -51,13 +51,13 @@ describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do end it 'does not match' do - expect(described_class.matches?(pipeline)).to eq false + expect(described_class.matches?(pipeline, double)).to eq false end end context 'when pipeline does not have warnings' do it 'does not match' do - expect(described_class.matches?(pipeline)).to eq false + expect(described_class.matches?(pipeline, double)).to eq false end end end diff --git a/spec/lib/gitlab/ci/status/stage/common_spec.rb b/spec/lib/gitlab/ci/status/stage/common_spec.rb index 9b7e6777dc1..8814a7614a0 100644 --- a/spec/lib/gitlab/ci/status/stage/common_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/common_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' describe Gitlab::Ci::Status::Stage::Common do let(:user) { create(:user) } - let(:pipeline) { create(:ci_empty_pipeline) } + let(:project) { create(:empty_project) } + let(:pipeline) { create(:ci_empty_pipeline, project: project) } let(:stage) do build(:ci_stage, pipeline: pipeline, name: 'test') @@ -17,14 +18,26 @@ describe Gitlab::Ci::Status::Stage::Common do expect(subject).not_to have_action end - it 'has details' do - expect(subject).to have_details - end - it 'links to the pipeline details page' do expect(subject.details_path) .to include "pipelines/#{pipeline.id}" expect(subject.details_path) .to include "##{stage.name}" end + + context 'when user has permission to read pipeline' do + before do + project.team << [user, :master] + end + + it 'has details' do + expect(subject).to have_details + end + end + + context 'when user does not have permission to read pipeline' do + it 'does not have details' do + expect(subject).not_to have_details + end + end end diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb index 5a281564415..6f8721d30c2 100644 --- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' describe Gitlab::Ci::Status::Stage::Factory do let(:user) { create(:user) } - let(:pipeline) { create(:ci_empty_pipeline) } + let(:project) { create(:empty_project) } + let(:pipeline) { create(:ci_empty_pipeline, project: project) } let(:stage) do build(:ci_stage, pipeline: pipeline, name: 'test') @@ -16,6 +17,10 @@ describe Gitlab::Ci::Status::Stage::Factory do subject.fabricate! end + before do + project.team << [user, :developer] + end + context 'when stage has a core status' do HasStatus::AVAILABLE_STATUSES.each do |core_status| context "when core status is #{core_status}" do -- cgit v1.2.1 From 980009e6e85562c9ee8026878929d09905b2a0a9 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 8 Dec 2016 17:52:24 +0100 Subject: Fix auto loading of constants for Ci Statuses --- app/models/ci/build.rb | 6 +++--- app/models/ci/pipeline.rb | 4 ++-- app/models/ci/stage.rb | 4 ++-- app/models/commit_status.rb | 4 ++-- app/views/ci/status/_icon_with_label.html.haml | 13 +++++++------ lib/gitlab/ci/status/build/cancelable.rb | 2 +- lib/gitlab/ci/status/build/play.rb | 2 +- lib/gitlab/ci/status/build/retryable.rb | 2 +- lib/gitlab/ci/status/build/stop.rb | 4 ++-- 9 files changed, 21 insertions(+), 20 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 73564dd2aa0..65ee327a8e5 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -100,8 +100,8 @@ module Ci end end - def detailed_status - Gitlab::Ci::Status::Build::Factory.new(self).fabricate! + def detailed_status(current_user) + Gitlab::Ci::Status::Build::Factory.new(self, current_user).fabricate! end def manual? @@ -156,7 +156,7 @@ module Ci end def environment_action - self.options.fetch(:environment, {}).fetch(:action, 'start') + self.options.fetch(:environment, {}).fetch(:action, 'start') if self.options end def outdated_deployment? diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index fda8228a1e9..1f33106d358 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -336,8 +336,8 @@ module Ci .select { |merge_request| merge_request.head_pipeline.try(:id) == self.id } end - def detailed_status - Gitlab::Ci::Status::Pipeline::Factory.new(self).fabricate! + def detailed_status(current_user) + Gitlab::Ci::Status::Pipeline::Factory.new(self, current_user).fabricate! end private diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index d2a37c0a827..be52cce20f1 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -22,8 +22,8 @@ module Ci @status ||= statuses.latest.status end - def detailed_status - Gitlab::Ci::Status::Stage::Factory.new(self).fabricate! + def detailed_status(current_user) + Gitlab::Ci::Status::Stage::Factory.new(self, current_user).fabricate! end def statuses diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index fce16174e22..6548a7dda2c 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -132,7 +132,7 @@ class CommitStatus < ActiveRecord::Base false end - def detailed_status - Gitlab::Ci::Status::Factory.new(self).fabricate! + def detailed_status(current_user) + Gitlab::Ci::Status::Factory.new(self, current_user).fabricate! end end diff --git a/app/views/ci/status/_icon_with_label.html.haml b/app/views/ci/status/_icon_with_label.html.haml index 65a74e88444..d3fe332cc78 100644 --- a/app/views/ci/status/_icon_with_label.html.haml +++ b/app/views/ci/status/_icon_with_label.html.haml @@ -1,10 +1,11 @@ -- details_path = subject.details_path if subject.has_details?(current_user) -- klass = "ci-status ci-#{subject.status}" +- detailed_status = subject.detailed_status(current_user) +- details_path = detailed_status.details_path if detailed_status.has_details? +- klass = "ci-status ci-#{detailed_status}" - if details_path = link_to details_path, class: klass do - = custom_icon(status.icon) - = status.text + = custom_icon(detailed_status.icon) + = detailed_status.text - else %span{ class: klass } - = custom_icon(status.icon) - = status.text + = custom_icon(detailed_status.icon) + = detailed_status.text diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb index a8830b04715..88be0cd924b 100644 --- a/lib/gitlab/ci/status/build/cancelable.rb +++ b/lib/gitlab/ci/status/build/cancelable.rb @@ -1,7 +1,7 @@ module Gitlab module Ci module Status - module Status + module Build class Cancelable < SimpleDelegator include Status::Extended diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb index 70c08197ea1..57c7058fe84 100644 --- a/lib/gitlab/ci/status/build/play.rb +++ b/lib/gitlab/ci/status/build/play.rb @@ -1,7 +1,7 @@ module Gitlab module Ci module Status - module Status + module Build class Play < SimpleDelegator include Status::Extended diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb index 3309e8808e1..69f2ad1d277 100644 --- a/lib/gitlab/ci/status/build/retryable.rb +++ b/lib/gitlab/ci/status/build/retryable.rb @@ -1,7 +1,7 @@ module Gitlab module Ci module Status - module Status + module Build class Retryable < SimpleDelegator include Status::Extended diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb index 6fb51890bec..cd9bd959a7c 100644 --- a/lib/gitlab/ci/status/build/stop.rb +++ b/lib/gitlab/ci/status/build/stop.rb @@ -1,8 +1,8 @@ module Gitlab module Ci module Status - module Status - class Play < SimpleDelegator + module Build + class Stop < SimpleDelegator include Status::Extended def text -- cgit v1.2.1 From d1dd89356c4cd5e70f2b81ef416e52b745486293 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 8 Dec 2016 18:16:23 +0100 Subject: Rename icon_with_label to icon_with_description --- app/views/admin/runners/show.html.haml | 2 +- app/views/ci/status/_icon_with_description.html.haml | 12 ++++++++++++ app/views/ci/status/_icon_with_label.html.haml | 11 ----------- app/views/projects/builds/_header.html.haml | 2 +- app/views/projects/ci/builds/_build.html.haml | 2 +- app/views/projects/ci/pipelines/_pipeline.html.haml | 2 +- .../generic_commit_statuses/_generic_commit_status.html.haml | 2 +- app/views/projects/pipelines/_info.html.haml | 2 +- app/views/projects/stage/_graph.html.haml | 4 ++-- 9 files changed, 20 insertions(+), 19 deletions(-) create mode 100644 app/views/ci/status/_icon_with_description.html.haml delete mode 100644 app/views/ci/status/_icon_with_label.html.haml diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index fa8be25ffa8..badeb11b208 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -91,7 +91,7 @@ %strong ##{build.id} %td.status - = render "ci/status/icon_with_label", subject: build + = render "ci/status/icon_with_description", subject: build %td.status - if project diff --git a/app/views/ci/status/_icon_with_description.html.haml b/app/views/ci/status/_icon_with_description.html.haml new file mode 100644 index 00000000000..34c923440d0 --- /dev/null +++ b/app/views/ci/status/_icon_with_description.html.haml @@ -0,0 +1,12 @@ +- detailed_status = subject.detailed_status(current_user) +- details_path = detailed_status.details_path if detailed_status.has_details? +- klass = "ci-status ci-#{detailed_status}" + +- if details_path + = link_to details_path, class: klass do + = custom_icon(detailed_status.icon) + = detailed_status.text +- else + %span{ class: klass } + = custom_icon(detailed_status.icon) + = detailed_status.text diff --git a/app/views/ci/status/_icon_with_label.html.haml b/app/views/ci/status/_icon_with_label.html.haml deleted file mode 100644 index d3fe332cc78..00000000000 --- a/app/views/ci/status/_icon_with_label.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -- detailed_status = subject.detailed_status(current_user) -- details_path = detailed_status.details_path if detailed_status.has_details? -- klass = "ci-status ci-#{detailed_status}" -- if details_path - = link_to details_path, class: klass do - = custom_icon(detailed_status.icon) - = detailed_status.text -- else - %span{ class: klass } - = custom_icon(detailed_status.icon) - = detailed_status.text diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml index 5e4e30f08d5..85d1793ecb9 100644 --- a/app/views/projects/builds/_header.html.haml +++ b/app/views/projects/builds/_header.html.haml @@ -1,6 +1,6 @@ .content-block.build-header .header-content - = render "ci/status/icon_with_label", subject: build + = render "ci/status/icon_with_description", subject: build Build %strong ##{@build.id} in pipeline diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 6b0cd3e49a0..4257bb86859 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -9,7 +9,7 @@ %tr.build.commit{class: ('retried' if retried)} %td.status - = render "ci/status/icon_with_label", subject: build + = render "ci/status/icon_with_description", subject: build %td.branch-commit - if can?(current_user, :read_build, build) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 84243e4306d..6dff955ea3d 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -4,7 +4,7 @@ %tr.commit %td.commit-link - = render "ci/status/icon_with_label", subject: pipeline + = render "ci/status/icon_with_description", subject: pipeline %td = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index 69cb1631ee8..1dd07ae1a2a 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -8,7 +8,7 @@ %tr.generic_commit_status{class: ('retried' if retried)} %td.status - = render "ci/status/icon_with_label", subject: generic_commit_status + = render "ci/status/icon_with_description", subject: generic_commit_status %td.generic_commit_status-link - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index f7385184a2b..d05697b4ee3 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -1,6 +1,6 @@ .page-content-header .header-main-content - = render "ci/status/icon_with_label", subject: @pipeline + = render "ci/status/icon_with_description", subject: @pipeline %strong Pipeline ##{@commit.pipelines.last.id} triggered #{time_ago_with_tooltip(@commit.authored_date)} by = author_avatar(@commit, size: 24) diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml index 1d8fa10db0c..745b6d143f4 100644 --- a/app/views/projects/stage/_graph.html.haml +++ b/app/views/projects/stage/_graph.html.haml @@ -14,9 +14,9 @@ %li.build{ class: ("playable" if is_playable) } .curve .build-content - = render "projects/#{status.to_partial_path}_pipeline", subject: status + = render 'ci/status/icon_with_name_and_action', subject: status - else %li.build .curve .dropdown.inline.build-content - = render "projects/stage/in_stage_group", name: group_name, subject: grouped_statuses + = render 'projects/stage/in_stage_group', name: group_name, subject: grouped_statuses -- cgit v1.2.1 From d9a979c84f2d91b25ccde9131f9b4b03b40c4ef7 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 8 Dec 2016 18:18:30 +0100 Subject: Add action_class/action_title --- lib/gitlab/ci/status/build/cancelable.rb | 6 +++++- lib/gitlab/ci/status/build/play.rb | 8 ++++++++ lib/gitlab/ci/status/build/retryable.rb | 6 +++++- lib/gitlab/ci/status/build/stop.rb | 6 +++++- lib/gitlab/ci/status/core.rb | 7 +++++++ 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb index 88be0cd924b..a979fe7d573 100644 --- a/lib/gitlab/ci/status/build/cancelable.rb +++ b/lib/gitlab/ci/status/build/cancelable.rb @@ -10,7 +10,7 @@ module Gitlab end def action_icon - 'remove' + 'ban' end def action_path @@ -23,6 +23,10 @@ module Gitlab :post end + def action_title + 'Cancel' + end + def self.matches?(build, user) build.cancelable? end diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb index 57c7058fe84..e3066d40a37 100644 --- a/lib/gitlab/ci/status/build/play.rb +++ b/lib/gitlab/ci/status/build/play.rb @@ -17,10 +17,18 @@ module Gitlab can?(user, :update_build, subject) end + def action_title + 'Play' + end + def action_icon 'play' end + def action_class + 'ci-play-icon' + end + def action_path play_namespace_project_build_path(subject.project.namespace, subject.project, diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb index 69f2ad1d277..8e38d6a8523 100644 --- a/lib/gitlab/ci/status/build/retryable.rb +++ b/lib/gitlab/ci/status/build/retryable.rb @@ -10,7 +10,11 @@ module Gitlab end def action_icon - 'repeat' + 'refresh' + end + + def action_title + 'Retry' end def action_path diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb index cd9bd959a7c..487fd033960 100644 --- a/lib/gitlab/ci/status/build/stop.rb +++ b/lib/gitlab/ci/status/build/stop.rb @@ -26,7 +26,11 @@ module Gitlab end def action_icon - :play + 'stop' + end + + def action_title + 'Stop' end def action_path diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb index 7e9f6e35012..dd3a824e486 100644 --- a/lib/gitlab/ci/status/core.rb +++ b/lib/gitlab/ci/status/core.rb @@ -49,6 +49,9 @@ module Gitlab raise NotImplementedError end + def action_class + end + def action_path raise NotImplementedError end @@ -56,6 +59,10 @@ module Gitlab def action_method raise NotImplementedError end + + def action_title + raise NotImplementedError + end end end end -- cgit v1.2.1 From 23f02681c036b150966ce4459410c94694167b34 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 8 Dec 2016 18:21:11 +0100 Subject: Revert some unneeded changes --- app/views/projects/stage/_graph.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml index 745b6d143f4..bf8c75b6e5c 100644 --- a/app/views/projects/stage/_graph.html.haml +++ b/app/views/projects/stage/_graph.html.haml @@ -14,7 +14,7 @@ %li.build{ class: ("playable" if is_playable) } .curve .build-content - = render 'ci/status/icon_with_name_and_action', subject: status + = render "projects/#{status.to_partial_path}_pipeline", subject: status - else %li.build .curve -- cgit v1.2.1 From a2a16503a8ecfe1b38f6ef8e31c96b53537dfe02 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Mon, 12 Dec 2016 12:12:37 +0000 Subject: Fix vertical alignment of action icon with status icon --- app/assets/stylesheets/pages/pipelines.scss | 42 +++++++++++++---------------- app/views/projects/stage/_graph.html.haml | 3 +-- 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 304a7932a06..75b127eb7f6 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -654,6 +654,7 @@ i { font-size: 11px; + margin-top: 0; } } @@ -672,33 +673,26 @@ } // Action Icons -.ci-action-icon-container { - padding: 0; +.ci-action-icon-container .ci-action-icon-wrapper { + float: right; + margin-top: -1px; - .ci-action-icon-wrapper { - display: inline-block; - float: right; - - i { - color: $stage-badge-text; - border-radius: 100%; - border: 1px solid $stage-badge-text; - text-align: center; - display: table-cell; - vertical-align: middle; - padding: 5px; - font-size: 13px; - background: $white-light; + i { + color: $stage-badge-text; + border-radius: 100%; + border: 1px solid $stage-badge-text; + padding: 5px 6px; + font-size: 13px; + background: $white-light; - &:hover { - color: $gl-text-color; - background-color: $stage-hover-bg; - border: 1px solid $stage-hover-bg; - } + &:hover { + color: $gl-text-color; + background-color: $stage-hover-bg; + border: 1px solid $stage-hover-bg; } + } - .ci-play-icon { - padding: 5px 4px 5px 7px; - } + .ci-play-icon { + padding: 5px 5px 5px 7px; } } diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml index 255091cbfe8..cf3050eea63 100644 --- a/app/views/projects/stage/_graph.html.haml +++ b/app/views/projects/stage/_graph.html.haml @@ -10,8 +10,7 @@ - status_groups.each do |group_name, grouped_statuses| - if grouped_statuses.one? - status = grouped_statuses.first - - is_playable = status.playable? && can?(current_user, :update_build, @project) - %li.build{ class: ("playable" if is_playable) } + %li.build .curve .build-content = render 'ci/status/graph_icon_with_name_and_action', subject: status -- cgit v1.2.1 From 161d7c02d4ff49d6d91b982faee61b5de00d2631 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Wed, 7 Dec 2016 19:15:26 +0200 Subject: Modify namespace name and path validation Currently namespace name and path have uniq validaiton which does not allow us to use same group name/path inside different groups. This commit changes validation in next way: * Allow same namespace name with different parent_id * Allow same namespace path. Uniq validation should be handled by routes table Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- app/models/namespace.rb | 3 +- ...153749_remove_uniq_path_index_from_namespace.rb | 36 ++++++++++++++++++++++ .../20161206153751_add_path_index_to_namespace.rb | 20 ++++++++++++ ...153753_remove_uniq_name_index_from_namespace.rb | 36 ++++++++++++++++++++++ .../20161206153754_add_name_index_to_namespace.rb | 20 ++++++++++++ db/schema.rb | 6 ++-- spec/models/group_spec.rb | 3 +- spec/models/namespace_spec.rb | 6 +--- 8 files changed, 118 insertions(+), 12 deletions(-) create mode 100644 db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb create mode 100644 db/migrate/20161206153751_add_path_index_to_namespace.rb create mode 100644 db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb create mode 100644 db/migrate/20161206153754_add_name_index_to_namespace.rb diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 37374044551..f0479d94986 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -17,14 +17,13 @@ class Namespace < ActiveRecord::Base validates :owner, presence: true, unless: ->(n) { n.type == "Group" } validates :name, presence: true, - uniqueness: true, + uniqueness: { scope: :parent_id }, length: { maximum: 255 }, namespace_name: true validates :description, length: { maximum: 255 } validates :path, presence: true, - uniqueness: { case_sensitive: false }, length: { maximum: 255 }, namespace: true diff --git a/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb b/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb new file mode 100644 index 00000000000..2977917f2d1 --- /dev/null +++ b/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb @@ -0,0 +1,36 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveUniqPathIndexFromNamespace < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = false + + def up + constraint_name = 'namespaces_path_key' + + transaction do + if index_exists?(:namespaces, :path) + remove_index(:namespaces, :path) + end + + # In some bizarre cases PostgreSQL might have a separate unique constraint + # that we'll need to drop. + if constraint_exists?(constraint_name) && Gitlab::Database.postgresql? + execute("ALTER TABLE namespaces DROP CONSTRAINT IF EXISTS #{constraint_name};") + end + end + end + + def down + unless index_exists?(:namespaces, :path) + add_concurrent_index(:namespaces, :path, unique: true) + end + end + + def constraint_exists?(name) + indexes(:namespaces).map(&:name).include?(name) + end +end diff --git a/db/migrate/20161206153751_add_path_index_to_namespace.rb b/db/migrate/20161206153751_add_path_index_to_namespace.rb new file mode 100644 index 00000000000..b0bac7d121e --- /dev/null +++ b/db/migrate/20161206153751_add_path_index_to_namespace.rb @@ -0,0 +1,20 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddPathIndexToNamespace < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = false + + def up + add_concurrent_index :namespaces, :path + end + + def down + if index_exists?(:namespaces, :path) + remove_index :namespaces, :path + end + end +end diff --git a/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb b/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb new file mode 100644 index 00000000000..cc9d4974baa --- /dev/null +++ b/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb @@ -0,0 +1,36 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveUniqNameIndexFromNamespace < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = false + + def up + constraint_name = 'namespaces_name_key' + + transaction do + if index_exists?(:namespaces, :name) + remove_index(:namespaces, :name) + end + + # In some bizarre cases PostgreSQL might have a separate unique constraint + # that we'll need to drop. + if constraint_exists?(constraint_name) && Gitlab::Database.postgresql? + execute("ALTER TABLE namespaces DROP CONSTRAINT IF EXISTS #{constraint_name};") + end + end + end + + def down + unless index_exists?(:namespaces, :name) + add_concurrent_index(:namespaces, :name, unique: true) + end + end + + def constraint_exists?(name) + indexes(:namespaces).map(&:name).include?(name) + end +end diff --git a/db/migrate/20161206153754_add_name_index_to_namespace.rb b/db/migrate/20161206153754_add_name_index_to_namespace.rb new file mode 100644 index 00000000000..aaa35ed6f0a --- /dev/null +++ b/db/migrate/20161206153754_add_name_index_to_namespace.rb @@ -0,0 +1,20 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddNameIndexToNamespace < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = false + + def up + add_concurrent_index(:namespaces, [:name, :parent_id], unique: true) + end + + def down + if index_exists?(:namespaces, :name) + remove_index :namespaces, [:name, :parent_id] + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 9c46f573719..08b1590e484 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: 20161202152035) do +ActiveRecord::Schema.define(version: 20161206153754) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -744,11 +744,11 @@ ActiveRecord::Schema.define(version: 20161202152035) do add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree add_index "namespaces", ["deleted_at"], name: "index_namespaces_on_deleted_at", using: :btree - add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree + add_index "namespaces", ["name", "parent_id"], name: "index_namespaces_on_name_and_parent_id", unique: true, using: :btree add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"} add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree add_index "namespaces", ["parent_id", "id"], name: "index_namespaces_on_parent_id_and_id", unique: true, using: :btree - add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree + add_index "namespaces", ["path"], name: "index_namespaces_on_path", using: :btree add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"} add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 1613a586a2c..40f1cf92e00 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -50,9 +50,8 @@ describe Group, models: true do describe 'validations' do it { is_expected.to validate_presence_of :name } - it { is_expected.to validate_uniqueness_of(:name) } + it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) } it { is_expected.to validate_presence_of :path } - it { is_expected.to validate_uniqueness_of(:path) } it { is_expected.not_to validate_presence_of :owner } end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 7f82e85563b..069c59fb5ca 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -6,20 +6,16 @@ describe Namespace, models: true do it { is_expected.to have_many :projects } it { is_expected.to validate_presence_of(:name) } - it { is_expected.to validate_uniqueness_of(:name) } + it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) } it { is_expected.to validate_length_of(:name).is_at_most(255) } it { is_expected.to validate_length_of(:description).is_at_most(255) } it { is_expected.to validate_presence_of(:path) } - it { is_expected.to validate_uniqueness_of(:path) } it { is_expected.to validate_length_of(:path).is_at_most(255) } it { is_expected.to validate_presence_of(:owner) } - describe "Mass assignment" do - end - describe "Respond to" do it { is_expected.to respond_to(:human_name) } it { is_expected.to respond_to(:to_param) } -- cgit v1.2.1 From dc67554c08ac6cfe23809af9f086336ca9ca740e Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 12 Dec 2016 13:28:02 +0100 Subject: Improve detailed status badge partial --- app/views/admin/runners/show.html.haml | 2 +- app/views/ci/status/_badge.html.haml | 8 ++++++++ app/views/ci/status/_icon_with_description.html.haml | 12 ------------ app/views/projects/builds/_header.html.haml | 2 +- app/views/projects/ci/builds/_build.html.haml | 2 +- app/views/projects/ci/pipelines/_pipeline.html.haml | 2 +- .../generic_commit_statuses/_generic_commit_status.html.haml | 2 +- app/views/projects/pipelines/_info.html.haml | 2 +- 8 files changed, 14 insertions(+), 18 deletions(-) create mode 100644 app/views/ci/status/_badge.html.haml delete mode 100644 app/views/ci/status/_icon_with_description.html.haml diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index badeb11b208..ca503e35623 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -91,7 +91,7 @@ %strong ##{build.id} %td.status - = render "ci/status/icon_with_description", subject: build + = render 'ci/status/badge', status: build.detailed_status(current_user) %td.status - if project diff --git a/app/views/ci/status/_badge.html.haml b/app/views/ci/status/_badge.html.haml new file mode 100644 index 00000000000..b1b6e9c2b05 --- /dev/null +++ b/app/views/ci/status/_badge.html.haml @@ -0,0 +1,8 @@ +- if status.has_details? + = link_to status.details_path, class: "ci-status ci-#{status}" do + = custom_icon(status.icon) + = status.text +- else + %span{ class: "ci-status ci-#{status}" } + = custom_icon(status.icon) + = detailed_status.text diff --git a/app/views/ci/status/_icon_with_description.html.haml b/app/views/ci/status/_icon_with_description.html.haml deleted file mode 100644 index 34c923440d0..00000000000 --- a/app/views/ci/status/_icon_with_description.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -- detailed_status = subject.detailed_status(current_user) -- details_path = detailed_status.details_path if detailed_status.has_details? -- klass = "ci-status ci-#{detailed_status}" - -- if details_path - = link_to details_path, class: klass do - = custom_icon(detailed_status.icon) - = detailed_status.text -- else - %span{ class: klass } - = custom_icon(detailed_status.icon) - = detailed_status.text diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml index 85d1793ecb9..057a720a54a 100644 --- a/app/views/projects/builds/_header.html.haml +++ b/app/views/projects/builds/_header.html.haml @@ -1,6 +1,6 @@ .content-block.build-header .header-content - = render "ci/status/icon_with_description", subject: build + = render 'ci/status/badge', status: @build.detailed_status(current_user) Build %strong ##{@build.id} in pipeline diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 4257bb86859..f1cb0201032 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -9,7 +9,7 @@ %tr.build.commit{class: ('retried' if retried)} %td.status - = render "ci/status/icon_with_description", subject: build + = render "ci/status/badge", status: build.detailed_status(current_user) %td.branch-commit - if can?(current_user, :read_build, build) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 6dff955ea3d..3f05a21990f 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -4,7 +4,7 @@ %tr.commit %td.commit-link - = render "ci/status/icon_with_description", subject: pipeline + = render 'ci/status/badge', status: pipeline.detailed_status(current_user) %td = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index 1dd07ae1a2a..9f444f076c0 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -8,7 +8,7 @@ %tr.generic_commit_status{class: ('retried' if retried)} %td.status - = render "ci/status/icon_with_description", subject: generic_commit_status + = render 'ci/status/badge', status: generic_commit_status.detailed_status(current_user) %td.generic_commit_status-link - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index d05697b4ee3..b00ba2d5307 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -1,6 +1,6 @@ .page-content-header .header-main-content - = render "ci/status/icon_with_description", subject: @pipeline + = render 'ci/status/badge', status: @pipeline.detailed_status(current_user) %strong Pipeline ##{@commit.pipelines.last.id} triggered #{time_ago_with_tooltip(@commit.authored_date)} by = author_avatar(@commit, size: 24) -- cgit v1.2.1 From d60820146f87863a1ef93ccdc678fd617029950a Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 12 Dec 2016 13:28:20 +0100 Subject: Fix path to build status details in common helpers --- lib/gitlab/ci/status/build/common.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/ci/status/build/common.rb b/lib/gitlab/ci/status/build/common.rb index 2b602f1e247..3fec2c5d4db 100644 --- a/lib/gitlab/ci/status/build/common.rb +++ b/lib/gitlab/ci/status/build/common.rb @@ -10,7 +10,7 @@ module Gitlab def details_path namespace_project_build_path(subject.project.namespace, subject.project, - subject.pipeline) + subject) end end end -- cgit v1.2.1 From 7c9a85e35386623bc26374e4eb9b4a53e4fbbce7 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 9 Dec 2016 14:37:41 +0000 Subject: Fix TypeError: Cannot read property 'initTabs' --- app/assets/javascripts/pipelines.js.es6 | 2 +- changelogs/unreleased/25483-broken-tabs.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/25483-broken-tabs.yml diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 index 72c6c4a1fcd..a7a384fd856 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 @@ -4,7 +4,7 @@ ((global) => { class Pipelines { - constructor(options) { + constructor(options = {}) { if (options.initTabs && options.tabsOptions) { new global.LinkedTabs(options.tabsOptions); diff --git a/changelogs/unreleased/25483-broken-tabs.yml b/changelogs/unreleased/25483-broken-tabs.yml new file mode 100644 index 00000000000..7bc50bdf860 --- /dev/null +++ b/changelogs/unreleased/25483-broken-tabs.yml @@ -0,0 +1,4 @@ +--- +title: Fix TypeError: Cannot read property 'initTabs' on commit builds tab +merge_request: +author: -- cgit v1.2.1 From 401a2ec0b159b3c5f4de617768b9a0489a7cdde3 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 9 Dec 2016 15:13:20 +0000 Subject: Adds tests to prevent future errors. Fix undefined variable in es5 --- app/assets/javascripts/pipelines.js.es6 | 2 +- changelogs/unreleased/25483-broken-tabs.yml | 2 +- spec/javascripts/fixtures/pipeline_graph.html.haml | 15 +++++++++++++ spec/javascripts/pipelines_spec.js.es6 | 25 ++++++++++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 spec/javascripts/fixtures/pipeline_graph.html.haml create mode 100644 spec/javascripts/pipelines_spec.js.es6 diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 index a7a384fd856..fd1e320dc35 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 @@ -16,7 +16,7 @@ addMarginToBuildColumns() { this.pipelineGraph = document.querySelector('.pipeline-graph'); const secondChildBuildNodes = document.querySelector('.pipeline-graph').querySelectorAll('.build:nth-child(2)'); - for (buildNodeIndex in secondChildBuildNodes) { + for (const buildNodeIndex in secondChildBuildNodes) { const buildNode = secondChildBuildNodes[buildNodeIndex]; const firstChildBuildNode = buildNode.previousElementSibling; if (!firstChildBuildNode || !firstChildBuildNode.matches('.build')) continue; diff --git a/changelogs/unreleased/25483-broken-tabs.yml b/changelogs/unreleased/25483-broken-tabs.yml index 7bc50bdf860..d6c92014bea 100644 --- a/changelogs/unreleased/25483-broken-tabs.yml +++ b/changelogs/unreleased/25483-broken-tabs.yml @@ -1,4 +1,4 @@ --- title: Fix TypeError: Cannot read property 'initTabs' on commit builds tab -merge_request: +merge_request: 8009 author: diff --git a/spec/javascripts/fixtures/pipeline_graph.html.haml b/spec/javascripts/fixtures/pipeline_graph.html.haml new file mode 100644 index 00000000000..be5f105767c --- /dev/null +++ b/spec/javascripts/fixtures/pipeline_graph.html.haml @@ -0,0 +1,15 @@ +%div.pipeline-visualization.pipeline-graph + %ul.stage-column-list + %li.stage-column + .stage-name + %a{:href => "/"} + Test + .builds-container + %ul + %li.build + .curve + .build-content + %a + %svg + .ci-status-text + stop_review diff --git a/spec/javascripts/pipelines_spec.js.es6 b/spec/javascripts/pipelines_spec.js.es6 new file mode 100644 index 00000000000..85c9cf4b4f1 --- /dev/null +++ b/spec/javascripts/pipelines_spec.js.es6 @@ -0,0 +1,25 @@ +//= require pipelines + +(() => { + describe('Pipelines', () => { + fixture.preload('pipeline_graph'); + + beforeEach(() => { + fixture.load('pipeline_graph'); + }); + + it('should be defined', () => { + expect(window.gl.Pipelines).toBeDefined(); + }); + + it('should create a `Pipelines` instance without options', () => { + expect(() => { new window.gl.Pipelines(); }).not.toThrow(); //eslint-disable-line + }); + + it('should create a `Pipelines` instance with options', () => { + const pipelines = new window.gl.Pipelines({ foo: 'bar' }); + + expect(pipelines.pipelineGraph).toBeDefined(); + }); + }); +})(); -- cgit v1.2.1 From 94e0f402334af845bb44e92a3e2646780d633ce2 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 9 Dec 2016 16:14:54 +0000 Subject: Fix Pipeline graph disappeared from the builds tab in commits and merge request views --- app/assets/javascripts/pipelines.js.es6 | 4 ++-- app/views/projects/commit/_pipeline.html.haml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 index fd1e320dc35..f09c6bb7def 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 @@ -14,8 +14,8 @@ } addMarginToBuildColumns() { - this.pipelineGraph = document.querySelector('.pipeline-graph'); - const secondChildBuildNodes = document.querySelector('.pipeline-graph').querySelectorAll('.build:nth-child(2)'); + this.pipelineGraph = document.querySelector('.js-pipeline-graph'); + const secondChildBuildNodes = document.querySelector('.js-pipeline-graph').querySelectorAll('.build:nth-child(2)'); for (const buildNodeIndex in secondChildBuildNodes) { const buildNode = secondChildBuildNodes[buildNodeIndex]; const firstChildBuildNode = buildNode.previousElementSibling; diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index c7b5c1124b3..08d3443b3d0 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -24,7 +24,7 @@ in = time_interval_in_words pipeline.duration - .row-content-block.build-content.middle-block.hidden + .row-content-block.build-content.middle-block.js-pipeline-graph.hidden = render "projects/pipelines/graph", pipeline: pipeline - if pipeline.yaml_errors.present? -- cgit v1.2.1 From 52e0c4ba916d2cbc9bdb0fa0782c6b705c03c5a6 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 9 Dec 2016 16:16:14 +0000 Subject: Fix tests Fix broken tests --- app/assets/javascripts/pipelines.js.es6 | 5 ++++- app/views/projects/ci/builds/_build_pipeline.html.haml | 4 ++-- .../_generic_commit_status_pipeline.html.haml | 2 +- app/views/projects/pipelines/_with_tabs.html.haml | 2 +- spec/javascripts/fixtures/pipeline_graph.html.haml | 2 +- spec/views/projects/pipelines/show.html.haml_spec.rb | 2 +- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 index f09c6bb7def..fb95648e1c7 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 @@ -15,7 +15,9 @@ addMarginToBuildColumns() { this.pipelineGraph = document.querySelector('.js-pipeline-graph'); - const secondChildBuildNodes = document.querySelector('.js-pipeline-graph').querySelectorAll('.build:nth-child(2)'); + + const secondChildBuildNodes = this.pipelineGraph.querySelectorAll('.build:nth-child(2)'); + for (const buildNodeIndex in secondChildBuildNodes) { const buildNode = secondChildBuildNodes[buildNodeIndex]; const firstChildBuildNode = buildNode.previousElementSibling; @@ -28,6 +30,7 @@ const columnBuilds = previousColumn.querySelectorAll('.build'); if (columnBuilds.length === 1) previousColumn.classList.add('no-margin'); } + this.pipelineGraph.classList.remove('hidden'); } } diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml index 423a1282eb2..ad1a7360a8b 100644 --- a/app/views/projects/ci/builds/_build_pipeline.html.haml +++ b/app/views/projects/ci/builds/_build_pipeline.html.haml @@ -1,10 +1,10 @@ - is_playable = subject.playable? && can?(current_user, :update_build, @project) - if is_playable - = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.pipeline-graph', placement: 'bottom' } do + = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.js-pipeline-graph', placement: 'bottom' } do = ci_icon_for_status('play') .ci-status-text= subject.name - elsif can?(current_user, :read_build, @project) - = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } do + = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } do %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} = ci_icon_for_status(subject.status) .ci-status-text= subject.name diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml index 7b82d913d29..1bba0443154 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml @@ -1,4 +1,4 @@ -%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } } +%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } } - if subject.target_url = link_to subject.target_url do %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 739e5930822..88af41aa835 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -12,7 +12,7 @@ .tab-content #js-tab-pipeline.tab-pane - .build-content.middle-block + .build-content.middle-block.js-pipeline-graph = render "projects/pipelines/graph", pipeline: pipeline #js-tab-builds.tab-pane diff --git a/spec/javascripts/fixtures/pipeline_graph.html.haml b/spec/javascripts/fixtures/pipeline_graph.html.haml index be5f105767c..deca50ceaa7 100644 --- a/spec/javascripts/fixtures/pipeline_graph.html.haml +++ b/spec/javascripts/fixtures/pipeline_graph.html.haml @@ -1,4 +1,4 @@ -%div.pipeline-visualization.pipeline-graph +%div.pipeline-visualization.js-pipeline-graph %ul.stage-column-list %li.stage-column .stage-name diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb index bf027499c94..a066ea078e6 100644 --- a/spec/views/projects/pipelines/show.html.haml_spec.rb +++ b/spec/views/projects/pipelines/show.html.haml_spec.rb @@ -28,7 +28,7 @@ describe 'projects/pipelines/show' do it 'shows a graph with grouped stages' do render - expect(rendered).to have_css('.pipeline-graph') + expect(rendered).to have_css('.js-pipeline-graph') expect(rendered).to have_css('.grouped-pipeline-dropdown') # stages -- cgit v1.2.1 From 81a12c10fe7ba6f58ab4d1ae57861aa8899d545f Mon Sep 17 00:00:00 2001 From: Robert Schilling <rschilling@student.tugraz.at> Date: Mon, 12 Dec 2016 09:55:29 +0100 Subject: API: Fix groups filter --- lib/api/groups.rb | 11 ++++++++++- spec/requests/api/groups_spec.rb | 11 +++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/api/groups.rb b/lib/api/groups.rb index fbf7513302b..105d3ee342e 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -1,7 +1,7 @@ module API class Groups < Grape::API include PaginationParams - + before { authenticate! } helpers do @@ -117,11 +117,20 @@ module API success Entities::Project end params do + optional :archived, type: Boolean, default: false, desc: 'Limit by archived status' + optional :visibility, type: String, values: %w[public internal private], + desc: 'Limit by visibility' + optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria' + optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at], + default: 'created_at', desc: 'Return projects ordered by field' + optional :sort, type: String, values: %w[asc desc], default: 'desc', + desc: 'Return projects sorted in ascending and descending order' use :pagination end get ":id/projects" do group = find_group!(params[:id]) projects = GroupProjectsFinder.new(group).execute(current_user) + projects = filter_projects(projects) present paginate(projects), with: Entities::Project, user: current_user end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 548ed8e1892..15647b262b6 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -245,6 +245,17 @@ describe API::Groups, api: true do expect(project_names).to match_array([project1.name, project3.name]) end + it 'filters the groups projects' do + public_projet = create(:project, :public, path: 'test1', group: group1) + + get api("/groups/#{group1.id}/projects", user1), visibility: 'public' + + expect(response).to have_http_status(200) + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(1) + expect(json_response.first['name']).to eq(public_projet.name) + end + it "does not return a non existing group" do get api("/groups/1328/projects", user1) expect(response).to have_http_status(404) -- cgit v1.2.1 From 2f45d3bcf0f28d4cd4124b4c9722edc1d3085201 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Fri, 9 Dec 2016 18:48:20 +0100 Subject: API: Memoize the current_user so that the sudo can work properly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The issue was arising when `#current_user` was called a second time after a user was impersonated: the `User#is_admin?` check would be performed on it and it would fail. Signed-off-by: Rémy Coutable <remy@rymai.me> --- changelogs/unreleased/25482-fix-api-sudo.yml | 4 + lib/api/helpers.rb | 131 +++++---- lib/api/users.rb | 2 +- spec/requests/api/api_helpers_spec.rb | 404 ------------------------- spec/requests/api/helpers_spec.rb | 425 +++++++++++++++++++++++++++ spec/requests/api/users_spec.rb | 27 +- 6 files changed, 523 insertions(+), 470 deletions(-) create mode 100644 changelogs/unreleased/25482-fix-api-sudo.yml delete mode 100644 spec/requests/api/api_helpers_spec.rb create mode 100644 spec/requests/api/helpers_spec.rb diff --git a/changelogs/unreleased/25482-fix-api-sudo.yml b/changelogs/unreleased/25482-fix-api-sudo.yml new file mode 100644 index 00000000000..3b23bfd3a21 --- /dev/null +++ b/changelogs/unreleased/25482-fix-api-sudo.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Memoize the current_user so that the sudo can work properly' +merge_request: 8017 +author: diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 8b0f8deadfa..2041f0dac6b 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -7,67 +7,23 @@ module API SUDO_HEADER = "HTTP_SUDO" SUDO_PARAM = :sudo - def private_token - params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER] - end - - def warden - env['warden'] - end - - # Check the Rails session for valid authentication details - # - # Until CSRF protection is added to the API, disallow this method for - # state-changing endpoints - def find_user_from_warden - warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD']) - end - def declared_params(options = {}) options = { include_parent_namespaces: false }.merge(options) declared(params, options).to_h.symbolize_keys end - def find_user_by_private_token - token = private_token - return nil unless token.present? - - User.find_by_authentication_token(token) || User.find_by_personal_access_token(token) - end - def current_user - @current_user ||= find_user_by_private_token - @current_user ||= doorkeeper_guard - @current_user ||= find_user_from_warden - - unless @current_user && Gitlab::UserAccess.new(@current_user).allowed? - return nil - end - - identifier = sudo_identifier + return @current_user if defined?(@current_user) - if identifier - # We check for private_token because we cannot allow PAT to be used - forbidden!('Must be admin to use sudo') unless @current_user.is_admin? - forbidden!('Private token must be specified in order to use sudo') unless private_token_used? + @current_user = initial_current_user - @impersonator = @current_user - @current_user = User.by_username_or_id(identifier) - not_found!("No user id or username for: #{identifier}") if @current_user.nil? - end + sudo! @current_user end - def sudo_identifier - identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER] - - # Regex for integers - if !!(identifier =~ /\A[0-9]+\z/) - identifier.to_i - else - identifier - end + def sudo? + initial_current_user != current_user end def user_project @@ -354,6 +310,79 @@ module API private + def private_token + params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER] + end + + def warden + env['warden'] + end + + # Check the Rails session for valid authentication details + # + # Until CSRF protection is added to the API, disallow this method for + # state-changing endpoints + def find_user_from_warden + warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD']) + end + + def find_user_by_private_token + token = private_token + return nil unless token.present? + + User.find_by_authentication_token(token) || User.find_by_personal_access_token(token) + end + + def initial_current_user + return @initial_current_user if defined?(@initial_current_user) + + @initial_current_user ||= find_user_by_private_token + @initial_current_user ||= doorkeeper_guard + @initial_current_user ||= find_user_from_warden + + unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed? + @initial_current_user = nil + end + + @initial_current_user + end + + def sudo! + return unless sudo_identifier + return unless initial_current_user.is_a?(User) + + unless initial_current_user.is_admin? + forbidden!('Must be admin to use sudo') + end + + # Only private tokens should be used for the SUDO feature + unless private_token == initial_current_user.private_token + forbidden!('Private token must be specified in order to use sudo') + end + + sudoed_user = User.by_username_or_id(sudo_identifier) + + if sudoed_user + @current_user = sudoed_user + else + not_found!("No user id or username for: #{sudo_identifier}") + end + end + + def sudo_identifier + return @sudo_identifier if defined?(@sudo_identifier) + + identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER] + + # Regex for integers + @sudo_identifier = + if !!(identifier =~ /\A[0-9]+\z/) + identifier.to_i + else + identifier + end + end + def add_pagination_headers(paginated_data) header 'X-Total', paginated_data.total_count.to_s header 'X-Total-Pages', paginated_data.total_pages.to_s @@ -386,10 +415,6 @@ module API links.join(', ') end - def private_token_used? - private_token == @current_user.private_token - end - def secret_token Gitlab::Shell.secret_token end diff --git a/lib/api/users.rb b/lib/api/users.rb index 1dab799dd61..c7db2d71017 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -353,7 +353,7 @@ module API success Entities::UserPublic end get do - present current_user, with: @impersonator ? Entities::UserWithPrivateToken : Entities::UserPublic + present current_user, with: sudo? ? Entities::UserWithPrivateToken : Entities::UserPublic end desc "Get the currently authenticated user's SSH keys" do diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb deleted file mode 100644 index 3f34309f419..00000000000 --- a/spec/requests/api/api_helpers_spec.rb +++ /dev/null @@ -1,404 +0,0 @@ -require 'spec_helper' - -describe API::Helpers, api: true do - include API::Helpers - include ApiHelpers - include SentryHelper - - let(:user) { create(:user) } - let(:admin) { create(:admin) } - let(:key) { create(:key, user: user) } - - let(:params) { {} } - let(:env) { { 'REQUEST_METHOD' => 'GET' } } - let(:request) { Rack::Request.new(env) } - - def set_env(token_usr, identifier) - clear_env - clear_param - env[API::Helpers::PRIVATE_TOKEN_HEADER] = token_usr.private_token - env[API::Helpers::SUDO_HEADER] = identifier - end - - def set_param(token_usr, identifier) - clear_env - clear_param - params[API::Helpers::PRIVATE_TOKEN_PARAM] = token_usr.private_token - params[API::Helpers::SUDO_PARAM] = identifier - end - - def clear_env - env.delete(API::Helpers::PRIVATE_TOKEN_HEADER) - env.delete(API::Helpers::SUDO_HEADER) - end - - def clear_param - params.delete(API::Helpers::PRIVATE_TOKEN_PARAM) - params.delete(API::Helpers::SUDO_PARAM) - end - - def warden_authenticate_returns(value) - warden = double("warden", authenticate: value) - env['warden'] = warden - end - - def doorkeeper_guard_returns(value) - allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ value } - end - - def error!(message, status) - raise Exception.new("#{status} - #{message}") - end - - describe ".current_user" do - subject { current_user } - - describe "Warden authentication" do - before { doorkeeper_guard_returns false } - - context "with invalid credentials" do - context "GET request" do - before { env['REQUEST_METHOD'] = 'GET' } - it { is_expected.to be_nil } - end - end - - context "with valid credentials" do - before { warden_authenticate_returns user } - - context "GET request" do - before { env['REQUEST_METHOD'] = 'GET' } - it { is_expected.to eq(user) } - end - - context "HEAD request" do - before { env['REQUEST_METHOD'] = 'HEAD' } - it { is_expected.to eq(user) } - end - - context "PUT request" do - before { env['REQUEST_METHOD'] = 'PUT' } - it { is_expected.to be_nil } - end - - context "POST request" do - before { env['REQUEST_METHOD'] = 'POST' } - it { is_expected.to be_nil } - end - - context "DELETE request" do - before { env['REQUEST_METHOD'] = 'DELETE' } - it { is_expected.to be_nil } - end - end - end - - describe "when authenticating using a user's private token" do - it "returns nil for an invalid token" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token' - allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } - expect(current_user).to be_nil - end - - it "returns nil for a user without access" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token - allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) - expect(current_user).to be_nil - end - - it "leaves user as is when sudo not specified" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token - expect(current_user).to eq(user) - clear_env - params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token - expect(current_user).to eq(user) - end - end - - describe "when authenticating using a user's personal access tokens" do - let(:personal_access_token) { create(:personal_access_token, user: user) } - - it "returns nil for an invalid token" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token' - allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } - expect(current_user).to be_nil - end - - it "returns nil for a user without access" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token - allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) - expect(current_user).to be_nil - end - - it "leaves user as is when sudo not specified" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token - expect(current_user).to eq(user) - clear_env - params[API::Helpers::PRIVATE_TOKEN_PARAM] = personal_access_token.token - expect(current_user).to eq(user) - end - - it 'does not allow revoked tokens' do - personal_access_token.revoke! - env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token - allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } - expect(current_user).to be_nil - end - - it 'does not allow expired tokens' do - personal_access_token.update_attributes!(expires_at: 1.day.ago) - env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token - allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } - expect(current_user).to be_nil - end - end - - context 'sudo usage' do - context 'with admin' do - context 'with header' do - context 'with id' do - it 'changes current_user to sudo' do - set_env(admin, user.id) - - expect(current_user).to eq(user) - end - - it 'handles sudo to oneself' do - set_env(admin, admin.id) - - expect(current_user).to eq(admin) - end - - it 'throws an error when user cannot be found' do - id = user.id + admin.id - expect(user.id).not_to eq(id) - expect(admin.id).not_to eq(id) - - set_env(admin, id) - - expect { current_user }.to raise_error(Exception) - end - end - - context 'with username' do - it 'changes current_user to sudo' do - set_env(admin, user.username) - - expect(current_user).to eq(user) - end - - it 'handles sudo to oneself' do - set_env(admin, admin.username) - - expect(current_user).to eq(admin) - end - - it "throws an error when the user cannot be found for a given username" do - username = "#{user.username}#{admin.username}" - expect(user.username).not_to eq(username) - expect(admin.username).not_to eq(username) - - set_env(admin, username) - - expect { current_user }.to raise_error(Exception) - end - end - end - - context 'with param' do - context 'with id' do - it 'changes current_user to sudo' do - set_param(admin, user.id) - - expect(current_user).to eq(user) - end - - it 'handles sudo to oneself' do - set_param(admin, admin.id) - - expect(current_user).to eq(admin) - end - - it 'handles sudo to oneself using string' do - set_env(admin, user.id.to_s) - - expect(current_user).to eq(user) - end - - it 'throws an error when user cannot be found' do - id = user.id + admin.id - expect(user.id).not_to eq(id) - expect(admin.id).not_to eq(id) - - set_param(admin, id) - - expect { current_user }.to raise_error(Exception) - end - end - - context 'with username' do - it 'changes current_user to sudo' do - set_param(admin, user.username) - - expect(current_user).to eq(user) - end - - it 'handles sudo to oneself' do - set_param(admin, admin.username) - - expect(current_user).to eq(admin) - end - - it "throws an error when the user cannot be found for a given username" do - username = "#{user.username}#{admin.username}" - expect(user.username).not_to eq(username) - expect(admin.username).not_to eq(username) - - set_param(admin, username) - - expect { current_user }.to raise_error(Exception) - end - end - end - end - - context 'with regular user' do - context 'with env' do - it 'changes current_user to sudo when admin and user id' do - set_env(user, admin.id) - - expect { current_user }.to raise_error(Exception) - end - - it 'changes current_user to sudo when admin and user username' do - set_env(user, admin.username) - - expect { current_user }.to raise_error(Exception) - end - end - - context 'with params' do - it 'changes current_user to sudo when admin and user id' do - set_param(user, admin.id) - - expect { current_user }.to raise_error(Exception) - end - - it 'changes current_user to sudo when admin and user username' do - set_param(user, admin.username) - - expect { current_user }.to raise_error(Exception) - end - end - end - end - end - - describe '.sudo_identifier' do - it "returns integers when input is an int" do - set_env(admin, '123') - expect(sudo_identifier).to eq(123) - set_env(admin, '0001234567890') - expect(sudo_identifier).to eq(1234567890) - - set_param(admin, '123') - expect(sudo_identifier).to eq(123) - set_param(admin, '0001234567890') - expect(sudo_identifier).to eq(1234567890) - end - - it "returns string when input is an is not an int" do - set_env(admin, '12.30') - expect(sudo_identifier).to eq("12.30") - set_env(admin, 'hello') - expect(sudo_identifier).to eq('hello') - set_env(admin, ' 123') - expect(sudo_identifier).to eq(' 123') - - set_param(admin, '12.30') - expect(sudo_identifier).to eq("12.30") - set_param(admin, 'hello') - expect(sudo_identifier).to eq('hello') - set_param(admin, ' 123') - expect(sudo_identifier).to eq(' 123') - end - end - - describe '.handle_api_exception' do - before do - allow_any_instance_of(self.class).to receive(:sentry_enabled?).and_return(true) - allow_any_instance_of(self.class).to receive(:rack_response) - end - - it 'does not report a MethodNotAllowed exception to Sentry' do - exception = Grape::Exceptions::MethodNotAllowed.new({ 'X-GitLab-Test' => '1' }) - allow(exception).to receive(:backtrace).and_return(caller) - - expect(Raven).not_to receive(:capture_exception).with(exception) - - handle_api_exception(exception) - end - - it 'does report RuntimeError to Sentry' do - exception = RuntimeError.new('test error') - allow(exception).to receive(:backtrace).and_return(caller) - - expect_any_instance_of(self.class).to receive(:sentry_context) - expect(Raven).to receive(:capture_exception).with(exception) - - handle_api_exception(exception) - end - end - - describe '.authenticate_non_get!' do - %w[HEAD GET].each do |method_name| - context "method is #{method_name}" do - before do - expect_any_instance_of(self.class).to receive(:route).and_return(double(route_method: method_name)) - end - - it 'does not raise an error' do - expect_any_instance_of(self.class).not_to receive(:authenticate!) - - expect { authenticate_non_get! }.not_to raise_error - end - end - end - - %w[POST PUT PATCH DELETE].each do |method_name| - context "method is #{method_name}" do - before do - expect_any_instance_of(self.class).to receive(:route).and_return(double(route_method: method_name)) - end - - it 'calls authenticate!' do - expect_any_instance_of(self.class).to receive(:authenticate!) - - authenticate_non_get! - end - end - end - end - - describe '.authenticate!' do - context 'current_user is nil' do - before do - expect_any_instance_of(self.class).to receive(:current_user).and_return(nil) - end - - it 'returns a 401 response' do - expect { authenticate! }.to raise_error '401 - {"message"=>"401 Unauthorized"}' - end - end - - context 'current_user is present' do - before do - expect_any_instance_of(self.class).to receive(:current_user).and_return(true) - end - - it 'does not raise an error' do - expect { authenticate! }.not_to raise_error - end - end - end -end diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb new file mode 100644 index 00000000000..573154f69b4 --- /dev/null +++ b/spec/requests/api/helpers_spec.rb @@ -0,0 +1,425 @@ +require 'spec_helper' + +describe API::Helpers, api: true do + include API::Helpers + include SentryHelper + + let(:user) { create(:user) } + let(:admin) { create(:admin) } + let(:key) { create(:key, user: user) } + + let(:params) { {} } + let(:env) { { 'REQUEST_METHOD' => 'GET' } } + let(:request) { Rack::Request.new(env) } + + def set_env(user_or_token, identifier) + clear_env + clear_param + env[API::Helpers::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token + env[API::Helpers::SUDO_HEADER] = identifier + end + + def set_param(user_or_token, identifier) + clear_env + clear_param + params[API::Helpers::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token + params[API::Helpers::SUDO_PARAM] = identifier + end + + def clear_env + env.delete(API::Helpers::PRIVATE_TOKEN_HEADER) + env.delete(API::Helpers::SUDO_HEADER) + end + + def clear_param + params.delete(API::Helpers::PRIVATE_TOKEN_PARAM) + params.delete(API::Helpers::SUDO_PARAM) + end + + def warden_authenticate_returns(value) + warden = double("warden", authenticate: value) + env['warden'] = warden + end + + def doorkeeper_guard_returns(value) + allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ value } + end + + def error!(message, status) + raise Exception.new("#{status} - #{message}") + end + + describe ".current_user" do + subject { current_user } + + describe "Warden authentication" do + before { doorkeeper_guard_returns false } + + context "with invalid credentials" do + context "GET request" do + before { env['REQUEST_METHOD'] = 'GET' } + it { is_expected.to be_nil } + end + end + + context "with valid credentials" do + before { warden_authenticate_returns user } + + context "GET request" do + before { env['REQUEST_METHOD'] = 'GET' } + it { is_expected.to eq(user) } + end + + context "HEAD request" do + before { env['REQUEST_METHOD'] = 'HEAD' } + it { is_expected.to eq(user) } + end + + context "PUT request" do + before { env['REQUEST_METHOD'] = 'PUT' } + it { is_expected.to be_nil } + end + + context "POST request" do + before { env['REQUEST_METHOD'] = 'POST' } + it { is_expected.to be_nil } + end + + context "DELETE request" do + before { env['REQUEST_METHOD'] = 'DELETE' } + it { is_expected.to be_nil } + end + end + end + + describe "when authenticating using a user's private token" do + it "returns nil for an invalid token" do + env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token' + allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + expect(current_user).to be_nil + end + + it "returns nil for a user without access" do + env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token + allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) + expect(current_user).to be_nil + end + + it "leaves user as is when sudo not specified" do + env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token + expect(current_user).to eq(user) + clear_env + params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token + expect(current_user).to eq(user) + end + end + + describe "when authenticating using a user's personal access tokens" do + let(:personal_access_token) { create(:personal_access_token, user: user) } + + it "returns nil for an invalid token" do + env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token' + allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + expect(current_user).to be_nil + end + + it "returns nil for a user without access" do + env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token + allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) + expect(current_user).to be_nil + end + + it "leaves user as is when sudo not specified" do + env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token + expect(current_user).to eq(user) + clear_env + params[API::Helpers::PRIVATE_TOKEN_PARAM] = personal_access_token.token + expect(current_user).to eq(user) + end + + it 'does not allow revoked tokens' do + personal_access_token.revoke! + env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token + allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + expect(current_user).to be_nil + end + + it 'does not allow expired tokens' do + personal_access_token.update_attributes!(expires_at: 1.day.ago) + env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token + allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + expect(current_user).to be_nil + end + end + + context 'sudo usage' do + context 'with admin' do + context 'with header' do + context 'with id' do + it 'changes current_user to sudo' do + set_env(admin, user.id) + + expect(current_user).to eq(user) + end + + it 'memoize the current_user: sudo permissions are not run against the sudoed user' do + set_env(admin, user.id) + + expect(current_user).to eq(user) + expect(current_user).to eq(user) + end + + it 'handles sudo to oneself' do + set_env(admin, admin.id) + + expect(current_user).to eq(admin) + end + + it 'throws an error when user cannot be found' do + id = user.id + admin.id + expect(user.id).not_to eq(id) + expect(admin.id).not_to eq(id) + + set_env(admin, id) + + expect { current_user }.to raise_error(Exception) + end + end + + context 'with username' do + it 'changes current_user to sudo' do + set_env(admin, user.username) + + expect(current_user).to eq(user) + end + + it 'handles sudo to oneself' do + set_env(admin, admin.username) + + expect(current_user).to eq(admin) + end + + it "throws an error when the user cannot be found for a given username" do + username = "#{user.username}#{admin.username}" + expect(user.username).not_to eq(username) + expect(admin.username).not_to eq(username) + + set_env(admin, username) + + expect { current_user }.to raise_error(Exception) + end + end + end + + context 'with param' do + context 'with id' do + it 'changes current_user to sudo' do + set_param(admin, user.id) + + expect(current_user).to eq(user) + end + + it 'handles sudo to oneself' do + set_param(admin, admin.id) + + expect(current_user).to eq(admin) + end + + it 'handles sudo to oneself using string' do + set_env(admin, user.id.to_s) + + expect(current_user).to eq(user) + end + + it 'throws an error when user cannot be found' do + id = user.id + admin.id + expect(user.id).not_to eq(id) + expect(admin.id).not_to eq(id) + + set_param(admin, id) + + expect { current_user }.to raise_error(Exception) + end + end + + context 'with username' do + it 'changes current_user to sudo' do + set_param(admin, user.username) + + expect(current_user).to eq(user) + end + + it 'handles sudo to oneself' do + set_param(admin, admin.username) + + expect(current_user).to eq(admin) + end + + it "throws an error when the user cannot be found for a given username" do + username = "#{user.username}#{admin.username}" + expect(user.username).not_to eq(username) + expect(admin.username).not_to eq(username) + + set_param(admin, username) + + expect { current_user }.to raise_error(Exception) + end + end + end + end + + context 'with regular user' do + context 'with env' do + it 'changes current_user to sudo when admin and user id' do + set_env(user, admin.id) + + expect { current_user }.to raise_error(Exception) + end + + it 'changes current_user to sudo when admin and user username' do + set_env(user, admin.username) + + expect { current_user }.to raise_error(Exception) + end + end + + context 'with params' do + it 'changes current_user to sudo when admin and user id' do + set_param(user, admin.id) + + expect { current_user }.to raise_error(Exception) + end + + it 'changes current_user to sudo when admin and user username' do + set_param(user, admin.username) + + expect { current_user }.to raise_error(Exception) + end + end + end + end + end + + describe '.sudo?' do + context 'when no sudo env or param is passed' do + before do + doorkeeper_guard_returns(nil) + end + + it 'returns false' do + expect(sudo?).to be_falsy + end + end + + context 'when sudo env or param is passed', 'user is not an admin' do + before do + set_env(user, '123') + end + + it 'returns an 403 Forbidden' do + expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Must be admin to use sudo"}' + end + end + + context 'when sudo env or param is passed', 'user is admin' do + context 'personal access token is used' do + before do + personal_access_token = create(:personal_access_token, user: admin) + set_env(personal_access_token.token, user.id) + end + + it 'returns an 403 Forbidden' do + expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Private token must be specified in order to use sudo"}' + end + end + + context 'private access token is used' do + before do + set_env(admin.private_token, user.id) + end + + it 'returns true' do + expect(sudo?).to be_truthy + end + end + end + end + + describe '.handle_api_exception' do + before do + allow_any_instance_of(self.class).to receive(:sentry_enabled?).and_return(true) + allow_any_instance_of(self.class).to receive(:rack_response) + end + + it 'does not report a MethodNotAllowed exception to Sentry' do + exception = Grape::Exceptions::MethodNotAllowed.new({ 'X-GitLab-Test' => '1' }) + allow(exception).to receive(:backtrace).and_return(caller) + + expect(Raven).not_to receive(:capture_exception).with(exception) + + handle_api_exception(exception) + end + + it 'does report RuntimeError to Sentry' do + exception = RuntimeError.new('test error') + allow(exception).to receive(:backtrace).and_return(caller) + + expect_any_instance_of(self.class).to receive(:sentry_context) + expect(Raven).to receive(:capture_exception).with(exception) + + handle_api_exception(exception) + end + end + + describe '.authenticate_non_get!' do + %w[HEAD GET].each do |method_name| + context "method is #{method_name}" do + before do + expect_any_instance_of(self.class).to receive(:route).and_return(double(route_method: method_name)) + end + + it 'does not raise an error' do + expect_any_instance_of(self.class).not_to receive(:authenticate!) + + expect { authenticate_non_get! }.not_to raise_error + end + end + end + + %w[POST PUT PATCH DELETE].each do |method_name| + context "method is #{method_name}" do + before do + expect_any_instance_of(self.class).to receive(:route).and_return(double(route_method: method_name)) + end + + it 'calls authenticate!' do + expect_any_instance_of(self.class).to receive(:authenticate!) + + authenticate_non_get! + end + end + end + end + + describe '.authenticate!' do + context 'current_user is nil' do + before do + expect_any_instance_of(self.class).to receive(:current_user).and_return(nil) + end + + it 'returns a 401 response' do + expect { authenticate! }.to raise_error '401 - {"message"=>"401 Unauthorized"}' + end + end + + context 'current_user is present' do + before do + expect_any_instance_of(self.class).to receive(:current_user).and_return(true) + end + + it 'does not raise an error' do + expect { authenticate! }.not_to raise_error + end + end + end +end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index c37dbfa0a33..9e317f3a7e9 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -651,13 +651,12 @@ describe API::Users, api: true do end describe "GET /user" do - let(:personal_access_token) { create(:personal_access_token, user: user) } - let(:private_token) { user.private_token } + let(:personal_access_token) { create(:personal_access_token, user: user).token } context 'with regular user' do context 'with personal access token' do it 'returns 403 without private token when sudo is defined' do - get api("/user?private_token=#{personal_access_token.token}&sudo=#{user.id}") + get api("/user?private_token=#{personal_access_token}&sudo=123") expect(response).to have_http_status(403) end @@ -665,7 +664,7 @@ describe API::Users, api: true do context 'with private token' do it 'returns 403 without private token when sudo defined' do - get api("/user?private_token=#{private_token}&sudo=#{user.id}") + get api("/user?private_token=#{user.private_token}&sudo=123") expect(response).to have_http_status(403) end @@ -676,40 +675,44 @@ describe API::Users, api: true do expect(response).to have_http_status(200) expect(response).to match_response_schema('user/public') + expect(json_response['id']).to eq(user.id) end end context 'with admin' do - let(:user) { create(:admin) } + let(:admin_personal_access_token) { create(:personal_access_token, user: admin).token } context 'with personal access token' do it 'returns 403 without private token when sudo defined' do - get api("/user?private_token=#{personal_access_token.token}&sudo=#{user.id}") + get api("/user?private_token=#{admin_personal_access_token}&sudo=#{user.id}") expect(response).to have_http_status(403) end - it 'returns current user without private token when sudo not defined' do - get api("/user?private_token=#{personal_access_token.token}") + it 'returns initial current user without private token when sudo not defined' do + get api("/user?private_token=#{admin_personal_access_token}") expect(response).to have_http_status(200) expect(response).to match_response_schema('user/public') + expect(json_response['id']).to eq(admin.id) end end context 'with private token' do - it 'returns current user with private token when sudo defined' do - get api("/user?private_token=#{private_token}&sudo=#{user.id}") + it 'returns sudoed user with private token when sudo defined' do + get api("/user?private_token=#{admin.private_token}&sudo=#{user.id}") expect(response).to have_http_status(200) expect(response).to match_response_schema('user/login') + expect(json_response['id']).to eq(user.id) end - it 'returns current user without private token when sudo not defined' do - get api("/user?private_token=#{private_token}") + it 'returns initial current user without private token when sudo not defined' do + get api("/user?private_token=#{admin.private_token}") expect(response).to have_http_status(200) expect(response).to match_response_schema('user/public') + expect(json_response['id']).to eq(admin.id) end end end -- cgit v1.2.1 From 62f8717c035f8d287324d27563b3a42fd27839d6 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" <lbennett@gitlab.com> Date: Fri, 25 Nov 2016 18:42:25 +0000 Subject: Added hiddenInterval and immediateExecution settings, fixed visibilitychange listening, implemented with mr widget Updated tests Added tests Review changes --- app/assets/javascripts/merge_request_widget.js.es6 | 74 ++++++++-------------- app/assets/javascripts/smart_interval.js.es6 | 69 ++++++++++++++------ .../unreleased/24807-stop-ddosing-ourselves.yml | 4 ++ spec/javascripts/smart_interval_spec.js.es6 | 35 ++++++++-- 4 files changed, 105 insertions(+), 77 deletions(-) create mode 100644 changelogs/unreleased/24807-stop-ddosing-ourselves.yml diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index d9495e50388..7022aa1263b 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -40,19 +40,26 @@ $('#modal_merge_info').modal({ show: false }); - this.firstCICheck = true; - this.readyForCICheck = false; - this.readyForCIEnvironmentCheck = false; - this.cancel = false; - clearInterval(this.fetchBuildStatusInterval); - clearInterval(this.fetchBuildEnvironmentStatusInterval); this.clearEventListeners(); this.addEventListeners(); this.getCIStatus(false); - this.getCIEnvironmentsStatus(); this.retrieveSuccessIcon(); - this.pollCIStatus(); - this.pollCIEnvironmentsStatus(); + + this.ciStatusInterval = new global.SmartInterval({ + callback: this.getCIStatus.bind(this, true), + startingInterval: 10000, + maxInterval: 30000, + hiddenInterval: 120000, + incrementByFactorOf: 5000, + }); + this.ciEnvironmentStatusInterval = new global.SmartInterval({ + callback: this.getCIEnvironmentsStatus.bind(this), + startingInterval: 30000, + maxInterval: 120000, + hiddenInterval: 240000, + incrementByFactorOf: 15000, + immediateExecution: true, + }); notifyPermissions(); } @@ -60,10 +67,6 @@ return $(document).off('page:change.merge_request'); }; - MergeRequestWidget.prototype.cancelPolling = function() { - return this.cancel = true; - }; - MergeRequestWidget.prototype.addEventListeners = function() { var allowedPages; allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes']; @@ -72,9 +75,6 @@ var page; page = $('body').data('page').split(':').last(); if (allowedPages.indexOf(page) < 0) { - clearInterval(_this.fetchBuildStatusInterval); - clearInterval(_this.fetchBuildEnvironmentStatusInterval); - _this.cancelPolling(); return _this.clearEventListeners(); } }; @@ -114,6 +114,11 @@ }); }; + MergeRequestWidget.prototype.cancelPolling = function () { + this.ciStatusInterval.cancel(); + this.ciEnvironmentStatusInterval.cancel(); + }; + MergeRequestWidget.prototype.getMergeStatus = function() { return $.get(this.opts.merge_check_url, function(data) { return $('.mr-state-widget').replaceWith(data); @@ -131,18 +136,6 @@ } }; - MergeRequestWidget.prototype.pollCIStatus = function() { - return this.fetchBuildStatusInterval = setInterval(((function(_this) { - return function() { - if (!_this.readyForCICheck) { - return; - } - _this.getCIStatus(true); - return _this.readyForCICheck = false; - }; - })(this)), 10000); - }; - MergeRequestWidget.prototype.getCIStatus = function(showNotification) { var _this; _this = this; @@ -150,23 +143,17 @@ return $.getJSON(this.opts.ci_status_url, (function(_this) { return function(data) { var message, status, title; - if (_this.cancel) { - return; - } - _this.readyForCICheck = true; if (data.status === '') { return; } if (data.environments && data.environments.length) _this.renderEnvironments(data.environments); - if (_this.firstCICheck || data.status !== _this.opts.ci_status && (data.status != null)) { + if (data.status !== _this.opts.ci_status && (data.status != null)) { _this.opts.ci_status = data.status; _this.showCIStatus(data.status); if (data.coverage) { _this.showCICoverage(data.coverage); } - // The first check should only update the UI, a notification - // should only be displayed on status changes - if (showNotification && !_this.firstCICheck) { + if (showNotification) { status = _this.ciLabelForStatus(data.status); if (status === "preparing") { title = _this.opts.ci_title.preparing; @@ -184,24 +171,13 @@ return Turbolinks.visit(_this.opts.builds_path); }); } - return _this.firstCICheck = false; } }; })(this)); }; - MergeRequestWidget.prototype.pollCIEnvironmentsStatus = function() { - this.fetchBuildEnvironmentStatusInterval = setInterval(() => { - if (!this.readyForCIEnvironmentCheck) return; - this.getCIEnvironmentsStatus(); - this.readyForCIEnvironmentCheck = false; - }, 300000); - }; - MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() { $.getJSON(this.opts.ci_environments_status_url, (environments) => { - if (this.cancel) return; - this.readyForCIEnvironmentCheck = true; if (environments && environments.length) this.renderEnvironments(environments); }); }; @@ -212,11 +188,11 @@ if ($(`.mr-state-widget #${ environment.id }`).length) return; const $template = $(DEPLOYMENT_TEMPLATE); if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove(); - + if (!environment.stop_url) { $('.js-stop-env-link', $template).remove(); } - + if (environment.deployed_at && environment.deployed_at_formatted) { environment.deployed_at = gl.utils.getTimeago().format(environment.deployed_at, 'gl_en') + '.'; } else { diff --git a/app/assets/javascripts/smart_interval.js.es6 b/app/assets/javascripts/smart_interval.js.es6 index 5eb15dba79b..40f67637c7c 100644 --- a/app/assets/javascripts/smart_interval.js.es6 +++ b/app/assets/javascripts/smart_interval.js.es6 @@ -7,24 +7,31 @@ (() => { class SmartInterval { /** - * @param { function } callback Function to be called on each iteration (required) - * @param { milliseconds } startingInterval `currentInterval` is set to this initially - * @param { milliseconds } maxInterval `currentInterval` will be incremented to this - * @param { integer } incrementByFactorOf `currentInterval` is incremented by this factor - * @param { boolean } lazyStart Configure if timer is initialized on instantiation or lazily + * @param { function } opts.callback Function to be called on each iteration (required) + * @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially + * @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this + * @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this + * when the page is hidden + * @param { integer } opts.incrementByFactorOf `currentInterval` is incremented by this factor + * @param { boolean } opts.lazyStart Configure if timer is initialized on + * instantiation or lazily + * @param { boolean } opts.immediateExecution Configure if callback should + * be executed before the first interval. */ - constructor({ callback, startingInterval, maxInterval, incrementByFactorOf, lazyStart }) { + constructor(opts = {}) { this.cfg = { - callback, - startingInterval, - maxInterval, - incrementByFactorOf, - lazyStart, + callback: opts.callback, + startingInterval: opts.startingInterval, + maxInterval: opts.maxInterval, + hiddenInterval: opts.hiddenInterval, + incrementByFactorOf: opts.incrementByFactorOf, + lazyStart: opts.lazyStart, + immediateExecution: opts.immediateExecution, }; this.state = { intervalId: null, - currentInterval: startingInterval, + currentInterval: this.cfg.startingInterval, pageVisibility: 'visible', }; @@ -36,6 +43,11 @@ const cfg = this.cfg; const state = this.state; + if (cfg.immediateExecution) { + cfg.immediateExecution = false; + cfg.callback(); + } + state.intervalId = window.setInterval(() => { cfg.callback(); @@ -54,14 +66,29 @@ this.stopTimer(); } + onVisibilityHidden() { + if (this.cfg.hiddenInterval) { + this.setCurrentInterval(this.cfg.hiddenInterval); + this.resume(); + } else { + this.cancel(); + } + } + // start a timer, using the existing interval resume() { this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped this.start(); } + onVisibilityVisible() { + this.cancel(); + this.start(); + } + destroy() { this.cancel(); + document.removeEventListener('visibilitychange', this.handleVisibilityChange); $(document).off('visibilitychange').off('page:before-unload'); } @@ -80,11 +107,7 @@ initVisibilityChangeHandling() { // cancel interval when tab no longer shown (prevents cached pages from polling) - $(document) - .off('visibilitychange').on('visibilitychange', (e) => { - this.state.pageVisibility = e.target.visibilityState; - this.handleVisibilityChange(); - }); + document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); } initPageUnloadHandling() { @@ -92,10 +115,11 @@ $(document).on('page:before-unload', () => this.cancel()); } - handleVisibilityChange() { - const state = this.state; - - const intervalAction = state.pageVisibility === 'hidden' ? this.cancel : this.resume; + handleVisibilityChange(e) { + this.state.pageVisibility = e.target.visibilityState; + const intervalAction = this.isPageVisible() ? + this.onVisibilityVisible : + this.onVisibilityHidden; intervalAction.apply(this); } @@ -111,6 +135,7 @@ incrementInterval() { const cfg = this.cfg; const currentInterval = this.getCurrentInterval(); + if (cfg.hiddenInterval && !this.isPageVisible()) return; let nextInterval = currentInterval * cfg.incrementByFactorOf; if (nextInterval > cfg.maxInterval) { @@ -120,6 +145,8 @@ this.setCurrentInterval(nextInterval); } + isPageVisible() { return this.state.pageVisibility === 'visible'; } + stopTimer() { const state = this.state; diff --git a/changelogs/unreleased/24807-stop-ddosing-ourselves.yml b/changelogs/unreleased/24807-stop-ddosing-ourselves.yml new file mode 100644 index 00000000000..49e6c5e56e5 --- /dev/null +++ b/changelogs/unreleased/24807-stop-ddosing-ourselves.yml @@ -0,0 +1,4 @@ +--- +title: Use SmartInterval for MR widget and improve visibilitychange functionality +merge_request: 7762 +author: diff --git a/spec/javascripts/smart_interval_spec.js.es6 b/spec/javascripts/smart_interval_spec.js.es6 index ed6166a25a8..1b7ca97cde4 100644 --- a/spec/javascripts/smart_interval_spec.js.es6 +++ b/spec/javascripts/smart_interval_spec.js.es6 @@ -14,8 +14,9 @@ startingInterval: DEFAULT_STARTING_INTERVAL, maxInterval: DEFAULT_MAX_INTERVAL, incrementByFactorOf: DEFAULT_INCREMENT_FACTOR, - delayStartBy: 0, lazyStart: false, + immediateExecution: false, + hiddenInterval: null, }; if (config) { @@ -114,14 +115,31 @@ expect(interval.state.intervalId).toBeTruthy(); // simulates triggering of visibilitychange event - interval.state.pageVisibility = 'hidden'; - interval.handleVisibilityChange(); + interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } }); expect(interval.state.intervalId).toBeUndefined(); done(); }, DEFAULT_SHORT_TIMEOUT); }); + it('should change to the hidden interval when page is not visible', function (done) { + const HIDDEN_INTERVAL = 1500; + const interval = createDefaultSmartInterval({ hiddenInterval: HIDDEN_INTERVAL }); + + setTimeout(() => { + expect(interval.state.intervalId).toBeTruthy(); + expect(interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL && + interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL).toBeTruthy(); + + // simulates triggering of visibilitychange event + interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } }); + + expect(interval.state.intervalId).toBeTruthy(); + expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL); + done(); + }, DEFAULT_SHORT_TIMEOUT); + }); + it('should resume when page is becomes visible at the previous interval', function (done) { const interval = this.smartInterval; @@ -129,14 +147,12 @@ expect(interval.state.intervalId).toBeTruthy(); // simulates triggering of visibilitychange event - interval.state.pageVisibility = 'hidden'; - interval.handleVisibilityChange(); + interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } }); expect(interval.state.intervalId).toBeUndefined(); // simulates triggering of visibilitychange event - interval.state.pageVisibility = 'visible'; - interval.handleVisibilityChange(); + interval.handleVisibilityChange({ target: { visibilityState: 'visible' } }); expect(interval.state.intervalId).toBeTruthy(); @@ -154,6 +170,11 @@ done(); }, DEFAULT_SHORT_TIMEOUT); }); + + it('should execute callback before first interval', function () { + const interval = createDefaultSmartInterval({ immediateExecution: true }); + expect(interval.cfg.immediateExecution).toBeFalsy(); + }); }); }); })(window.gl || (window.gl = {})); -- cgit v1.2.1 From ffafd0973100a53efc46011ca4415e8385e0cc07 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 12 Dec 2016 14:14:35 +0100 Subject: Fix build stop extended status CSS class --- lib/gitlab/ci/status/build/stop.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb index 487fd033960..76c66fab323 100644 --- a/lib/gitlab/ci/status/build/stop.rb +++ b/lib/gitlab/ci/status/build/stop.rb @@ -17,10 +17,6 @@ module Gitlab 'icon_status_skipped' end - def to_s - 'stop' - end - def has_action? can?(user, :update_build, subject) end -- cgit v1.2.1 From 8d0645c2ce3769268020ad6f8e51db07fb1e4bc6 Mon Sep 17 00:00:00 2001 From: Robert Schilling <rschilling@student.tugraz.at> Date: Mon, 12 Dec 2016 14:27:52 +0100 Subject: Grapify the service API --- doc/api/services.md | 22 ++++++++++++++++------ .../project_services/hipchat_service_spec.rb | 2 +- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/doc/api/services.md b/doc/api/services.md index acb54448664..3dad953cd1e 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -153,9 +153,12 @@ PUT /projects/:id/services/builds-email Parameters: -- `recipients` (**required**) - Comma-separated list of recipient email addresses -- `add_pusher` (optional) - Add pusher to recipients list -- `notify_only_broken_builds` (optional) -Notify only broken builds +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `recipients` | string | yes | Comma-separated list of recipient email addresses | +| `add_pusher` | boolean | no | Add pusher to recipients list | +| `notify_only_broken_builds` | boolean | no | Notify only broken builds | + ### Delete Build-Emails service @@ -538,7 +541,10 @@ PUT /projects/:id/services/mattermost-slash-commands Parameters: -- `token` (**required**) - The Mattermost token +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `token` | string | yes | The Mattermost token | + ### Delete Mattermost Slash Command service @@ -570,8 +576,12 @@ PUT /projects/:id/services/pipelines-email Parameters: -- `recipients` (**required**) - Comma-separated list of recipient email addresses -- `notify_only_broken_builds` (optional) -Notify only broken pipelines +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `recipients` | string | yes | Comma-separated list of recipient email addresses | +| `add_pusher` | boolean | no | Add pusher to recipients list | +| `notify_only_broken_builds` | boolean | no | Notify only broken pipelines | + ### Delete Pipeline-Emails service diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 564e49d5459..2da3a9cb09f 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -358,7 +358,7 @@ describe HipchatService, models: true do context 'with a failed build' do it 'uses the red color' do build_data = { object_kind: 'build', commit: { status: 'failed' } } - + expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'red' }) end end -- cgit v1.2.1 From 17e3d3fde8c9a83f58d797c5f62f36b59eedd870 Mon Sep 17 00:00:00 2001 From: winniehell <git@winniehell.de> Date: Tue, 6 Dec 2016 01:29:43 +0100 Subject: Avoid escaping relative links in Markdown twice (!7940) --- changelogs/unreleased/unescape-relative-path.yml | 4 ++++ lib/banzai/filter/relative_link_filter.rb | 14 ++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 changelogs/unreleased/unescape-relative-path.yml diff --git a/changelogs/unreleased/unescape-relative-path.yml b/changelogs/unreleased/unescape-relative-path.yml new file mode 100644 index 00000000000..755b0379a16 --- /dev/null +++ b/changelogs/unreleased/unescape-relative-path.yml @@ -0,0 +1,4 @@ +--- +title: Avoid escaping relative links in Markdown twice +merge_request: 7940 +author: winniehell diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb index f09d78be0ce..9e23c8f8c55 100644 --- a/lib/banzai/filter/relative_link_filter.rb +++ b/lib/banzai/filter/relative_link_filter.rb @@ -46,7 +46,7 @@ module Banzai end def rebuild_relative_uri(uri) - file_path = relative_file_path(uri.path) + file_path = relative_file_path(uri) uri.path = [ relative_url_root, @@ -59,8 +59,10 @@ module Banzai uri end - def relative_file_path(path) - nested_path = build_relative_path(path, context[:requested_path]) + def relative_file_path(uri) + path = Addressable::URI.unescape(uri.path) + request_path = Addressable::URI.unescape(context[:requested_path]) + nested_path = build_relative_path(path, request_path) file_exists?(nested_path) ? nested_path : path end @@ -108,11 +110,7 @@ module Banzai end def uri_type(path) - @uri_types[path] ||= begin - unescaped_path = Addressable::URI.unescape(path) - - current_commit.uri_type(unescaped_path) - end + @uri_types[path] ||= current_commit.uri_type(path) end def current_commit -- cgit v1.2.1 From f91e8269c18ac80d6f43fdd7b87362e5c9ccbc94 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 12 Dec 2016 14:53:05 +0100 Subject: Add tests for build play extended detailed status --- spec/factories/ci/builds.rb | 13 ++++-- spec/lib/gitlab/ci/status/build/play_spec.rb | 59 ++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 spec/lib/gitlab/ci/status/build/play_spec.rb diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index c443af09075..62466c06194 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -12,12 +12,14 @@ FactoryGirl.define do 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", services: ["postgres"] } end + yaml_variables do [ { key: :DB_NAME, value: 'postgres', public: true } @@ -60,15 +62,20 @@ FactoryGirl.define do end trait :teardown_environment do - options do - { environment: { action: 'stop' } } - end + environment 'staging' + options environment: { name: 'staging', + action: 'stop' } end trait :allowed_to_fail do allow_failure true end + trait :playable do + skipped + manual + end + after(:build) do |build, evaluator| build.project = build.pipeline.project end diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb new file mode 100644 index 00000000000..ae103d8993d --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/play_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Play do + let(:core_status) { double('core status') } + let(:user) { double('user') } + + subject do + described_class.new(core_status) + end + + describe '#text' do + it { expect(subject.text).to eq 'play' } + end + + describe '#label' do + it { expect(subject.label).to eq 'play' } + end + + describe '#icon' do + it 'does not override core status icon' do + expect(core_status).to receive(:icon) + + subject.icon + end + end + + describe '.matches?' do + context 'build is playable' do + context 'when build stops an environment' do + let(:build) do + create(:ci_build, :playable, :teardown_environment) + end + + it 'does not match' do + expect(described_class.matches?(build, user)) + .to be false + end + end + + context 'when build does not stop an environment' do + let(:build) { create(:ci_build, :playable) } + + it 'is a correct match' do + expect(described_class.matches?(build, user)) + .to be true + end + end + end + + context 'when build is not playable' do + let(:build) { create(:ci_build) } + + it 'does not match' do + expect(described_class.matches?(build, user)) + .to be false + end + end + end +end -- cgit v1.2.1 From ab37be2dda8188c56d6e76a23b46ccc88c42baa8 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 12 Dec 2016 14:59:46 +0100 Subject: Add specs for build stop extended detailed status --- lib/gitlab/ci/status/build/stop.rb | 4 -- spec/lib/gitlab/ci/status/build/stop_spec.rb | 59 ++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 spec/lib/gitlab/ci/status/build/stop_spec.rb diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb index 76c66fab323..a4966a55892 100644 --- a/lib/gitlab/ci/status/build/stop.rb +++ b/lib/gitlab/ci/status/build/stop.rb @@ -13,10 +13,6 @@ module Gitlab 'stop' end - def icon - 'icon_status_skipped' - end - def has_action? can?(user, :update_build, subject) end diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb new file mode 100644 index 00000000000..f2121210417 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Stop do + let(:core_status) { double('core status') } + let(:user) { double('user') } + + subject do + described_class.new(core_status) + end + + describe '#text' do + it { expect(subject.text).to eq 'stop' } + end + + describe '#label' do + it { expect(subject.label).to eq 'stop' } + end + + describe '#icon' do + it 'does not override core status icon' do + expect(core_status).to receive(:icon) + + subject.icon + end + end + + describe '.matches?' do + context 'build is playable' do + context 'when build stops an environment' do + let(:build) do + create(:ci_build, :playable, :teardown_environment) + end + + it 'is a correct match' do + expect(described_class.matches?(build, user)) + .to be true + end + end + + context 'when build does not stop an environment' do + let(:build) { create(:ci_build, :playable) } + + it 'does not match' do + expect(described_class.matches?(build, user)) + .to be false + end + end + end + + context 'when build is not playable' do + let(:build) { create(:ci_build) } + + it 'does not match' do + expect(described_class.matches?(build, user)) + .to be false + end + end + end +end -- cgit v1.2.1 From 48ea142819f206bb005184a050e812966cd23157 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 12 Dec 2016 15:14:31 +0100 Subject: Fix pipeline specs for detailed statuses [ci skip] --- spec/models/ci/pipeline_spec.rb | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 8158e71dd55..e78ae14b737 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -442,11 +442,15 @@ describe Ci::Pipeline, models: true do end describe '#detailed_status' do + let(:user) { create(:user) } + + subject { pipeline.detailed_status(user) } + context 'when pipeline is created' do let(:pipeline) { create(:ci_pipeline, status: :created) } it 'returns detailed status for created pipeline' do - expect(pipeline.detailed_status.text).to eq 'created' + expect(subject.text).to eq 'created' end end @@ -454,7 +458,7 @@ describe Ci::Pipeline, models: true do let(:pipeline) { create(:ci_pipeline, status: :pending) } it 'returns detailed status for pending pipeline' do - expect(pipeline.detailed_status.text).to eq 'pending' + expect(subject.text).to eq 'pending' end end @@ -462,7 +466,7 @@ describe Ci::Pipeline, models: true do let(:pipeline) { create(:ci_pipeline, status: :running) } it 'returns detailed status for running pipeline' do - expect(pipeline.detailed_status.text).to eq 'running' + expect(subject.text).to eq 'running' end end @@ -470,7 +474,7 @@ describe Ci::Pipeline, models: true do let(:pipeline) { create(:ci_pipeline, status: :success) } it 'returns detailed status for successful pipeline' do - expect(pipeline.detailed_status.text).to eq 'passed' + expect(subject.text).to eq 'passed' end end @@ -478,7 +482,7 @@ describe Ci::Pipeline, models: true do let(:pipeline) { create(:ci_pipeline, status: :failed) } it 'returns detailed status for failed pipeline' do - expect(pipeline.detailed_status.text).to eq 'failed' + expect(subject.text).to eq 'failed' end end @@ -486,7 +490,7 @@ describe Ci::Pipeline, models: true do let(:pipeline) { create(:ci_pipeline, status: :canceled) } it 'returns detailed status for canceled pipeline' do - expect(pipeline.detailed_status.text).to eq 'canceled' + expect(subject.text).to eq 'canceled' end end @@ -494,7 +498,7 @@ describe Ci::Pipeline, models: true do let(:pipeline) { create(:ci_pipeline, status: :skipped) } it 'returns detailed status for skipped pipeline' do - expect(pipeline.detailed_status.text).to eq 'skipped' + expect(subject.text).to eq 'skipped' end end @@ -506,7 +510,7 @@ describe Ci::Pipeline, models: true do end it 'retruns detailed status for successful pipeline with warnings' do - expect(pipeline.detailed_status.label).to eq 'passed with warnings' + expect(subject.label).to eq 'passed with warnings' end end end -- cgit v1.2.1 From 314c4746bc24a31efe88b142cd83ab36c3473cc9 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Mon, 12 Dec 2016 16:16:51 +0200 Subject: Specs for Bitbucket::Connections and Bitbucket::Collections --- lib/bitbucket/connection.rb | 20 +++++++---------- lib/bitbucket/paginator.rb | 4 +--- spec/lib/bitbucket/collection_spec.rb | 23 +++++++++++++++++++ spec/lib/bitbucket/connection_spec.rb | 26 ++++++++++++++++++++++ .../bitbucket_import/project_creator_spec.rb | 2 ++ 5 files changed, 60 insertions(+), 15 deletions(-) create mode 100644 spec/lib/bitbucket/collection_spec.rb create mode 100644 spec/lib/bitbucket/connection_spec.rb diff --git a/lib/bitbucket/connection.rb b/lib/bitbucket/connection.rb index 692a596c057..c150a20761e 100644 --- a/lib/bitbucket/connection.rb +++ b/lib/bitbucket/connection.rb @@ -15,18 +15,6 @@ module Bitbucket @refresh_token = options[:refresh_token] end - def client - @client ||= OAuth2::Client.new(provider.app_id, provider.app_secret, options) - end - - def connection - @connection ||= OAuth2::AccessToken.new(client, @token, refresh_token: @refresh_token, expires_at: @expires_at, expires_in: @expires_in) - end - - def set_default_query_parameters(params = {}) - @default_query.merge!(params) - end - def get(path, extra_query = {}) refresh! if expired? @@ -52,6 +40,14 @@ module Bitbucket attr_reader :expires_at, :expires_in, :refresh_token, :token + def client + @client ||= OAuth2::Client.new(provider.app_id, provider.app_secret, options) + end + + def connection + @connection ||= OAuth2::AccessToken.new(client, @token, refresh_token: @refresh_token, expires_at: @expires_at, expires_in: @expires_in) + end + def build_url(path) return path if path.starts_with?(root_url) diff --git a/lib/bitbucket/paginator.rb b/lib/bitbucket/paginator.rb index 641a6ed79d6..b38cd99855c 100644 --- a/lib/bitbucket/paginator.rb +++ b/lib/bitbucket/paginator.rb @@ -7,8 +7,6 @@ module Bitbucket @type = type @url = url @page = nil - - connection.set_default_query_parameters(pagelen: PAGE_LENGTH, sort: :created_on) end def items @@ -31,7 +29,7 @@ module Bitbucket end def fetch_next_page - parsed_response = connection.get(next_url) + parsed_response = connection.get(next_url, { pagelen: PAGE_LENGTH, sort: :created_on }) Page.new(parsed_response, type) end end diff --git a/spec/lib/bitbucket/collection_spec.rb b/spec/lib/bitbucket/collection_spec.rb new file mode 100644 index 00000000000..eeed61b0488 --- /dev/null +++ b/spec/lib/bitbucket/collection_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +# Emulates paginator. It returns 2 pages with results +class TestPaginator + def initialize + @current_page = 0 + end + + def items + @current_page += 1 + + raise StopIteration if @current_page > 2 + + ["result_1_page_#{@current_page}", "result_2_page_#{@current_page}"] + end +end + +describe Bitbucket::Collection do + it "iterates paginator" do + collection = described_class.new(TestPaginator.new) + expect(collection.to_a).to match(["result_1_page_1", "result_2_page_1", "result_1_page_2", "result_2_page_2"]) + end +end diff --git a/spec/lib/bitbucket/connection_spec.rb b/spec/lib/bitbucket/connection_spec.rb new file mode 100644 index 00000000000..5242c6fac34 --- /dev/null +++ b/spec/lib/bitbucket/connection_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe Bitbucket::Connection do + describe '#get' do + it 'calls OAuth2::AccessToken::get' do + expect_any_instance_of(OAuth2::AccessToken).to receive(:get).and_return(double(parsed: true)) + connection = described_class.new({}) + connection.get('/users') + end + end + + describe '#expired?' do + it 'calls connection.expired?' do + expect_any_instance_of(OAuth2::AccessToken).to receive(:expired?).and_return(true) + expect(described_class.new({}).expired?).to be_truthy + end + end + + describe '#refresh!' do + it 'calls connection.refresh!' do + response = double(token: nil, expires_at: nil, expires_in: nil, refresh_token: nil) + expect_any_instance_of(OAuth2::AccessToken).to receive(:refresh!).and_return(response) + described_class.new({}).refresh! + end + end +end diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb index bb007949557..b6d052a4612 100644 --- a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe Gitlab::BitbucketImport::ProjectCreator, lib: true do let(:user) { create(:user) } + let(:repo) do double(name: 'Vim', slug: 'vim', @@ -12,6 +13,7 @@ describe Gitlab::BitbucketImport::ProjectCreator, lib: true do visibility_level: Gitlab::VisibilityLevel::PRIVATE, clone_url: 'ssh://git@bitbucket.org/asd/vim.git') end + let(:namespace){ create(:group, owner: user) } let(:token) { "asdasd12345" } let(:secret) { "sekrettt" } -- cgit v1.2.1 From be13fc68e74cd6820a5b175365f72f12de339e57 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 12 Dec 2016 15:17:18 +0100 Subject: Fix detailed status badge for generic commit status [ci skip] --- app/views/ci/status/_badge.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/ci/status/_badge.html.haml b/app/views/ci/status/_badge.html.haml index b1b6e9c2b05..c00ddd183fb 100644 --- a/app/views/ci/status/_badge.html.haml +++ b/app/views/ci/status/_badge.html.haml @@ -5,4 +5,4 @@ - else %span{ class: "ci-status ci-#{status}" } = custom_icon(status.icon) - = detailed_status.text + = status.text -- cgit v1.2.1 From 48f24735a8190f72e9bfdeeccf982a38233efcd7 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Mon, 12 Dec 2016 14:32:03 +0000 Subject: Added GFM tests --- spec/features/issues/gfm_autocomplete_spec.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index c421da97d76..6034ca48db2 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -40,4 +40,12 @@ feature 'GFM autocomplete', feature: true, js: true do expect(page).not_to have_selector('.atwho-view') end + + it 'doesnt open autocomplete after non-word character' do + page.within '.timeline-content-form' do + find('#note_note').native.send_keys("@#{user.username[0..2]}!") + end + + expect(page).not_to have_selector('.atwho-view') + end end -- cgit v1.2.1 From 211f019382c5e3fcf4812199aaf09b850eab09dd Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Mon, 12 Dec 2016 16:30:24 +0200 Subject: Add index to routes table on lower path for postgresql Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- ...0161212142807_add_lower_path_index_to_routes.rb | 22 ++++++++++++++++++++++ db/schema.rb | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20161212142807_add_lower_path_index_to_routes.rb diff --git a/db/migrate/20161212142807_add_lower_path_index_to_routes.rb b/db/migrate/20161212142807_add_lower_path_index_to_routes.rb new file mode 100644 index 00000000000..6958500306f --- /dev/null +++ b/db/migrate/20161212142807_add_lower_path_index_to_routes.rb @@ -0,0 +1,22 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddLowerPathIndexToRoutes < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + return unless Gitlab::Database.postgresql? + + execute 'CREATE INDEX CONCURRENTLY index_on_routes_lower_path ON routes (LOWER(path));' + end + + def down + return unless Gitlab::Database.postgresql? + + remove_index :routes, name: :index_on_routes_lower_path + end +end diff --git a/db/schema.rb b/db/schema.rb index 9c46f573719..ae47456084f 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: 20161202152035) do +ActiveRecord::Schema.define(version: 20161212142807) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" -- cgit v1.2.1 From 7d36c88916717646f583a95118a47a095ab449a0 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 12 Dec 2016 15:20:21 +0100 Subject: Fix detailed status specs for pipeline stage model --- spec/models/ci/stage_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index f232761dba2..8fff38f7cda 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -68,7 +68,9 @@ describe Ci::Stage, models: true do end describe '#detailed_status' do - subject { stage.detailed_status } + let(:user) { create(:user) } + + subject { stage.detailed_status(user) } context 'when build is created' do let!(:stage_build) { create_job(:ci_build, status: :created) } -- cgit v1.2.1 From 20a6183be6abe356c8e301c90536fa3252214127 Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Mon, 12 Dec 2016 13:30:50 +0000 Subject: Update custom hooks docs and set 4.1.0 --- GITLAB_SHELL_VERSION | 2 +- doc/administration/custom_hooks.md | 22 +++++++++++++++------- doc/update/8.14-to-8.15.md | 2 +- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index c4e41f94594..ee74734aa22 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -4.0.3 +4.1.0 diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md index 06291705702..80e5d80aa41 100644 --- a/doc/administration/custom_hooks.md +++ b/doc/administration/custom_hooks.md @@ -44,22 +44,30 @@ as appropriate. ## Chained hooks support -> [Introduced][93] in GitLab Shell 4.1.0. +> [Introduced][93] in GitLab Shell 4.1.0 and GitLab 8.15. -The hooks could be also placed in `hooks/<hook_name>.d` (global) or `custom_hooks/<hook_name>.d` (per project) -directories supporting chained execution of the hooks. +Hooks can be also placed in `hooks/<hook_name>.d` (global) or +`custom_hooks/<hook_name>.d` (per project) directories supporting chained +execution of the hooks. + +To look in a different directory for the global custom hooks (those in +`hooks/<hook_name.d>`), set `custom_hooks_dir` in gitlab-shell config. For +Omnibus installations, this can be set in `gitlab.rb`; and in source +installations, this can be set in `gitlab-shell/config.yml`. The hooks are searched and executed in this order: + 1. `<project>.git/hooks/` - symlink to `gitlab-shell/hooks` global dir 1. `<project>.git/hooks/<hook_name>` - executed by `git` itself, this is `gitlab-shell/hooks/<hook_name>` 1. `<project>.git/custom_hooks/<hook_name>` - per project hook (this is already existing behavior) 1. `<project>.git/custom_hooks/<hook_name>.d/*` - per project hooks -1. `<project>.git/hooks/<hook_name>.d/*` - global hooks: all executable files (minus editor backup files) +1. `<project>.git/hooks/<hook_name>.d/*` OR `<custom_hooks_dir>/<hook_name.d>/*` - global hooks: all executable files (minus editor backup files) -Files in `.d` directories need to be executable and not match the backup file pattern (`*~`). +Files in `.d` directories need to be executable and not match the backup file +pattern (`*~`). -The hooks of the same type are executed in order and execution stops on the first -script exiting with non-zero value. +The hooks of the same type are executed in order and execution stops on the +first script exiting with a non-zero value. ## Custom error messages diff --git a/doc/update/8.14-to-8.15.md b/doc/update/8.14-to-8.15.md index 5556dae2551..4eacab0c890 100644 --- a/doc/update/8.14-to-8.15.md +++ b/doc/update/8.14-to-8.15.md @@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-15-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch --all --tags -sudo -u git -H git checkout v4.0.3 +sudo -u git -H git checkout v4.1.0 ``` ### 6. Update gitlab-workhorse -- cgit v1.2.1 From 3a0fecb4924f1a6dbcc3e61041e0cac95ec3b21b Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Mon, 12 Dec 2016 17:29:25 +0200 Subject: Spec for bitbucket page --- lib/bitbucket/page.rb | 1 + spec/lib/bitbucket/page_spec.rb | 50 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 spec/lib/bitbucket/page_spec.rb diff --git a/lib/bitbucket/page.rb b/lib/bitbucket/page.rb index 8f50f67f84d..2b0a3fe7b1a 100644 --- a/lib/bitbucket/page.rb +++ b/lib/bitbucket/page.rb @@ -23,6 +23,7 @@ module Bitbucket def parse_values(raw, bitbucket_rep_class) return [] unless raw['values'] && raw['values'].is_a?(Array) + bitbucket_rep_class.decorate(raw['values']) end diff --git a/spec/lib/bitbucket/page_spec.rb b/spec/lib/bitbucket/page_spec.rb new file mode 100644 index 00000000000..04d5a0470b1 --- /dev/null +++ b/spec/lib/bitbucket/page_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Bitbucket::Page do + let(:response) { { 'values' => [{ 'username' => 'Ben' }], 'pagelen' => 2, 'next' => '' } } + + before do + # Autoloading hack + Bitbucket::Representation::User.new({}) + end + + describe '#items' do + it 'returns collection of needed objects' do + page = described_class.new(response, :user) + + expect(page.items.first).to be_a(Bitbucket::Representation::User) + expect(page.items.count).to eq(1) + end + end + + describe '#attrs' do + it 'returns attributes' do + page = described_class.new(response, :user) + + expect(page.attrs.keys).to include(:pagelen, :next) + end + end + + describe '#next?' do + it 'returns true' do + page = described_class.new(response, :user) + + expect(page.next?).to be_truthy + end + + it 'returns false' do + response['next'] = nil + page = described_class.new(response, :user) + + expect(page.next?).to be_falsey + end + end + + describe '#next' do + it 'returns next attribute' do + page = described_class.new(response, :user) + + expect(page.next).to eq('') + end + end +end -- cgit v1.2.1 From c45484ba193baa811e50aaa106a2c0ed3721d6e8 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Mon, 12 Dec 2016 18:20:23 +0200 Subject: Spec for Bitbucket::Paginator --- spec/lib/bitbucket/paginator_spec.rb | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 spec/lib/bitbucket/paginator_spec.rb diff --git a/spec/lib/bitbucket/paginator_spec.rb b/spec/lib/bitbucket/paginator_spec.rb new file mode 100644 index 00000000000..2c972da682e --- /dev/null +++ b/spec/lib/bitbucket/paginator_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Bitbucket::Paginator do + let(:last_page) { double(:page, next?: false, items: ['item_2']) } + let(:first_page) { double(:page, next?: true, next: last_page, items: ['item_1']) } + + describe 'items' do + it 'return items and raises StopIteration in the end' do + paginator = described_class.new(nil, nil, nil) + + allow(paginator).to receive(:fetch_next_page).and_return(first_page) + expect(paginator.items).to match(['item_1']) + + allow(paginator).to receive(:fetch_next_page).and_return(last_page) + expect(paginator.items).to match(['item_2']) + + allow(paginator).to receive(:fetch_next_page).and_return(nil) + expect{ paginator.items }.to raise_error(StopIteration) + end + end +end -- cgit v1.2.1 From c1518a3230fb3f59111325c34fa5b13fa53e4767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Mon, 12 Dec 2016 18:17:21 +0100 Subject: Don't require `API::API` in routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `API::API` is autoloaded and shouldn't be required in non-autoloaded code. Otherwise, we get the following dreaded error: A copy of API::Helpers has been removed from the module tree but is still active! Signed-off-by: Rémy Coutable <remy@rymai.me> --- config/routes.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 03b47261e7e..06d565df469 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,5 @@ require 'sidekiq/web' require 'sidekiq/cron/web' -require 'api/api' require 'constraints/group_url_constrainer' Rails.application.routes.draw do -- cgit v1.2.1 From ac00009fccec66a768ae9dffff4b453ad2a6b13f Mon Sep 17 00:00:00 2001 From: Felipe Artur <felipefac@gmail.com> Date: Thu, 8 Dec 2016 18:32:08 -0200 Subject: Allow to delete tag release note --- app/controllers/projects/releases_controller.rb | 9 +++- changelogs/unreleased/issue_13270.yml | 4 ++ .../projects/releases_controller_spec.rb | 55 ++++++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/issue_13270.yml create mode 100644 spec/controllers/projects/releases_controller_spec.rb diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb index 0825a4311cb..2c097cb4d8d 100644 --- a/app/controllers/projects/releases_controller.rb +++ b/app/controllers/projects/releases_controller.rb @@ -10,7 +10,14 @@ class Projects::ReleasesController < Projects::ApplicationController end def update - release.update_attributes(release_params) + # Release belongs to Tag which is not active record object, + # it exists only to save a description to each Tag. + # If description is empty we should destroy the existing record. + if release_params[:description].present? + release.update_attributes(release_params) + else + release.destroy + end redirect_to namespace_project_tag_path(@project.namespace, @project, @tag.name) end diff --git a/changelogs/unreleased/issue_13270.yml b/changelogs/unreleased/issue_13270.yml new file mode 100644 index 00000000000..9c15c436876 --- /dev/null +++ b/changelogs/unreleased/issue_13270.yml @@ -0,0 +1,4 @@ +--- +title: Allow to delete tag release note +merge_request: +author: diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb new file mode 100644 index 00000000000..9fd5c3b85f6 --- /dev/null +++ b/spec/controllers/projects/releases_controller_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe Projects::ReleasesController do + let!(:project) { create(:project) } + let!(:user) { create(:user) } + let!(:release) { create(:release, project: project) } + let!(:tag) { release.tag } + + before do + project.team << [user, :developer] + sign_in(user) + end + + describe 'GET #edit' do + it 'initializes a new release' do + tag_id = release.tag + project.releases.destroy_all + + get :edit, namespace_id: project.namespace.path, project_id: project.path, tag_id: tag_id + + release = assigns(:release) + expect(release).not_to be_nil + expect(release).not_to be_persisted + end + + it 'retrieves an existing release' do + get :edit, namespace_id: project.namespace.path, project_id: project.path, tag_id: release.tag + + release = assigns(:release) + expect(release).not_to be_nil + expect(release).to be_persisted + end + end + + describe 'PUT #update' do + it 'updates release note description' do + update_release('description updated') + + release = project.releases.find_by_tag(tag) + expect(release.description).to eq("description updated") + end + + it 'deletes release note when description is null' do + expect { update_release('') }.to change(project.releases, :count).by(-1) + end + end + + def update_release(description) + put :update, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + tag_id: release.tag, + release: { description: description } + end +end -- cgit v1.2.1 From a2be395401f6320d2722bbd98de0c046d05f0480 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Mon, 12 Dec 2016 20:41:55 +0200 Subject: specs for bitbucket representers --- .../representation/pull_request_comment.rb | 6 +-- spec/lib/bitbucket/representation/comment_spec.rb | 22 ++++++++++ spec/lib/bitbucket/representation/issue_spec.rb | 42 +++++++++++++++++++ .../representation/pull_request_comment_spec.rb | 35 ++++++++++++++++ .../bitbucket/representation/pull_request_spec.rb | 47 ++++++++++++++++++++++ spec/lib/bitbucket/representation/user_spec.rb | 11 +++++ 6 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 spec/lib/bitbucket/representation/comment_spec.rb create mode 100644 spec/lib/bitbucket/representation/issue_spec.rb create mode 100644 spec/lib/bitbucket/representation/pull_request_comment_spec.rb create mode 100644 spec/lib/bitbucket/representation/pull_request_spec.rb create mode 100644 spec/lib/bitbucket/representation/user_spec.rb diff --git a/lib/bitbucket/representation/pull_request_comment.rb b/lib/bitbucket/representation/pull_request_comment.rb index ae2b069d6a2..4f3809fbcea 100644 --- a/lib/bitbucket/representation/pull_request_comment.rb +++ b/lib/bitbucket/representation/pull_request_comment.rb @@ -6,15 +6,15 @@ module Bitbucket end def file_path - inline.fetch('path', nil) + inline.fetch('path') end def old_pos - inline.fetch('from', nil) + inline.fetch('from') end def new_pos - inline.fetch('to', nil) + inline.fetch('to') end def parent_id diff --git a/spec/lib/bitbucket/representation/comment_spec.rb b/spec/lib/bitbucket/representation/comment_spec.rb new file mode 100644 index 00000000000..5864193cbfc --- /dev/null +++ b/spec/lib/bitbucket/representation/comment_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Bitbucket::Representation::Comment do + describe '#author' do + it { expect(described_class.new('user' => { 'username' => 'Ben' }).author).to eq('Ben') } + it { expect(described_class.new({}).author).to eq('Anonymous') } + end + + describe '#note' do + it { expect(described_class.new('content' => { 'raw' => 'Text' }).note).to eq('Text') } + it { expect(described_class.new({}).note).to be_nil } + end + + describe '#created_at' do + it { expect(described_class.new('created_on' => Date.today).created_at).to eq(Date.today) } + end + + describe '#updated_at' do + it { expect(described_class.new('updated_on' => Date.today).updated_at).to eq(Date.today) } + it { expect(described_class.new('created_on' => Date.today).updated_at).to eq(Date.today) } + end +end diff --git a/spec/lib/bitbucket/representation/issue_spec.rb b/spec/lib/bitbucket/representation/issue_spec.rb new file mode 100644 index 00000000000..56deae63bbc --- /dev/null +++ b/spec/lib/bitbucket/representation/issue_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe Bitbucket::Representation::Issue do + describe '#iid' do + it { expect(described_class.new('id' => 1).iid).to eq(1) } + end + + describe '#kind' do + it { expect(described_class.new('kind' => 'bug').kind).to eq('bug') } + end + + describe '#author' do + it { expect(described_class.new({ 'reporter' => { 'username' => 'Ben' }}).author).to eq('Ben') } + it { expect(described_class.new({}).author).to eq('Anonymous') } + end + + describe '#description' do + it { expect(described_class.new({ 'content' => { 'raw' => 'Text' }}).description).to eq('Text') } + it { expect(described_class.new({}).description).to be_nil } + end + + describe '#state' do + it { expect(described_class.new({ 'state' => 'invalid' }).state).to eq('closed') } + it { expect(described_class.new({ 'state' => 'wontfix' }).state).to eq('closed') } + it { expect(described_class.new({ 'state' => 'resolved' }).state).to eq('closed') } + it { expect(described_class.new({ 'state' => 'duplicate' }).state).to eq('closed') } + it { expect(described_class.new({ 'state' => 'closed' }).state).to eq('closed') } + it { expect(described_class.new({ 'state' => 'opened' }).state).to eq('opened') } + end + + describe '#title' do + it { expect(described_class.new('title' => 'Issue').title).to eq('Issue') } + end + + describe '#created_at' do + it { expect(described_class.new('created_on' => Date.today).created_at).to eq(Date.today) } + end + + describe '#updated_at' do + it { expect(described_class.new('edited_on' => Date.today).updated_at).to eq(Date.today) } + end +end diff --git a/spec/lib/bitbucket/representation/pull_request_comment_spec.rb b/spec/lib/bitbucket/representation/pull_request_comment_spec.rb new file mode 100644 index 00000000000..8377f0540cd --- /dev/null +++ b/spec/lib/bitbucket/representation/pull_request_comment_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Bitbucket::Representation::PullRequestComment do + describe '#iid' do + it { expect(described_class.new('id' => 1).iid).to eq(1) } + end + + describe '#file_path' do + it { expect(described_class.new('inline' => { 'path' => '/path' }).file_path).to eq('/path') } + end + + describe '#old_pos' do + it { expect(described_class.new('inline' => { 'from' => 3 }).old_pos).to eq(3) } + end + + describe '#new_pos' do + it { expect(described_class.new('inline' => { 'to' => 3 }).new_pos).to eq(3) } + end + + + describe '#parent_id' do + it { expect(described_class.new({ 'parent' => { 'id' => 2 }}).parent_id).to eq(2) } + it { expect(described_class.new({}).parent_id).to be_nil } + end + + describe '#inline?' do + it { expect(described_class.new('inline' => {}).inline?).to be_truthy } + it { expect(described_class.new({}).inline?).to be_falsey } + end + + describe '#has_parent?' do + it { expect(described_class.new('parent' => {}).has_parent?).to be_truthy } + it { expect(described_class.new({}).has_parent?).to be_falsey } + end +end diff --git a/spec/lib/bitbucket/representation/pull_request_spec.rb b/spec/lib/bitbucket/representation/pull_request_spec.rb new file mode 100644 index 00000000000..661422efddf --- /dev/null +++ b/spec/lib/bitbucket/representation/pull_request_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Bitbucket::Representation::PullRequest do + describe '#iid' do + it { expect(described_class.new('id' => 1).iid).to eq(1) } + end + + describe '#author' do + it { expect(described_class.new({ 'author' => { 'username' => 'Ben' }}).author).to eq('Ben') } + it { expect(described_class.new({}).author).to eq('Anonymous') } + end + + describe '#description' do + it { expect(described_class.new({ 'description' => 'Text' }).description).to eq('Text') } + it { expect(described_class.new({}).description).to be_nil } + end + + describe '#state' do + it { expect(described_class.new({ 'state' => 'MERGED' }).state).to eq('merged') } + it { expect(described_class.new({ 'state' => 'DECLINED' }).state).to eq('closed') } + it { expect(described_class.new({}).state).to eq('opened') } + end + + describe '#title' do + it { expect(described_class.new('title' => 'Issue').title).to eq('Issue') } + end + + describe '#source_branch_name' do + it { expect(described_class.new({ source: { branch: { name: 'feature' } } }.with_indifferent_access).source_branch_name).to eq('feature') } + it { expect(described_class.new({ source: {} }.with_indifferent_access).source_branch_name).to be_nil } + end + + describe '#source_branch_sha' do + it { expect(described_class.new({ source: { commit: { hash: 'abcd123' } } }.with_indifferent_access).source_branch_sha).to eq('abcd123') } + it { expect(described_class.new({ source: {} }.with_indifferent_access).source_branch_sha).to be_nil } + end + + describe '#target_branch_name' do + it { expect(described_class.new({ destination: { branch: { name: 'master' } } }.with_indifferent_access).target_branch_name).to eq('master') } + it { expect(described_class.new({ destination: {} }.with_indifferent_access).target_branch_name).to be_nil } + end + + describe '#target_branch_sha' do + it { expect(described_class.new({ destination: { commit: { hash: 'abcd123' } } }.with_indifferent_access).target_branch_sha).to eq('abcd123') } + it { expect(described_class.new({ destination: {} }.with_indifferent_access).target_branch_sha).to be_nil } + end +end diff --git a/spec/lib/bitbucket/representation/user_spec.rb b/spec/lib/bitbucket/representation/user_spec.rb new file mode 100644 index 00000000000..f79ff4edb7b --- /dev/null +++ b/spec/lib/bitbucket/representation/user_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe Bitbucket::Representation::User do + describe '#username' do + it 'returns correct value' do + user = described_class.new('username' => 'Ben') + + expect(user.username).to eq('Ben') + end + end +end -- cgit v1.2.1 From e8d1c7a5b6b942f591539531bfe48dc72b3cd16b Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Mon, 12 Dec 2016 13:03:22 -0600 Subject: break comment "edited at" block out of the note-text element to prevent issues with inline text nodes --- 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 ba8895438c5..778a32e6345 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -65,7 +65,7 @@ .note-text.md = preserve do = note.redacted_note_html - = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) + = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) - if note_editable = render 'projects/notes/edit_form', note: note .note-awards -- cgit v1.2.1 From 98d42e9f85b0636632b77a67d1a20d64eb6ed22a Mon Sep 17 00:00:00 2001 From: Matt Lee <mattl@gitlab.com> Date: Fri, 9 Dec 2016 21:21:29 +0000 Subject: 8.14 requires GitLab shell 4.0.3 --- doc/update/8.13-to-8.14.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/update/8.13-to-8.14.md b/doc/update/8.13-to-8.14.md index a0e895773ce..c64d3407461 100644 --- a/doc/update/8.13-to-8.14.md +++ b/doc/update/8.13-to-8.14.md @@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-14-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch --all --tags -sudo -u git -H git checkout v4.0.0 +sudo -u git -H git checkout v4.0.3 ``` ### 6. Update gitlab-workhorse -- cgit v1.2.1 From 569645a809d042fb7a69d5c95ad65fe6983c2f75 Mon Sep 17 00:00:00 2001 From: victorwu <victor@gitlab.com> Date: Fri, 9 Dec 2016 15:48:23 -0600 Subject: Clean up commit copy to clipboard and make consistent --- app/assets/javascripts/copy_to_clipboard.js | 4 ++-- app/helpers/button_helper.rb | 2 +- app/views/projects/commit/_commit_box.html.haml | 9 ++------- app/views/projects/tree/_tree_content.html.haml | 6 +----- 4 files changed, 6 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js index 1cc34e490c2..efa228a75d9 100644 --- a/app/assets/javascripts/copy_to_clipboard.js +++ b/app/assets/javascripts/copy_to_clipboard.js @@ -6,7 +6,7 @@ var genericError, genericSuccess, showTooltip; genericSuccess = function(e) { - showTooltip(e.trigger, 'Copied!'); + showTooltip(e.trigger, 'Copied'); // Clear the selection and blur the trigger so it loses its border e.clearSelection(); return $(e.trigger).blur(); @@ -31,7 +31,7 @@ var originalTitle = $target.data('original-title'); $target - .attr('title', 'Copied!') + .attr('title', 'Copied') .tooltip('fixTitle') .tooltip('show') .attr('title', originalTitle) diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index dee3c78df47..4c7c16d694c 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -16,7 +16,7 @@ module ButtonHelper # See http://clipboardjs.com/#usage def clipboard_button(data = {}) css_class = data[:class] || 'btn-clipboard btn-transparent' - title = data[:title] || 'Copy to Clipboard' + title = data[:title] || 'Copy to clipboard' data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data) content_tag :button, icon('clipboard'), diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 65151ac3a56..c08ed8f6c16 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -1,13 +1,8 @@ .page-content-header .header-main-content - %strong Commit - %strong.monospace.js-details-short= @commit.short_id - = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do - %span.text-expander - \... - %span.js-details-content.hide - %strong.monospace.commit-hash-full= @commit.id + %strong = clipboard_button(clipboard_text: @commit.id) + = @commit.short_id %span.hidden-xs authored #{time_ago_with_tooltip(@commit.authored_date)} %span by diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index 21e378b8735..dc7b4bab208 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -5,14 +5,10 @@ %tr %th Name %th.hidden-xs - .pull-left Last Commit .last-commit.hidden-sm.pull-left -   - %i.fa.fa-angle-right -   %small.light + = clipboard_button(clipboard_text: @commit.id) = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" - – = time_ago_with_tooltip(@commit.committed_date) = @commit.full_title %small.commit-history-link-spacer | -- cgit v1.2.1 From a6ca5689b0df3a020a9cf9e7f9b9ed1a76863ec1 Mon Sep 17 00:00:00 2001 From: victorwu <victor@gitlab.com> Date: Fri, 9 Dec 2016 16:48:53 -0600 Subject: Tweak style and add back wording --- app/views/projects/tree/_tree_content.html.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index dc7b4bab208..d37c376c36b 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -5,8 +5,9 @@ %tr %th Name %th.hidden-xs + .pull-left Last commit .last-commit.hidden-sm.pull-left - %small.light + %small.light = clipboard_button(clipboard_text: @commit.id) = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" = time_ago_with_tooltip(@commit.committed_date) -- cgit v1.2.1 From 602447a399b831c16023af9701db60e38931e77d Mon Sep 17 00:00:00 2001 From: Victor Wu <victor@gitlab.com> Date: Sat, 10 Dec 2016 00:55:16 +0000 Subject: Fix test --- spec/views/projects/commit/_commit_box.html.haml_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/views/projects/commit/_commit_box.html.haml_spec.rb b/spec/views/projects/commit/_commit_box.html.haml_spec.rb index 16bf0698c4b..e741e3cf9b6 100644 --- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb +++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb @@ -13,7 +13,7 @@ describe 'projects/commit/_commit_box.html.haml' do it 'shows the commit SHA' do render - expect(rendered).to have_text("Commit #{Commit.truncate_sha(project.commit.sha)}") + expect(rendered).to have_text("#{Commit.truncate_sha(project.commit.sha)}") end it 'shows the last pipeline that ran for the commit' do -- cgit v1.2.1 From 53dc398bd608dc0e1c731aa62e275197b6a2345d Mon Sep 17 00:00:00 2001 From: Jon Bailey <hobart@gmail.com> Date: Mon, 12 Dec 2016 20:31:04 +0000 Subject: Crontab typo '* */6' -> '0 */6' (4x/day not 1x-per-min-for-1h 4x/day) --- 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 9ddd1554811..0ee1b1ec634 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -302,7 +302,7 @@ Settings.cron_jobs['remove_expired_group_links_worker'] ||= Settingslogic.new({} Settings.cron_jobs['remove_expired_group_links_worker']['cron'] ||= '10 0 * * *' Settings.cron_jobs['remove_expired_group_links_worker']['job_class'] = 'RemoveExpiredGroupLinksWorker' Settings.cron_jobs['prune_old_events_worker'] ||= Settingslogic.new({}) -Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '* */6 * * *' +Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '0 */6 * * *' Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker' Settings.cron_jobs['trending_projects_worker'] ||= Settingslogic.new({}) -- cgit v1.2.1 From 1de2edc1f14a805afa23c6fde79bdd499af75f33 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Mon, 12 Dec 2016 21:09:17 +0000 Subject: Fix pipeline stroke position --- app/assets/stylesheets/pages/pipelines.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 75b127eb7f6..4a5b0b5922c 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -427,7 +427,7 @@ width: 21px; height: 25px; position: absolute; - top: -31px; + top: -30px; border-top: 2px solid $border-color; } -- cgit v1.2.1 From f21e13ae194cc6669100b663d757124612fe7c28 Mon Sep 17 00:00:00 2001 From: winniehell <git@winniehell.de> Date: Tue, 22 Nov 2016 13:49:06 +0100 Subject: Replace static fixture for awards_handler_spec (!7661) --- changelogs/unreleased/awards_handler.yml | 4 ++ spec/javascripts/awards_handler_spec.js | 10 ++--- spec/javascripts/fixtures/awards_handler.html.haml | 52 ---------------------- 3 files changed, 9 insertions(+), 57 deletions(-) create mode 100644 changelogs/unreleased/awards_handler.yml delete mode 100644 spec/javascripts/fixtures/awards_handler.html.haml diff --git a/changelogs/unreleased/awards_handler.yml b/changelogs/unreleased/awards_handler.yml new file mode 100644 index 00000000000..1f9904c0691 --- /dev/null +++ b/changelogs/unreleased/awards_handler.yml @@ -0,0 +1,4 @@ +--- +title: Replace static fixture for awards_handler_spec +merge_request: 7661 +author: winniehell diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index ac1404f6e1c..7b2e3db6218 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -33,9 +33,9 @@ }; describe('AwardsHandler', function() { - fixture.preload('awards_handler.html'); + fixture.preload('issues/open-issue.html.raw'); beforeEach(function() { - fixture.load('awards_handler.html'); + fixture.load('issues/open-issue.html.raw'); awardsHandler = new AwardsHandler; spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) { return function(url, emoji, cb) { @@ -113,7 +113,7 @@ }); describe('::getAwardUrl', function() { return it('should return the url for request', function() { - return expect(awardsHandler.getAwardUrl()).toBe('/gitlab-org/gitlab-test/issues/8/toggle_award_emoji'); + return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/issues-project/issues/1/toggle_award_emoji'); }); }); describe('::addAward and ::checkMutuality', function() { @@ -209,7 +209,7 @@ $('.js-add-award').eq(0).click(); $menu = $('.emoji-menu'); $block = $('.js-awards-block'); - $emoji = $menu.find(".emoji-menu-list-item " + selector); + $emoji = $menu.find('.emoji-menu-list:not(.frequent-emojis) ' + selector); expect($emoji.length).toBe(1); expect($block.find(selector).length).toBe(0); $emoji.click(); @@ -224,7 +224,7 @@ openEmojiMenuAndAddEmoji(); $('.js-add-award').eq(0).click(); $block = $('.js-awards-block'); - $emoji = $('.emoji-menu').find(".emoji-menu-list-item " + selector); + $emoji = $('.emoji-menu').find(".emoji-menu-list:not(.frequent-emojis) " + selector); $emoji.click(); return expect($block.find(selector).length).toBe(0); }); diff --git a/spec/javascripts/fixtures/awards_handler.html.haml b/spec/javascripts/fixtures/awards_handler.html.haml deleted file mode 100644 index 1ef2e8f8624..00000000000 --- a/spec/javascripts/fixtures/awards_handler.html.haml +++ /dev/null @@ -1,52 +0,0 @@ -.issue-details.issuable-details - .detail-page-description.content-block - %h2.title Quibusdam sint officiis earum molestiae ipsa autem voluptatem nisi rem. - .description.js-task-list-container.is-task-list-enabled - .wiki - %p Qui exercitationem magnam optio quae fuga earum odio. - %textarea.hidden.js-task-list-field Qui exercitationem magnam optio quae fuga earum odio. - %small.edited-text - .content-block.content-block-small - .awards.js-awards-block{"data-award-url" => "/gitlab-org/gitlab-test/issues/8/toggle_award_emoji"} - %button.award-control.btn.js-emoji-btn{"data-placement" => "bottom", "data-title" => "", :type => "button"} - .icon.emoji-icon.emoji-1F44D{"data-aliases" => "", "data-emoji" => "thumbsup", "data-unicode-name" => "1F44D", :title => "thumbsup"} - %span.award-control-text.js-counter 0 - %button.award-control.btn.js-emoji-btn{"data-placement" => "bottom", "data-title" => "", :type => "button"} - .icon.emoji-icon.emoji-1F44E{"data-aliases" => "", "data-emoji" => "thumbsdown", "data-unicode-name" => "1F44E", :title => "thumbsdown"} - %span.award-control-text.js-counter 0 - .award-menu-holder.js-award-holder - %button.btn.award-control.js-add-award{:type => "button"} - %i.fa.fa-smile-o.award-control-icon.award-control-icon-normal - %i.fa.fa-spinner.fa-spin.award-control-icon.award-control-icon-loading - %span.award-control-text Add - %section.issuable-discussion - #notes - %ul#notes-list.notes.main-notes-list.timeline - %li#note_348.note.note-row-348.timeline-entry{"data-author-id" => "18", "data-editable" => ""} - .timeline-entry-inner - .timeline-icon - %a{:href => "/u/agustin"} - %img.avatar.s40{:alt => "", :src => "#"}/ - .timeline-content - .note-header - %a.author_link{:href => "/u/agustin"} - %span.author Brenna Stokes - .inline.note-headline-light - @agustin commented - %a{:href => "#note_348"} - %time 11 days ago - .note-actions - %span.note-role Reporter - %a.note-action-button.note-emoji-button.js-add-award.js-note-emoji{"data-position" => "right", :href => "#", :title => "Award Emoji"} - %i.fa.fa-spinner.fa-spin - %i.fa.fa-smile-o.link-highlight - .js-task-list-container.note-body.is-task-list-enabled - .note-text - %p Suscipit sunt quia quisquam sed eveniet ipsam. - .note-awards - .awards.hidden.js-awards-block{"data-award-url" => "/gitlab-org/gitlab-test/notes/348/toggle_award_emoji"} - .award-menu-holder.js-award-holder - %button.btn.award-control.js-add-award{:type => "button"} - %i.fa.fa-smile-o.award-control-icon.award-control-icon-normal - %i.fa.fa-spinner.fa-spin.award-control-icon.award-control-icon-loading - %span.award-control-text Add -- cgit v1.2.1 From 2cbd07e69c647726cfb927bf35a594850d4f22fa Mon Sep 17 00:00:00 2001 From: Robert Schilling <rschilling@student.tugraz.at> Date: Wed, 30 Nov 2016 17:12:43 +0100 Subject: Don't allow blank MR titles in API --- lib/api/merge_requests.rb | 4 ++-- spec/requests/api/merge_requests_spec.rb | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 55bdbc6a47c..5d1fe22f2df 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -143,8 +143,8 @@ module API success Entities::MergeRequest end params do - optional :title, type: String, desc: 'The title of the merge request' - optional :target_branch, type: String, desc: 'The target branch' + optional :title, type: String, allow_blank: false, desc: 'The title of the merge request' + optional :target_branch, type: String, allow_blank: false, desc: 'The target branch' optional :state_event, type: String, values: %w[close reopen merge], desc: 'Status of the merge request' use :optional_params diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 75b270aa93c..f032d1b683d 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -533,6 +533,22 @@ describe API::MergeRequests, api: true do expect(json_response['labels']).to include '?' expect(json_response['labels']).to include '&' end + + it 'does not update state when title is empty' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', title: nil + + merge_request.reload + expect(response).to have_http_status(400) + expect(merge_request.state).to eq('opened') + end + + it 'does not update state when target_branch is empty' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', target_branch: nil + + merge_request.reload + expect(response).to have_http_status(400) + expect(merge_request.state).to eq('opened') + end end describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do -- cgit v1.2.1 From c16b24af04643edc598b20b338eff25e463ed29b Mon Sep 17 00:00:00 2001 From: winniehell <git@winniehell.de> Date: Tue, 13 Dec 2016 02:10:30 +0100 Subject: Add failing test for #20190 --- .../projects/files/creating_a_file_spec.rb | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 spec/features/projects/files/creating_a_file_spec.rb diff --git a/spec/features/projects/files/creating_a_file_spec.rb b/spec/features/projects/files/creating_a_file_spec.rb new file mode 100644 index 00000000000..ae448706130 --- /dev/null +++ b/spec/features/projects/files/creating_a_file_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +feature 'User wants to create a file', feature: true do + include WaitForAjax + + let(:project) { create(:project) } + let(:user) { create(:user) } + + background do + project.team << [user, :master] + login_as user + visit namespace_project_new_blob_path(project.namespace, project, project.default_branch) + end + + def submit_new_file(options) + file_name = find('#file_name') + file_name.set options[:file_name] || 'README.md' + + file_content = find('#file-content') + file_content.set options[:file_content] || 'Some content' + + click_button 'Commit Changes' + end + + scenario 'file name contains Chinese characters' do + submit_new_file(file_name: '测试.md') + expect(page).to have_content 'The file has been successfully created.' + end + + scenario 'directory name contains Chinese characters' do + submit_new_file(file_name: '中文/测试.md') + expect(page).to have_content 'The file has been successfully created.' + end + + scenario 'file name contains invalid characters' do + submit_new_file(file_name: '\\') + expect(page).to have_content 'Your changes could not be committed, because the file name can contain only' + end + + scenario 'file name contains directory traversal' do + submit_new_file(file_name: '../README.md') + expect(page).to have_content 'Your changes could not be committed, because the file name cannot include directory traversal.' + end +end -- cgit v1.2.1 From 61aa90ef2089c9d840b88f14a553ef0dd49b779f Mon Sep 17 00:00:00 2001 From: winniehell <git@winniehell.de> Date: Thu, 8 Dec 2016 22:53:15 +0100 Subject: Allow all alphanumeric characters in file names (!8002) --- changelogs/unreleased/allow-more-filenames.yml | 4 ++++ lib/gitlab/regex.rb | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/allow-more-filenames.yml diff --git a/changelogs/unreleased/allow-more-filenames.yml b/changelogs/unreleased/allow-more-filenames.yml new file mode 100644 index 00000000000..7989f94e528 --- /dev/null +++ b/changelogs/unreleased/allow-more-filenames.yml @@ -0,0 +1,4 @@ +--- +title: Allow all alphanumeric characters in file names +merge_request: 8002 +author: winniehell diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index a06cf6a989c..d9d1e3cccca 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -61,7 +61,7 @@ module Gitlab end def file_name_regex - @file_name_regex ||= /\A[a-zA-Z0-9_\-\.\@]*\z/.freeze + @file_name_regex ||= /\A[[[:alnum:]]_\-\.\@]*\z/.freeze end def file_name_regex_message @@ -69,7 +69,7 @@ module Gitlab end def file_path_regex - @file_path_regex ||= /\A[a-zA-Z0-9_\-\.\/\@]*\z/.freeze + @file_path_regex ||= /\A[[[:alnum:]]_\-\.\/\@]*\z/.freeze end def file_path_regex_message -- cgit v1.2.1 From 605715067767572ff96964370d78e7b31083ddde Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Mon, 28 Nov 2016 11:34:02 +0000 Subject: Adds manual action icon and case to show it --- app/helpers/ci_status_helper.rb | 2 ++ app/views/shared/icons/_icon_status_manual.svg | 1 + 2 files changed, 3 insertions(+) create mode 100755 app/views/shared/icons/_icon_status_manual.svg diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index d9f5e01f0dc..eb2aeaa4628 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -48,6 +48,8 @@ module CiStatusHelper 'icon_status_created' when 'skipped' 'icon_status_skipped' + when 'manual' + 'icon_status_manual' else 'icon_status_canceled' end diff --git a/app/views/shared/icons/_icon_status_manual.svg b/app/views/shared/icons/_icon_status_manual.svg new file mode 100755 index 00000000000..ffb00c6f443 --- /dev/null +++ b/app/views/shared/icons/_icon_status_manual.svg @@ -0,0 +1 @@ +<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M17.8571429,10 C17.8571429,5.71428571 14.2857143,2.14285714 10,2.14285714 C5.71428571,2.14285714 2.14285714,5.71428571 2.14285714,10 C2.14285714,14.2857143 5.71428571,17.8571429 10,17.8571429 C14.2857143,17.8571429 17.8571429,14.2857143 17.8571429,10 M0,10 C0,4.42857143 4.42857143,0 10,0 C15.5714286,0 20,4.42857143 20,10 C20,15.5714286 15.5714286,20 10,20 C4.42857143,20 0,15.5714286 0,10"/><path d="M15,10.9006211 L15,9.09937888 L13.875,8.91304348 C13.8125,8.66459627 13.6875,8.41614907 13.5,8.04347826 L14.1875,7.11180124 L12.875,5.80745342 L11.9375,6.49068323 C11.625,6.30434783 11.3125,6.18012422 11.0625,6.11801242 L10.9375,5 L9.125,5 L8.9375,6.11801242 C8.625,6.18012422 8.375,6.30434783 8.0625,6.49068323 L7.125,5.80745342 L5.8125,7.11180124 L6.4375,8.04347826 C6.25,8.35403727 6.1875,8.60248447 6.0625,8.91304348 L5,9.03726708 L5,10.8385093 L6.125,11.0248447 C6.1875,11.3354037 6.3125,11.5838509 6.5,11.8944099 L5.875,12.826087 L7.1875,14.1304348 L8.125,13.5093168 C8.375,13.6335404 8.6875,13.757764 9,13.8819876 L9.1875,15 L11.0625,15 L11.25,13.8819876 C11.5625,13.8198758 11.8125,13.6956522 12.125,13.5093168 L13.0625,14.1925466 L14.375,12.8881988 L13.6875,11.9565217 C13.875,11.6459627 14,11.3354037 14.0625,11.0869565 L15,10.9006211 L15,10.9006211 Z M10,11.8322981 C8.9375,11.8322981 8.125,11.0248447 8.125,9.9689441 C8.125,8.91304348 8.9375,8.10559006 10,8.10559006 C11.0625,8.10559006 11.875,8.91304348 11.875,9.9689441 C11.875,11.0248447 11.0625,11.8322981 10,11.8322981 L10,11.8322981 Z"/></svg> -- cgit v1.2.1 From 2011f8f1c2b4788df04fd76dcab816ab337e9e08 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 13 Dec 2016 11:53:36 +0100 Subject: Use manual build icon in play/stop build statuses --- app/helpers/ci_status_helper.rb | 2 -- lib/gitlab/ci/status/build/play.rb | 8 ++++++-- lib/gitlab/ci/status/build/stop.rb | 8 ++++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index eb2aeaa4628..d9f5e01f0dc 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -48,8 +48,6 @@ module CiStatusHelper 'icon_status_created' when 'skipped' 'icon_status_skipped' - when 'manual' - 'icon_status_manual' else 'icon_status_canceled' end diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb index e3066d40a37..974b9e9b1f9 100644 --- a/lib/gitlab/ci/status/build/play.rb +++ b/lib/gitlab/ci/status/build/play.rb @@ -6,11 +6,15 @@ module Gitlab include Status::Extended def text - 'play' + 'manual' end def label - 'play' + 'manual play action' + end + + def icon + 'icon_status_manual' end def has_action? diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb index a4966a55892..f8ffa95cde4 100644 --- a/lib/gitlab/ci/status/build/stop.rb +++ b/lib/gitlab/ci/status/build/stop.rb @@ -6,11 +6,15 @@ module Gitlab include Status::Extended def text - 'stop' + 'manual' end def label - 'stop' + 'manual stop action' + end + + def icon + 'icon_status_manual' end def has_action? -- cgit v1.2.1 From 7caab6c2ae6e8063472a224d846f9157c07dc53f Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Tue, 13 Dec 2016 11:04:43 +0000 Subject: Fix broken test in chrome --- spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6 b/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6 index 1c5bf8887f8..3645dd70c55 100644 --- a/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6 +++ b/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6 @@ -11,7 +11,7 @@ describe('Custom Event Polyfill', () => { expect(e.type).toEqual('foo'); expect(e.bubbles).toBe(false); expect(e.cancelable).toBe(false); - expect(e.detail).toBe(undefined); + expect(e.detail).toBeFalsy(); }); it('should create a `CustomEvent` instance with a `details` object', () => { @@ -29,7 +29,7 @@ describe('Custom Event Polyfill', () => { expect(e.type).toEqual('bar'); expect(e.bubbles).toBe(true); expect(e.cancelable).toBe(false); - expect(e.detail).toBe(undefined); + expect(e.detail).toBeFalsy(); }); it('should create a `CustomEvent` instance with a `cancelable` boolean', () => { @@ -38,6 +38,6 @@ describe('Custom Event Polyfill', () => { expect(e.type).toEqual('bar'); expect(e.bubbles).toBe(false); expect(e.cancelable).toBe(true); - expect(e.detail).toBe(undefined); + expect(e.detail).toBeFalsy(); }); }); -- cgit v1.2.1 From 48d43608b8e9118033c1701c98f330aa10f9eb54 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 13 Dec 2016 12:27:01 +0100 Subject: Refine build stop/play extended status specs --- lib/gitlab/ci/status/build/play.rb | 8 +++---- spec/lib/gitlab/ci/status/build/play_spec.rb | 27 ++++++++------------- spec/lib/gitlab/ci/status/build/stop_spec.rb | 35 +++++++++++++++++----------- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb index 974b9e9b1f9..5c506e6d59f 100644 --- a/lib/gitlab/ci/status/build/play.rb +++ b/lib/gitlab/ci/status/build/play.rb @@ -21,14 +21,14 @@ module Gitlab can?(user, :update_build, subject) end - def action_title - 'Play' - end - def action_icon 'play' end + def action_title + 'Play' + end + def action_class 'ci-play-icon' end diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb index ae103d8993d..180a2808a42 100644 --- a/spec/lib/gitlab/ci/status/build/play_spec.rb +++ b/spec/lib/gitlab/ci/status/build/play_spec.rb @@ -1,30 +1,26 @@ require 'spec_helper' describe Gitlab::Ci::Status::Build::Play do - let(:core_status) { double('core status') } + let(:status) { double('core') } let(:user) { double('user') } - subject do - described_class.new(core_status) - end + subject { described_class.new(status) } describe '#text' do - it { expect(subject.text).to eq 'play' } + it { expect(subject.text).to eq 'manual' } end describe '#label' do - it { expect(subject.label).to eq 'play' } + it { expect(subject.label).to eq 'manual play action' } end describe '#icon' do - it 'does not override core status icon' do - expect(core_status).to receive(:icon) - - subject.icon - end + it { expect(subject.icon).to eq 'icon_status_manual' } end describe '.matches?' do + subject { described_class.matches?(build, user) } + context 'build is playable' do context 'when build stops an environment' do let(:build) do @@ -32,8 +28,7 @@ describe Gitlab::Ci::Status::Build::Play do end it 'does not match' do - expect(described_class.matches?(build, user)) - .to be false + expect(subject).to be false end end @@ -41,8 +36,7 @@ describe Gitlab::Ci::Status::Build::Play do let(:build) { create(:ci_build, :playable) } it 'is a correct match' do - expect(described_class.matches?(build, user)) - .to be true + expect(subject).to be true end end end @@ -51,8 +45,7 @@ describe Gitlab::Ci::Status::Build::Play do let(:build) { create(:ci_build) } it 'does not match' do - expect(described_class.matches?(build, user)) - .to be false + expect(subject).to be false end end end diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb index f2121210417..bc1939fb493 100644 --- a/spec/lib/gitlab/ci/status/build/stop_spec.rb +++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb @@ -1,30 +1,40 @@ require 'spec_helper' describe Gitlab::Ci::Status::Build::Stop do - let(:core_status) { double('core status') } + let(:status) { double('core status') } let(:user) { double('user') } subject do - described_class.new(core_status) + described_class.new(status) end describe '#text' do - it { expect(subject.text).to eq 'stop' } + it { expect(subject.text).to eq 'manual' } end describe '#label' do - it { expect(subject.label).to eq 'stop' } + it { expect(subject.label).to eq 'manual stop action' } end describe '#icon' do - it 'does not override core status icon' do - expect(core_status).to receive(:icon) + it { expect(subject.icon).to eq 'icon_status_manual' } + end - subject.icon - end + describe '#has_action?' do + end + + describe '#action_icon' do + end + + describe '#action_path' do + end + + describe '#action_title' do end describe '.matches?' do + subject { described_class.matches?(build, user) } + context 'build is playable' do context 'when build stops an environment' do let(:build) do @@ -32,8 +42,7 @@ describe Gitlab::Ci::Status::Build::Stop do end it 'is a correct match' do - expect(described_class.matches?(build, user)) - .to be true + expect(subject).to be true end end @@ -41,8 +50,7 @@ describe Gitlab::Ci::Status::Build::Stop do let(:build) { create(:ci_build, :playable) } it 'does not match' do - expect(described_class.matches?(build, user)) - .to be false + expect(subject).to be false end end end @@ -51,8 +59,7 @@ describe Gitlab::Ci::Status::Build::Stop do let(:build) { create(:ci_build) } it 'does not match' do - expect(described_class.matches?(build, user)) - .to be false + expect(subject).to be false end end end -- cgit v1.2.1 From 9d097ebdb5396ad6fdb93301417fc7de5f71eff7 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra <dimitriehoekstra@gmail.com> Date: Tue, 13 Dec 2016 01:28:57 +0100 Subject: added border-radius and padding to labels --- app/assets/stylesheets/framework/variables.scss | 1 + app/assets/stylesheets/pages/issuable.scss | 1 + app/assets/stylesheets/pages/labels.scss | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 18716813c48..a1d5f6427f4 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -486,6 +486,7 @@ $jq-ui-default-color: #777; $label-gray-bg: #f8fafc; $label-inverse-bg: #333; $label-remove-border: rgba(0, 0, 0, .1); +$label-border-radius: 14px; /* * Lint diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 90587b9425b..407c0afbac8 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -30,6 +30,7 @@ .color-label { padding: 6px 10px; + border-radius: $label-border-radius; } } diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index b1ccd644450..25c91203ff4 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -104,7 +104,8 @@ } .color-label { - padding: 3px 4px; + padding: 3px 7px; + border-radius: $label-border-radius; } .dropdown-labels-error { -- cgit v1.2.1 From 0b9df1bd93f9b6b6a5714431d8b815b0225917b5 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Tue, 13 Dec 2016 11:30:12 +0000 Subject: Replace old icons with new ones --- app/views/shared/icons/_icon_status_canceled.svg | 2 +- app/views/shared/icons/_icon_status_canceled_graph.svg | 1 - app/views/shared/icons/_icon_status_created.svg | 2 +- app/views/shared/icons/_icon_status_created_graph.svg | 1 - app/views/shared/icons/_icon_status_failed.svg | 2 +- app/views/shared/icons/_icon_status_failed_graph.svg | 1 - app/views/shared/icons/_icon_status_manual.svg | 1 + app/views/shared/icons/_icon_status_manual_graph.svg | 1 - app/views/shared/icons/_icon_status_pending.svg | 2 +- app/views/shared/icons/_icon_status_pending_graph.svg | 1 - app/views/shared/icons/_icon_status_running.svg | 2 +- app/views/shared/icons/_icon_status_running_graph.svg | 1 - app/views/shared/icons/_icon_status_skipped.svg | 2 +- app/views/shared/icons/_icon_status_skipped_graph.svg | 1 - app/views/shared/icons/_icon_status_success.svg | 2 +- app/views/shared/icons/_icon_status_success_graph.svg | 1 - app/views/shared/icons/_icon_status_warning.svg | 2 +- app/views/shared/icons/_icon_status_warning_graph.svg | 1 - 18 files changed, 9 insertions(+), 17 deletions(-) mode change 100644 => 100755 app/views/shared/icons/_icon_status_canceled.svg delete mode 100755 app/views/shared/icons/_icon_status_canceled_graph.svg mode change 100644 => 100755 app/views/shared/icons/_icon_status_created.svg delete mode 100755 app/views/shared/icons/_icon_status_created_graph.svg mode change 100644 => 100755 app/views/shared/icons/_icon_status_failed.svg delete mode 100755 app/views/shared/icons/_icon_status_failed_graph.svg create mode 100755 app/views/shared/icons/_icon_status_manual.svg delete mode 100755 app/views/shared/icons/_icon_status_manual_graph.svg mode change 100644 => 100755 app/views/shared/icons/_icon_status_pending.svg delete mode 100755 app/views/shared/icons/_icon_status_pending_graph.svg mode change 100644 => 100755 app/views/shared/icons/_icon_status_running.svg delete mode 100755 app/views/shared/icons/_icon_status_running_graph.svg mode change 100644 => 100755 app/views/shared/icons/_icon_status_skipped.svg delete mode 100755 app/views/shared/icons/_icon_status_skipped_graph.svg mode change 100644 => 100755 app/views/shared/icons/_icon_status_success.svg delete mode 100755 app/views/shared/icons/_icon_status_success_graph.svg mode change 100644 => 100755 app/views/shared/icons/_icon_status_warning.svg delete mode 100755 app/views/shared/icons/_icon_status_warning_graph.svg diff --git a/app/views/shared/icons/_icon_status_canceled.svg b/app/views/shared/icons/_icon_status_canceled.svg old mode 100644 new mode 100755 index 41a210a8ed9..bd5d04e1cd7 --- a/app/views/shared/icons/_icon_status_canceled.svg +++ b/app/views/shared/icons/_icon_status_canceled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill-rule="evenodd"><path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/><rect width="8" height="2" x="3" y="6" transform="rotate(45 7 7)" rx=".5"/></g></svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_canceled_graph.svg b/app/views/shared/icons/_icon_status_canceled_graph.svg deleted file mode 100755 index bd5d04e1cd7..00000000000 --- a/app/views/shared/icons/_icon_status_canceled_graph.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_created.svg b/app/views/shared/icons/_icon_status_created.svg old mode 100644 new mode 100755 index 1f5c3b51b03..326ad04e017 --- a/app/views/shared/icons/_icon_status_created.svg +++ b/app/views/shared/icons/_icon_status_created.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" enable-background="new 0 0 14 14"><path d="M12.5,7 C12.5,4 10,1.5 7,1.5 C4,1.5 1.5,4 1.5,7 C1.5,10 4,12.5 7,12.5 C10,12.5 12.5,10 12.5,7 L12.5,7 Z M0,7 C0,3.1 3.1,0 7,0 C10.9,0 14,3.1 14,7 C14,10.9 10.9,14 7,14 C3.1,14 0,10.9 0,7 L0,7 Z" /><circle cx="7" cy="7" r="3.25"/></svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_created_graph.svg b/app/views/shared/icons/_icon_status_created_graph.svg deleted file mode 100755 index 326ad04e017..00000000000 --- a/app/views/shared/icons/_icon_status_created_graph.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_failed.svg b/app/views/shared/icons/_icon_status_failed.svg old mode 100644 new mode 100755 index af267b8938a..64da5aa31fc --- a/app/views/shared/icons/_icon_status_failed.svg +++ b/app/views/shared/icons/_icon_status_failed.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill-rule="evenodd"><path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/><path d="M7.72916667,6.27083333 L7.72916667,4.28939247 C7.72916667,4.12531853 7.59703895,4 7.43405116,4 L6.56594884,4 C6.40541585,4 6.27083333,4.12956542 6.27083333,4.28939247 L6.27083333,6.27083333 L4.28939247,6.27083333 C4.12531853,6.27083333 4,6.40296105 4,6.56594884 L4,7.43405116 C4,7.59458415 4.12956542,7.72916667 4.28939247,7.72916667 L6.27083333,7.72916667 L6.27083333,9.71060753 C6.27083333,9.87468147 6.40296105,10 6.56594884,10 L7.43405116,10 C7.59458415,10 7.72916667,9.87043458 7.72916667,9.71060753 L7.72916667,7.72916667 L9.71060753,7.72916667 C9.87468147,7.72916667 10,7.59703895 10,7.43405116 L10,6.56594884 C10,6.40541585 9.87043458,6.27083333 9.71060753,6.27083333 L7.72916667,6.27083333 Z" transform="rotate(-45 7 7)"/></g></svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_failed_graph.svg b/app/views/shared/icons/_icon_status_failed_graph.svg deleted file mode 100755 index 64da5aa31fc..00000000000 --- a/app/views/shared/icons/_icon_status_failed_graph.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_manual.svg b/app/views/shared/icons/_icon_status_manual.svg new file mode 100755 index 00000000000..c98839f51a9 --- /dev/null +++ b/app/views/shared/icons/_icon_status_manual.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_manual_graph.svg b/app/views/shared/icons/_icon_status_manual_graph.svg deleted file mode 100755 index c98839f51a9..00000000000 --- a/app/views/shared/icons/_icon_status_manual_graph.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_pending.svg b/app/views/shared/icons/_icon_status_pending.svg old mode 100644 new mode 100755 index 516231d1b44..02d5da407e3 --- a/app/views/shared/icons/_icon_status_pending.svg +++ b/app/views/shared/icons/_icon_status_pending.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill-rule="evenodd"><path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/><path d="M4.69999981,5.30065012 C4.69999981,5.13460564 4.83842754,5 5.00354719,5 L5.89645243,5 C6.06409702,5 6.19999981,5.13308716 6.19999981,5.30065012 L6.19999981,8.69934988 C6.19999981,8.86539436 6.06157207,9 5.89645243,9 L5.00354719,9 C4.8359026,9 4.69999981,8.86691284 4.69999981,8.69934988 L4.69999981,5.30065012 Z M7.69999981,5.30065012 C7.69999981,5.13460564 7.83842754,5 8.00354719,5 L8.89645243,5 C9.06409702,5 9.19999981,5.13308716 9.19999981,5.30065012 L9.19999981,8.69934988 C9.19999981,8.86539436 9.06157207,9 8.89645243,9 L8.00354719,9 C7.8359026,9 7.69999981,8.86691284 7.69999981,8.69934988 L7.69999981,5.30065012 Z"/></g></svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_pending_graph.svg b/app/views/shared/icons/_icon_status_pending_graph.svg deleted file mode 100755 index 02d5da407e3..00000000000 --- a/app/views/shared/icons/_icon_status_pending_graph.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_running.svg b/app/views/shared/icons/_icon_status_running.svg old mode 100644 new mode 100755 index d2618bce200..532f4fee33c --- a/app/views/shared/icons/_icon_status_running.svg +++ b/app/views/shared/icons/_icon_status_running.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill-rule="evenodd"><path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/><path d="M7,3 C9.209139,3 11,4.790861 11,7 C11,9.209139 9.209139,11 7,11 C5.65802855,11 4.47040669,10.3391508 3.74481446,9.32513253 L7,7 L7,3 L7,3 Z"/></g></svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_running_graph.svg b/app/views/shared/icons/_icon_status_running_graph.svg deleted file mode 100755 index 532f4fee33c..00000000000 --- a/app/views/shared/icons/_icon_status_running_graph.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_skipped.svg b/app/views/shared/icons/_icon_status_skipped.svg old mode 100644 new mode 100755 index 701f33bcbea..1998dfef9ea --- a/app/views/shared/icons/_icon_status_skipped.svg +++ b/app/views/shared/icons/_icon_status_skipped.svg @@ -1 +1 @@ -<svg width="14" height="14" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M10 17.857c4.286 0 7.857-3.571 7.857-7.857S14.286 2.143 10 2.143 2.143 5.714 2.143 10 5.714 17.857 10 17.857M10 0c5.571 0 10 4.429 10 10s-4.429 10-10 10S0 15.571 0 10 4.429 0 10 0"/><path d="M10.986 11l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.644-2.644a1.505 1.505 0 0 0 0-2.126l-2.644-2.644a1 1 0 0 0-1.414 1.414L10.986 9H6.4a1 1 0 0 0 0 2h4.586z"/></g></svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7.69 7.7l-.905.905a.7.7 0 0 0 .99.99l1.85-1.85c.411-.412.411-1.078 0-1.49l-1.85-1.85a.7.7 0 0 0-.99.99l.905.905H4.48a.7.7 0 0 0 0 1.4h3.21z"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_skipped_graph.svg b/app/views/shared/icons/_icon_status_skipped_graph.svg deleted file mode 100755 index 1998dfef9ea..00000000000 --- a/app/views/shared/icons/_icon_status_skipped_graph.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7.69 7.7l-.905.905a.7.7 0 0 0 .99.99l1.85-1.85c.411-.412.411-1.078 0-1.49l-1.85-1.85a.7.7 0 0 0-.99.99l.905.905H4.48a.7.7 0 0 0 0 1.4h3.21z"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_success.svg b/app/views/shared/icons/_icon_status_success.svg old mode 100644 new mode 100755 index b7c21ba6971..eed5006bebe --- a/app/views/shared/icons/_icon_status_success.svg +++ b/app/views/shared/icons/_icon_status_success.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill-rule="evenodd"><path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/><path d="M7.29166667,7.875 L5.54840803,7.875 C5.38293028,7.875 5.25,8.00712771 5.25,8.17011551 L5.25,9.03821782 C5.25,9.19875081 5.38360183,9.33333333 5.54840803,9.33333333 L8.24853534,9.33333333 C8.52035522,9.33333333 8.75,9.11228506 8.75,8.83960819 L8.75,8.46475969 L8.75,4.07392947 C8.75,3.92144267 8.61787229,3.79166667 8.45488449,3.79166667 L7.58678218,3.79166667 C7.42624919,3.79166667 7.29166667,3.91804003 7.29166667,4.07392947 L7.29166667,7.875 Z" transform="rotate(45 7 6.563)"/></g></svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_success_graph.svg b/app/views/shared/icons/_icon_status_success_graph.svg deleted file mode 100755 index eed5006bebe..00000000000 --- a/app/views/shared/icons/_icon_status_success_graph.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_warning.svg b/app/views/shared/icons/_icon_status_warning.svg old mode 100644 new mode 100755 index 9191e0050a6..cb785635b7e --- a/app/views/shared/icons/_icon_status_warning.svg +++ b/app/views/shared/icons/_icon_status_warning.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill-rule="evenodd"><path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/><path d="M6,3.49769878 C6,3.22282734 6.21403503,3 6.50468445,3 L7.49531555,3 C7.77404508,3 8,3.21484375 8,3.49769878 L8,7.50230122 C8,7.77717266 7.78596497,8 7.49531555,8 L6.50468445,8 C6.22595492,8 6,7.78515625 6,7.50230122 L6,3.49769878 Z M6,9.50468445 C6,9.22595492 6.21403503,9 6.50468445,9 L7.49531555,9 C7.77404508,9 8,9.21403503 8,9.50468445 L8,10.4953156 C8,10.7740451 7.78596497,11 7.49531555,11 L6.50468445,11 C6.22595492,11 6,10.785965 6,10.4953156 L6,9.50468445 Z"/></g></svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_warning_graph.svg b/app/views/shared/icons/_icon_status_warning_graph.svg deleted file mode 100755 index cb785635b7e..00000000000 --- a/app/views/shared/icons/_icon_status_warning_graph.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></svg> -- cgit v1.2.1 From a06016d00cd363eec7ef748e5fda6459c911eaba Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Tue, 13 Dec 2016 11:38:34 +0000 Subject: Remove unneeded partial --- app/views/ci/status/_graph_icon_with_name.html.haml | 12 ------------ .../ci/status/_graph_icon_with_name_and_action.html.haml | 8 -------- app/views/projects/stage/_graph.html.haml | 2 +- app/views/projects/stage/_in_stage_group.html.haml | 2 +- 4 files changed, 2 insertions(+), 22 deletions(-) delete mode 100644 app/views/ci/status/_graph_icon_with_name.html.haml delete mode 100644 app/views/ci/status/_graph_icon_with_name_and_action.html.haml diff --git a/app/views/ci/status/_graph_icon_with_name.html.haml b/app/views/ci/status/_graph_icon_with_name.html.haml deleted file mode 100644 index 51037a3bd20..00000000000 --- a/app/views/ci/status/_graph_icon_with_name.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -- detailed_status = subject.detailed_status(current_user) -- details_path = detailed_status.details_path if detailed_status.has_details? -- klass = "ci-status-icon ci-status-icon-#{detailed_status}" -- graph_status_icon = "#{detailed_status.icon}_graph" - -- if details_path - = link_to details_path, class: klass, data: { toggle: 'tooltip', title: "#{subject.name} - #{detailed_status}" } do - %span{ class: klass }= custom_icon(graph_status_icon) - .ci-status-text= subject.name -- else - %span{ class: klass }= custom_icon(graph_status_icon) - .ci-status-text= subject.name diff --git a/app/views/ci/status/_graph_icon_with_name_and_action.html.haml b/app/views/ci/status/_graph_icon_with_name_and_action.html.haml deleted file mode 100644 index 525075ced70..00000000000 --- a/app/views/ci/status/_graph_icon_with_name_and_action.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -= render "ci/status/graph_icon_with_name", subject: subject - -- detailed_status = subject.detailed_status(current_user) -- if detailed_status.has_action? - = link_to detailed_status.action_path, method: detailed_status.action_method, - title: "#{subject.name}: #{detailed_status.action_title}", class: 'ci-action-icon-container' do - %i.ci-action-icon-wrapper - = icon(detailed_status.action_icon, class: detailed_status.action_class) diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml index cf3050eea63..6d280468262 100644 --- a/app/views/projects/stage/_graph.html.haml +++ b/app/views/projects/stage/_graph.html.haml @@ -13,7 +13,7 @@ %li.build .curve .build-content - = render 'ci/status/graph_icon_with_name_and_action', subject: status + = render 'ci/status/icon_with_name_and_action', subject: status - else %li.build .curve diff --git a/app/views/projects/stage/_in_stage_group.html.haml b/app/views/projects/stage/_in_stage_group.html.haml index 70101ccf806..5c9b6549b37 100644 --- a/app/views/projects/stage/_in_stage_group.html.haml +++ b/app/views/projects/stage/_in_stage_group.html.haml @@ -10,4 +10,4 @@ %ul - subject.each do |status| %li.dropdown-build - = render 'ci/status/graph_icon_with_name_and_action', subject: status + = render 'ci/status/icon_with_name_and_action', subject: status -- cgit v1.2.1 From 92b0f54ea222ce4c4437a50683c972bacc1fee06 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Tue, 13 Dec 2016 11:39:13 +0000 Subject: Fix graph stroke position --- app/assets/stylesheets/pages/pipelines.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 4a5b0b5922c..15da30dda2b 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -427,7 +427,7 @@ width: 21px; height: 25px; position: absolute; - top: -30px; + top: -32px; border-top: 2px solid $border-color; } -- cgit v1.2.1 From 07d69d8a6763984536a590690bb58527f6175bc1 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Tue, 13 Dec 2016 19:58:26 +0800 Subject: Fix Slack pipeline message by API Pipelines triggered from API don't have a corresponding user, so we show that it's from API, same as from the web UI Closes #25609 --- .../slack_service/pipeline_message.rb | 2 +- .../fix-slack-pipeline-message-by-api.yml | 4 +++ .../slack_service/pipeline_message_spec.rb | 30 +++++++++++++++------- 3 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 changelogs/unreleased/fix-slack-pipeline-message-by-api.yml diff --git a/app/models/project_services/slack_service/pipeline_message.rb b/app/models/project_services/slack_service/pipeline_message.rb index f8d03c0e2fa..b6355fc4171 100644 --- a/app/models/project_services/slack_service/pipeline_message.rb +++ b/app/models/project_services/slack_service/pipeline_message.rb @@ -13,7 +13,7 @@ class SlackService @project_name = data[:project][:path_with_namespace] @project_url = data[:project][:web_url] - @user_name = data[:user] && data[:user][:name] + @user_name = (data[:user] && data[:user][:name]) || 'API' end def pretext diff --git a/changelogs/unreleased/fix-slack-pipeline-message-by-api.yml b/changelogs/unreleased/fix-slack-pipeline-message-by-api.yml new file mode 100644 index 00000000000..aa5ad5cd8d6 --- /dev/null +++ b/changelogs/unreleased/fix-slack-pipeline-message-by-api.yml @@ -0,0 +1,4 @@ +--- +title: Fix Slack pipeline message from pipelines made by API +merge_request: 8059 +author: diff --git a/spec/models/project_services/slack_service/pipeline_message_spec.rb b/spec/models/project_services/slack_service/pipeline_message_spec.rb index 363138a9454..4098500122f 100644 --- a/spec/models/project_services/slack_service/pipeline_message_spec.rb +++ b/spec/models/project_services/slack_service/pipeline_message_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe SlackService::PipelineMessage do subject { SlackService::PipelineMessage.new(args) } + let(:user) { { name: 'hacker' } } let(:args) do { @@ -15,7 +16,7 @@ describe SlackService::PipelineMessage do }, project: { path_with_namespace: 'project_name', web_url: 'example.gitlab.com' }, - user: { name: 'hacker' } + user: user } end @@ -28,9 +29,7 @@ describe SlackService::PipelineMessage do let(:message) { build_message('passed') } it 'returns a message with information about succeeded build' do - expect(subject.pretext).to be_empty - expect(subject.fallback).to eq(message) - expect(subject.attachments).to eq([text: message, color: color]) + verify_message end end @@ -40,16 +39,29 @@ describe SlackService::PipelineMessage do let(:duration) { 10 } it 'returns a message with information about failed build' do - expect(subject.pretext).to be_empty - expect(subject.fallback).to eq(message) - expect(subject.attachments).to eq([text: message, color: color]) + verify_message end + + context 'when triggered by API therefore lacking user' do + let(:user) { nil } + let(:message) { build_message(status, 'API') } + + it 'returns a message stating it is by API' do + verify_message + end + end + end + + def verify_message + expect(subject.pretext).to be_empty + expect(subject.fallback).to eq(message) + expect(subject.attachments).to eq([text: message, color: color]) end - def build_message(status_text = status) + def build_message(status_text = status, name = user[:name]) "<example.gitlab.com|project_name>:" \ " Pipeline <example.gitlab.com/pipelines/123|#123>" \ " of <example.gitlab.com/commits/develop|develop> branch" \ - " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}" + " by #{name} #{status_text} in #{duration} #{'second'.pluralize(duration)}" end end -- cgit v1.2.1 From 65c3bfe68a3a0599187219ba49a1a46f23b45312 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 13 Dec 2016 13:13:09 +0100 Subject: Extend specs for build play/stop detailed statuses --- spec/lib/gitlab/ci/status/build/play_spec.rb | 30 ++++++++++++++++++++++++++ spec/lib/gitlab/ci/status/build/stop_spec.rb | 32 ++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb index 180a2808a42..0fa4405c604 100644 --- a/spec/lib/gitlab/ci/status/build/play_spec.rb +++ b/spec/lib/gitlab/ci/status/build/play_spec.rb @@ -18,6 +18,36 @@ describe Gitlab::Ci::Status::Build::Play do it { expect(subject.icon).to eq 'icon_status_manual' } end + describe 'action details' do + let(:user) { create(:user) } + let(:build) { create(:ci_build) } + let(:status) { Gitlab::Ci::Status::Core.new(build, user) } + + describe '#has_action?' do + context 'when user is allowed to update build' do + before { build.project.team << [user, :developer] } + + it { is_expected.to have_action } + end + + context 'when user is not allowed to update build' do + it { is_expected.not_to have_action } + end + end + + describe '#action_path' do + it { expect(subject.action_path).to include "#{build.id}/play" } + end + + describe '#action_icon' do + it { expect(subject.action_icon).to eq 'play' } + end + + describe '#action_title' do + it { expect(subject.action_title).to eq 'Play' } + end + end + describe '.matches?' do subject { described_class.matches?(build, user) } diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb index bc1939fb493..984cd1e5007 100644 --- a/spec/lib/gitlab/ci/status/build/stop_spec.rb +++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb @@ -20,16 +20,34 @@ describe Gitlab::Ci::Status::Build::Stop do it { expect(subject.icon).to eq 'icon_status_manual' } end - describe '#has_action?' do - end + describe 'action details' do + let(:user) { create(:user) } + let(:build) { create(:ci_build) } + let(:status) { Gitlab::Ci::Status::Core.new(build, user) } - describe '#action_icon' do - end + describe '#has_action?' do + context 'when user is allowed to update build' do + before { build.project.team << [user, :developer] } - describe '#action_path' do - end + it { is_expected.to have_action } + end + + context 'when user is not allowed to update build' do + it { is_expected.not_to have_action } + end + end + + describe '#action_path' do + it { expect(subject.action_path).to include "#{build.id}/play" } + end - describe '#action_title' do + describe '#action_icon' do + it { expect(subject.action_icon).to eq 'stop' } + end + + describe '#action_title' do + it { expect(subject.action_title).to eq 'Stop' } + end end describe '.matches?' do -- cgit v1.2.1 From a91a95b2327a3570db16afa1fde91c899d79d5a5 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 13 Dec 2016 13:22:40 +0100 Subject: Add tests for build cancelable/retryable statuses --- spec/lib/gitlab/ci/status/build/cancelable_spec.rb | 86 ++++++++++++++++++++++ spec/lib/gitlab/ci/status/build/retryable_spec.rb | 86 ++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 spec/lib/gitlab/ci/status/build/cancelable_spec.rb create mode 100644 spec/lib/gitlab/ci/status/build/retryable_spec.rb diff --git a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb new file mode 100644 index 00000000000..989328c0719 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Cancelable do + let(:status) { double('core status') } + let(:user) { double('user') } + + subject do + described_class.new(status) + end + + describe '#text' do + it 'does not override status text' do + expect(status).to receive(:text) + + subject.text + end + end + + describe '#icon' do + it 'does not override status icon' do + expect(status).to receive(:icon) + + subject.icon + end + end + + describe '#label' do + it 'does not override status label' do + expect(status).to receive(:label) + + subject.label + end + end + + describe 'action details' do + let(:user) { create(:user) } + let(:build) { create(:ci_build) } + let(:status) { Gitlab::Ci::Status::Core.new(build, user) } + + describe '#has_action?' do + context 'when user is allowed to update build' do + before { build.project.team << [user, :developer] } + + it { is_expected.to have_action } + end + + context 'when user is not allowed to update build' do + it { is_expected.not_to have_action } + end + end + + describe '#action_path' do + it { expect(subject.action_path).to include "#{build.id}/cancel" } + end + + describe '#action_icon' do + it { expect(subject.action_icon).to eq 'ban' } + end + + describe '#action_title' do + it { expect(subject.action_title).to eq 'Cancel' } + end + end + + describe '.matches?' do + subject { described_class.matches?(build, user) } + + context 'build is cancelable' do + let(:build) do + create(:ci_build, :running) + end + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when build is not cancelable' do + let(:build) { create(:ci_build, :success) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/retryable_spec.rb b/spec/lib/gitlab/ci/status/build/retryable_spec.rb new file mode 100644 index 00000000000..59f6b78f99d --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/retryable_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Retryable do + let(:status) { double('core status') } + let(:user) { double('user') } + + subject do + described_class.new(status) + end + + describe '#text' do + it 'does not override status text' do + expect(status).to receive(:text) + + subject.text + end + end + + describe '#icon' do + it 'does not override status icon' do + expect(status).to receive(:icon) + + subject.icon + end + end + + describe '#label' do + it 'does not override status label' do + expect(status).to receive(:label) + + subject.label + end + end + + describe 'action details' do + let(:user) { create(:user) } + let(:build) { create(:ci_build) } + let(:status) { Gitlab::Ci::Status::Core.new(build, user) } + + describe '#has_action?' do + context 'when user is allowed to update build' do + before { build.project.team << [user, :developer] } + + it { is_expected.to have_action } + end + + context 'when user is not allowed to update build' do + it { is_expected.not_to have_action } + end + end + + describe '#action_path' do + it { expect(subject.action_path).to include "#{build.id}/retry" } + end + + describe '#action_icon' do + it { expect(subject.action_icon).to eq 'refresh' } + end + + describe '#action_title' do + it { expect(subject.action_title).to eq 'Retry' } + end + end + + describe '.matches?' do + subject { described_class.matches?(build, user) } + + context 'build is retryable' do + let(:build) do + create(:ci_build, :success) + end + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when build is not retryable' do + let(:build) { create(:ci_build, :running) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end -- cgit v1.2.1 From 5f590a71fd26b637501f8751bc6f9adff4d9c8db Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 13 Dec 2016 13:24:25 +0100 Subject: Improve readability in methods for detailed status --- app/models/ci/build.rb | 4 +++- app/models/ci/pipeline.rb | 4 +++- app/models/ci/stage.rb | 4 +++- app/models/commit_status.rb | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 65ee327a8e5..63a4a075f8e 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -101,7 +101,9 @@ module Ci end def detailed_status(current_user) - Gitlab::Ci::Status::Build::Factory.new(self, current_user).fabricate! + Gitlab::Ci::Status::Build::Factory + .new(self, current_user) + .fabricate! end def manual? diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 1f33106d358..54f73171fd4 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -337,7 +337,9 @@ module Ci end def detailed_status(current_user) - Gitlab::Ci::Status::Pipeline::Factory.new(self, current_user).fabricate! + Gitlab::Ci::Status::Pipeline::Factory + .new(self, current_user) + .fabricate! end private diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index be52cce20f1..7ef59445d77 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -23,7 +23,9 @@ module Ci end def detailed_status(current_user) - Gitlab::Ci::Status::Stage::Factory.new(self, current_user).fabricate! + Gitlab::Ci::Status::Stage::Factory + .new(self, current_user) + .fabricate! end def statuses diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 6548a7dda2c..31cd381dcd2 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -133,6 +133,8 @@ class CommitStatus < ActiveRecord::Base end def detailed_status(current_user) - Gitlab::Ci::Status::Factory.new(self, current_user).fabricate! + Gitlab::Ci::Status::Factory + .new(self, current_user) + .fabricate! end end -- cgit v1.2.1 From 549242c620a812b4ed0f1a3b08f3e315027b9e65 Mon Sep 17 00:00:00 2001 From: Julian Zinn <julian@asperasoft.com> Date: Fri, 4 Nov 2016 12:36:37 -0700 Subject: For single line git commit messages, the close quote should be on the same line as the open quote MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- app/models/project_services/jira_service.rb | 2 +- changelogs/unreleased/chomp-git-status-message.yml | 5 +++++ spec/services/system_note_service_spec.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/chomp-git-status-message.yml diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 894315a8593..2d969d2fcb6 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -220,7 +220,7 @@ class JiraService < IssueTrackerService entity_title = data[:entity][:title] project_name = data[:project][:name] - message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'" + message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title.chomp}'" link_title = "GitLab: Mentioned on #{entity_name} - #{entity_title}" link_props = build_remote_link_props(url: entity_url, title: link_title) diff --git a/changelogs/unreleased/chomp-git-status-message.yml b/changelogs/unreleased/chomp-git-status-message.yml new file mode 100644 index 00000000000..f70607df7a1 --- /dev/null +++ b/changelogs/unreleased/chomp-git-status-message.yml @@ -0,0 +1,5 @@ +--- +title: For single line git commit messages, the close quote should be on the same + line as the open quote +merge_request: +author: diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 90b7e62bc6f..0e8adb68721 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -694,7 +694,7 @@ describe SystemNoteService, services: true do describe "existing reference" do before do - message = "[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\n'#{commit.title}'" + message = "[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\n'#{commit.title.chomp}'" allow_any_instance_of(JIRA::Resource::Issue).to receive(:comments).and_return([OpenStruct.new(body: message)]) end -- cgit v1.2.1 From ec25abe6255a1cc0a4c68192894cc7ac1cb90f54 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Tue, 13 Dec 2016 14:48:02 +0200 Subject: Add AddLowerPathIndexToRoutes to setup_postgresql.rake Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- lib/tasks/migrate/setup_postgresql.rake | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake index 141a0b74ec0..f5caca3ddbf 100644 --- a/lib/tasks/migrate/setup_postgresql.rake +++ b/lib/tasks/migrate/setup_postgresql.rake @@ -1,8 +1,12 @@ +require Rails.root.join('lib/gitlab/database') +require Rails.root.join('lib/gitlab/database/migration_helpers') 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') +require Rails.root.join('db/migrate/20161212142807_add_lower_path_index_to_routes') desc 'GitLab | Sets up PostgreSQL' task setup_postgresql: :environment do NamespacesProjectsPathLowerIndexes.new.up AddUsersLowerUsernameEmailIndexes.new.up + AddLowerPathIndexToRoutes.new.up end -- cgit v1.2.1 From 7841be243e5f28828cea95500b554d7f5cc039eb Mon Sep 17 00:00:00 2001 From: Robert Schilling <rschilling@student.tugraz.at> Date: Tue, 13 Dec 2016 13:51:30 +0100 Subject: API: Ability to get group's project in simple representation --- changelogs/unreleased/api-simple-group-project.yml | 4 ++++ doc/api/groups.md | 17 +++++++++++------ lib/api/groups.rb | 5 ++++- spec/requests/api/groups_spec.rb | 15 +++++++++++++-- 4 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 changelogs/unreleased/api-simple-group-project.yml diff --git a/changelogs/unreleased/api-simple-group-project.yml b/changelogs/unreleased/api-simple-group-project.yml new file mode 100644 index 00000000000..54c8de610a6 --- /dev/null +++ b/changelogs/unreleased/api-simple-group-project.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Simple representation of group''s projects' +merge_request: 8060 +author: Robert Schilling diff --git a/doc/api/groups.md b/doc/api/groups.md index 5e6f498c365..134d7bda22f 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -50,12 +50,17 @@ GET /groups/:id/projects Parameters: -- `archived` (optional) - if passed, limit by archived status -- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private` -- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at` -- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` -- `search` (optional) - Return list of authorized projects according to a search criteria -- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or path of a group | +| `archived` | boolean | no | Limit by archived status | +| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` | +| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` | +| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` | +| `search` | string | no | Return list of authorized projects matching the search criteria | +| `simple` | boolean | no | Return only the ID, URL, name, and path of each project | + +Example response: ```json [ diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 105d3ee342e..9b9d3df7435 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -125,13 +125,16 @@ module API default: 'created_at', desc: 'Return projects ordered by field' optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'Return projects sorted in ascending and descending order' + optional :simple, type: Boolean, default: false, + desc: 'Return only the ID, URL, name, and path of each project' use :pagination end get ":id/projects" do group = find_group!(params[:id]) projects = GroupProjectsFinder.new(group).execute(current_user) projects = filter_projects(projects) - present paginate(projects), with: Entities::Project, user: current_user + entity = params[:simple] ? Entities::BasicProjectDetails : Entities::Project + present paginate(projects), with: entity, user: current_user end desc 'Transfer a project to the group namespace. Available only for admin.' do diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 15647b262b6..a75ba824e85 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -243,17 +243,28 @@ describe API::Groups, api: true do expect(json_response.length).to eq(2) project_names = json_response.map { |proj| proj['name' ] } expect(project_names).to match_array([project1.name, project3.name]) + expect(json_response.first['default_branch']).to be_present + end + + it "returns the group's projects with simple representation" do + get api("/groups/#{group1.id}/projects", user1), simple: true + + expect(response).to have_http_status(200) + expect(json_response.length).to eq(2) + project_names = json_response.map { |proj| proj['name' ] } + expect(project_names).to match_array([project1.name, project3.name]) + expect(json_response.first['default_branch']).not_to be_present end it 'filters the groups projects' do - public_projet = create(:project, :public, path: 'test1', group: group1) + public_project = create(:project, :public, path: 'test1', group: group1) get api("/groups/#{group1.id}/projects", user1), visibility: 'public' expect(response).to have_http_status(200) expect(json_response).to be_an(Array) expect(json_response.length).to eq(1) - expect(json_response.first['name']).to eq(public_projet.name) + expect(json_response.first['name']).to eq(public_project.name) end it "does not return a non existing group" do -- cgit v1.2.1 From a9ec4ec07e64d5f823c30d9bcc4c4bb1be7f2d75 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 13 Dec 2016 13:57:18 +0100 Subject: Make build retryable only if complete and executed --- app/models/ci/build.rb | 3 ++- spec/models/build_spec.rb | 38 ++++++++++++++++++++++++++++++-------- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 63a4a075f8e..89b0cae25ed 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -134,7 +134,8 @@ module Ci end def retryable? - project.builds_enabled? && commands.present? && complete? + project.builds_enabled? && commands.present? && + (success? || failed?) end def retried? diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index d4970e38f7c..2b678355ee5 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -900,20 +900,42 @@ describe Ci::Build, models: true do end describe '#retryable?' do - context 'when build is running' do - before do - build.run! + subject { build } + + context 'when build is retryable' do + context 'when build is successful' do + before do + build.success! + end + + it { is_expected.to be_retryable } end - it { expect(build).not_to be_retryable } + context 'when build is failed' do + before do + build.drop! + end + + it { is_expected.to be_retryable } + end end - context 'when build is finished' do - before do - build.success! + context 'when build is not retryable' do + context 'when build is running' do + before do + build.run! + end + + it { is_expected.not_to be_retryable } end - it { expect(build).to be_retryable } + context 'when build is skipped' do + before do + build.skip! + end + + it { is_expected.not_to be_retryable } + end end end -- cgit v1.2.1 From f4513d5c1981922e71a00b9e4b135605eb674c6f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 13 Dec 2016 14:01:17 +0100 Subject: Make it possible to retry build that was canceled --- app/models/ci/build.rb | 2 +- spec/models/build_spec.rb | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 89b0cae25ed..e7cf606a7ae 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -135,7 +135,7 @@ module Ci def retryable? project.builds_enabled? && commands.present? && - (success? || failed?) + (success? || failed? || canceled?) end def retried? diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 2b678355ee5..e9b4cac5fef 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -918,6 +918,14 @@ describe Ci::Build, models: true do it { is_expected.to be_retryable } end + + context 'when build is canceled' do + before do + build.cancel! + end + + it { is_expected.to be_retryable } + end end context 'when build is not retryable' do -- cgit v1.2.1 From d7a774320e9e79bebb57dbbb927ff6af6d09e0e2 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 13 Dec 2016 14:07:51 +0100 Subject: Add tests for detailed build statuses factory --- spec/lib/gitlab/ci/status/build/factory_spec.rb | 141 ++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 spec/lib/gitlab/ci/status/build/factory_spec.rb diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb new file mode 100644 index 00000000000..dccb29b5ef6 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -0,0 +1,141 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Factory do + let(:user) { create(:user) } + let(:project) { build.project } + + subject { described_class.new(build, user) } + let(:status) { subject.fabricate! } + + before { project.team << [user, :developer] } + + context 'when build is successful' do + let(:build) { create(:ci_build, :success) } + + it 'fabricates a retryable build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Retryable + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'passed' + expect(status.icon).to eq 'icon_status_success' + expect(status.label).to eq 'passed' + expect(status).to have_details + expect(status).to have_action + end + end + + context 'when build is failed' do + let(:build) { create(:ci_build, :failed) } + + it 'fabricates a retryable build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Retryable + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'failed' + expect(status.icon).to eq 'icon_status_failed' + expect(status.label).to eq 'failed' + expect(status).to have_details + expect(status).to have_action + end + end + + context 'when build is a canceled' do + let(:build) { create(:ci_build, :canceled) } + + it 'fabricates a retryable build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Retryable + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'canceled' + expect(status.icon).to eq 'icon_status_canceled' + expect(status.label).to eq 'canceled' + expect(status).to have_details + expect(status).to have_action + end + end + + context 'when build is running' do + let(:build) { create(:ci_build, :running) } + + it 'fabricates a canceable build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'running' + expect(status.icon).to eq 'icon_status_running' + expect(status.label).to eq 'running' + expect(status).to have_details + expect(status).to have_action + end + end + + context 'when build is pending' do + let(:build) { create(:ci_build, :pending) } + + it 'fabricates a cancelable build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'pending' + expect(status.icon).to eq 'icon_status_pending' + expect(status.label).to eq 'pending' + expect(status).to have_details + expect(status).to have_action + end + end + + context 'when build is skipped' do + let(:build) { create(:ci_build, :skipped) } + + it 'fabricates a core skipped status' do + expect(status).to be_a Gitlab::Ci::Status::Skipped + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'skipped' + expect(status.icon).to eq 'icon_status_skipped' + expect(status.label).to eq 'skipped' + expect(status).to have_details + expect(status).not_to have_action + end + end + + context 'when build is a manual action' do + context 'when build is a play action' do + let(:build) { create(:ci_build, :playable) } + + it 'fabricates a core skipped status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Play + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'manual' + expect(status.icon).to eq 'icon_status_manual' + expect(status.label).to eq 'manual play action' + expect(status).to have_details + expect(status).to have_action + end + end + + context 'when build is an environment stop action' do + let(:build) { create(:ci_build, :playable, :teardown_environment) } + + it 'fabricates a core skipped status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Stop + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'manual' + expect(status.icon).to eq 'icon_status_manual' + expect(status.label).to eq 'manual stop action' + expect(status).to have_details + expect(status).to have_action + end + end + end +end -- cgit v1.2.1 From 7f0ecf3a97a20a0b274ebfd46e762afad05870fc Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 13 Dec 2016 14:18:34 +0100 Subject: Add missing tests for build `cancelable?` method --- spec/models/build_spec.rb | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index e9b4cac5fef..ac596ce4a91 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -899,6 +899,42 @@ describe Ci::Build, models: true do end end + describe '#cancelable?' do + subject { build } + + context 'when build is cancelable' do + context 'when build is pending' do + it { is_expected.to be_cancelable } + end + + context 'when build is running' do + before do + build.run! + end + + it { is_expected.to be_cancelable } + end + end + + context 'when build is not cancelable' do + context 'when build is successful' do + before do + build.success! + end + + it { is_expected.not_to be_cancelable } + end + + context 'when build is failed' do + before do + build.drop! + end + + it { is_expected.not_to be_cancelable } + end + end + end + describe '#retryable?' do subject { build } -- cgit v1.2.1 From 8f743edee14374a6d39a3cf5aa0fc884a0d536b8 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 13 Dec 2016 14:26:44 +0100 Subject: Add tests for common build detailed status helpers --- spec/lib/gitlab/ci/status/build/common_spec.rb | 37 ++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 spec/lib/gitlab/ci/status/build/common_spec.rb diff --git a/spec/lib/gitlab/ci/status/build/common_spec.rb b/spec/lib/gitlab/ci/status/build/common_spec.rb new file mode 100644 index 00000000000..40b96b1807b --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/common_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Common do + let(:user) { create(:user) } + let(:build) { create(:ci_build) } + let(:project) { build.project } + + subject do + Gitlab::Ci::Status::Core + .new(build, user) + .extend(described_class) + end + + describe '#has_action?' do + it { is_expected.not_to have_action } + end + + describe '#has_details?' do + context 'when user has access to read build' do + before { project.team << [user, :developer] } + + it { is_expected.to have_details } + end + + context 'when user does not have access to read build' do + before { project.update(public_builds: false) } + + it { is_expected.not_to have_details } + end + end + + describe '#details_path' do + it 'links to the build details page' do + expect(subject.details_path).to include "builds/#{build.id}" + end + end +end -- cgit v1.2.1 From 24dd70d37834e55a7ead23ae14125e5e12f64d8b Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 13 Dec 2016 14:29:48 +0100 Subject: Extend tests for pipeline detailed status helpers --- spec/lib/gitlab/ci/status/pipeline/common_spec.rb | 30 ++++++++++++++--------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb index 2df9d574677..d665674bf70 100644 --- a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb @@ -2,29 +2,35 @@ require 'spec_helper' describe Gitlab::Ci::Status::Pipeline::Common do let(:user) { create(:user) } - let(:project) { create(:empty_project) } + let(:project) { create(:empty_project, :private) } let(:pipeline) { create(:ci_pipeline, project: project) } subject do - Class.new(Gitlab::Ci::Status::Core) + Gitlab::Ci::Status::Core .new(pipeline, user) .extend(described_class) end - before do - project.team << [user, :developer] + describe '#has_action?' do + it { is_expected.not_to have_action } end - it 'does not have action' do - expect(subject).not_to have_action - end + describe '#has_details?' do + context 'when user has access to read pipeline' do + before { project.team << [user, :developer] } + + it { is_expected.to have_details } + end - it 'has details' do - expect(subject).to have_details + context 'when user does not have access to read pipeline' do + it { is_expected.not_to have_details } + end end - it 'links to the pipeline details page' do - expect(subject.details_path) - .to include "pipelines/#{pipeline.id}" + describe '#details_path' do + it 'links to the pipeline details page' do + expect(subject.details_path) + .to include "pipelines/#{pipeline.id}" + end end end -- cgit v1.2.1 From 00970606d78486a8b6672659e9ea28521a102e44 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 13 Dec 2016 14:44:43 +0100 Subject: Extract abilities checking module from ability model --- app/models/ability.rb | 6 ------ lib/gitlab/allowable.rb | 8 ++++++++ lib/gitlab/ci/status/core.rb | 2 +- spec/lib/gitlab/allowable_spec.rb | 27 +++++++++++++++++++++++++++ 4 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 lib/gitlab/allowable.rb create mode 100644 spec/lib/gitlab/allowable_spec.rb diff --git a/app/models/ability.rb b/app/models/ability.rb index ce461caf686..fa8f8bc3a5f 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -1,10 +1,4 @@ class Ability - module Allowable - def can?(user, action, subject) - Ability.allowed?(user, action, subject) - end - end - class << self # Given a list of users and a project this method returns the users that can # read the given project. diff --git a/lib/gitlab/allowable.rb b/lib/gitlab/allowable.rb new file mode 100644 index 00000000000..3cc218f9db4 --- /dev/null +++ b/lib/gitlab/allowable.rb @@ -0,0 +1,8 @@ +module Gitlab + module Allowable + def can?(user, action, subject) + Ability.allowed?(user, action, subject) + end + end +end + diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb index dd3a824e486..12c0ca1d6d5 100644 --- a/lib/gitlab/ci/status/core.rb +++ b/lib/gitlab/ci/status/core.rb @@ -5,7 +5,7 @@ module Gitlab # class Core include Gitlab::Routing.url_helpers - include Ability::Allowable + include Gitlab::Allowable attr_reader :subject, :user diff --git a/spec/lib/gitlab/allowable_spec.rb b/spec/lib/gitlab/allowable_spec.rb new file mode 100644 index 00000000000..87733d53e92 --- /dev/null +++ b/spec/lib/gitlab/allowable_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Gitlab::Allowable do + subject do + Class.new.include(described_class).new + end + + describe '#can?' do + let(:user) { create(:user) } + + context 'when user is allowed to do something' do + let(:project) { create(:empty_project, :public) } + + it 'reports correct ability to perform action' do + expect(subject.can?(user, :read_project, project)).to be true + end + end + + context 'when user is not allowed to do something' do + let(:project) { create(:empty_project, :private) } + + it 'reports correct ability to perform action' do + expect(subject.can?(user, :read_project, project)).to be false + end + end + end +end -- cgit v1.2.1 From 8c639ac23ce67b763cabf3aed4b020f4b961f9be Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Tue, 13 Dec 2016 11:51:09 -0200 Subject: Backport hooks on group policies for the EE-specific implementation --- app/policies/group_member_policy.rb | 6 ++++++ app/policies/group_policy.rb | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/app/policies/group_member_policy.rb b/app/policies/group_member_policy.rb index 62335527654..5a3fe814b77 100644 --- a/app/policies/group_member_policy.rb +++ b/app/policies/group_member_policy.rb @@ -15,5 +15,11 @@ class GroupMemberPolicy < BasePolicy elsif @user == target_user can! :destroy_group_member end + + additional_rules! + end + + def additional_rules! + # This is meant to be overriden in EE end end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index b65fb68cd88..6f943feb2a7 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -33,6 +33,8 @@ class GroupPolicy < BasePolicy if globally_viewable && @subject.request_access_enabled && !member can! :request_access end + + additional_rules!(master) end def can_read_group? @@ -43,4 +45,8 @@ class GroupPolicy < BasePolicy GroupProjectsFinder.new(@subject).execute(@user).any? end + + def additional_rules!(master) + # This is meant to be overriden in EE + end end -- cgit v1.2.1 From 84290a452dcdd2271337e2301b846c9498b74c86 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 13 Dec 2016 14:51:23 +0100 Subject: Make it possible to mix `Gitlab::Routing` in --- lib/gitlab/ci/status/core.rb | 2 +- lib/gitlab/routing.rb | 6 ++++++ spec/lib/gitlab/routing_spec.rb | 23 +++++++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 spec/lib/gitlab/routing_spec.rb diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb index 12c0ca1d6d5..46fef8262c1 100644 --- a/lib/gitlab/ci/status/core.rb +++ b/lib/gitlab/ci/status/core.rb @@ -4,7 +4,7 @@ module Gitlab # Base abstract class fore core status # class Core - include Gitlab::Routing.url_helpers + include Gitlab::Routing include Gitlab::Allowable attr_reader :subject, :user diff --git a/lib/gitlab/routing.rb b/lib/gitlab/routing.rb index 5132177de51..632e2d87500 100644 --- a/lib/gitlab/routing.rb +++ b/lib/gitlab/routing.rb @@ -1,5 +1,11 @@ module Gitlab module Routing + extend ActiveSupport::Concern + + included do + include Gitlab::Routing.url_helpers + end + # Returns the URL helpers Module. # # This method caches the output as Rails' "url_helpers" method creates an diff --git a/spec/lib/gitlab/routing_spec.rb b/spec/lib/gitlab/routing_spec.rb new file mode 100644 index 00000000000..01d5acfc15b --- /dev/null +++ b/spec/lib/gitlab/routing_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::Routing do + context 'when module is included' do + subject do + Class.new.include(described_class).new + end + + it 'makes it possible to access url helpers' do + expect(subject).to respond_to(:namespace_project_path) + end + end + + context 'when module is not included' do + subject do + Class.new.include(described_class.url_helpers).new + end + + it 'exposes url helpers module through a method' do + expect(subject).to respond_to(:namespace_project_path) + end + end +end -- cgit v1.2.1 From f62e6081fd114641e41ffa1614d16c30a742cbaa Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 13 Dec 2016 14:58:56 +0100 Subject: Update manual build icon SVG --- app/views/shared/icons/_icon_status_manual.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/icons/_icon_status_manual.svg b/app/views/shared/icons/_icon_status_manual.svg index ffb00c6f443..c98839f51a9 100755 --- a/app/views/shared/icons/_icon_status_manual.svg +++ b/app/views/shared/icons/_icon_status_manual.svg @@ -1 +1 @@ -<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M17.8571429,10 C17.8571429,5.71428571 14.2857143,2.14285714 10,2.14285714 C5.71428571,2.14285714 2.14285714,5.71428571 2.14285714,10 C2.14285714,14.2857143 5.71428571,17.8571429 10,17.8571429 C14.2857143,17.8571429 17.8571429,14.2857143 17.8571429,10 M0,10 C0,4.42857143 4.42857143,0 10,0 C15.5714286,0 20,4.42857143 20,10 C20,15.5714286 15.5714286,20 10,20 C4.42857143,20 0,15.5714286 0,10"/><path d="M15,10.9006211 L15,9.09937888 L13.875,8.91304348 C13.8125,8.66459627 13.6875,8.41614907 13.5,8.04347826 L14.1875,7.11180124 L12.875,5.80745342 L11.9375,6.49068323 C11.625,6.30434783 11.3125,6.18012422 11.0625,6.11801242 L10.9375,5 L9.125,5 L8.9375,6.11801242 C8.625,6.18012422 8.375,6.30434783 8.0625,6.49068323 L7.125,5.80745342 L5.8125,7.11180124 L6.4375,8.04347826 C6.25,8.35403727 6.1875,8.60248447 6.0625,8.91304348 L5,9.03726708 L5,10.8385093 L6.125,11.0248447 C6.1875,11.3354037 6.3125,11.5838509 6.5,11.8944099 L5.875,12.826087 L7.1875,14.1304348 L8.125,13.5093168 C8.375,13.6335404 8.6875,13.757764 9,13.8819876 L9.1875,15 L11.0625,15 L11.25,13.8819876 C11.5625,13.8198758 11.8125,13.6956522 12.125,13.5093168 L13.0625,14.1925466 L14.375,12.8881988 L13.6875,11.9565217 C13.875,11.6459627 14,11.3354037 14.0625,11.0869565 L15,10.9006211 L15,10.9006211 Z M10,11.8322981 C8.9375,11.8322981 8.125,11.0248447 8.125,9.9689441 C8.125,8.91304348 8.9375,8.10559006 10,8.10559006 C11.0625,8.10559006 11.875,8.91304348 11.875,9.9689441 C11.875,11.0248447 11.0625,11.8322981 10,11.8322981 L10,11.8322981 Z"/></svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></svg> -- cgit v1.2.1 From 6328f26c41114c4bc6606097a718022fac2bb168 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Thu, 8 Dec 2016 18:28:49 +0200 Subject: Show full path in header UI for nested groups/projects Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- app/helpers/groups_helper.rb | 11 +++++++++-- app/helpers/projects_helper.rb | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index f6d4ea4659a..77dc9e7d538 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -12,11 +12,18 @@ module GroupsHelper end def group_title(group, name = nil, url = nil) - full_title = link_to(simple_sanitize(group.name), group_path(group)) + full_title = '' + + group.parents.each do |parent| + full_title += link_to(simple_sanitize(parent.name), group_path(parent)) + full_title += ' / '.html_safe + end + + full_title += link_to(simple_sanitize(group.name), group_path(group)) full_title += ' · '.html_safe + link_to(simple_sanitize(name), url) if name content_tag :span do - full_title + full_title.html_safe end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 9cda3b78761..d2177f683a1 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -52,7 +52,7 @@ module ProjectsHelper def project_title(project) namespace_link = if project.group - link_to(simple_sanitize(project.group.name), group_path(project.group)) + group_title(project.group) else owner = project.namespace.owner link_to(simple_sanitize(owner.name), user_path(owner)) @@ -390,7 +390,7 @@ module ProjectsHelper "success" end end - + def readme_cache_key sha = @project.commit.try(:sha) || 'nil' [@project.path_with_namespace, sha, "readme"].join('-') -- cgit v1.2.1 From 8128ce3054749401acfee2c91d38e0e731253624 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Thu, 8 Dec 2016 18:30:11 +0200 Subject: Show full path for nested groups at dashboard groups list Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- app/models/namespace.rb | 8 ++++++++ app/views/shared/groups/_group.html.haml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 37374044551..464be910f5a 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -162,6 +162,14 @@ class Namespace < ActiveRecord::Base end end + def full_name + if parent + parent.full_name + ' / ' + name + else + name + end + end + private def repository_storage_paths diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index 19221e3391f..8164f61797c 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -28,7 +28,7 @@ = image_tag group_icon(group), class: "avatar s40 hidden-xs" .title = link_to group, class: 'group-name' do - = group.name + = group.full_name - if group_member as -- cgit v1.2.1 From 5c06875c39c8b525db41e73560f8cb1746119dd9 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Thu, 8 Dec 2016 21:11:20 +0200 Subject: Use full group name for admin group index and show pages Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- app/views/admin/groups/_group.html.haml | 2 +- app/views/admin/groups/show.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml index 664bb417c6a..eb4de01fb18 100644 --- a/app/views/admin/groups/_group.html.haml +++ b/app/views/admin/groups/_group.html.haml @@ -20,7 +20,7 @@ = image_tag group_icon(group), class: "avatar s40 hidden-xs" .title = link_to [:admin, group], class: 'group-name' do - = group.name + = group.full_name - if group.description.present? .description diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 40871e32913..969b59ef232 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -1,6 +1,6 @@ - page_title @group.name, "Groups" %h3.page-title - Group: #{@group.name} + Group: #{@group.full_name} = link_to edit_admin_group_path(@group), class: "btn pull-right" do %i.fa.fa-pencil-square-o -- cgit v1.2.1 From d95b709a66a5597dced25a2b9df9a1e24fc6d49a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Tue, 13 Dec 2016 15:53:00 +0100 Subject: Be smarter when finding a sudoed user in API::Helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- app/models/user.rb | 4 ---- changelogs/unreleased/25482-fix-api-sudo.yml | 4 ++-- lib/api/helpers.rb | 24 +++++++++++------------- spec/models/user_spec.rb | 11 ----------- spec/requests/api/helpers_spec.rb | 4 ++-- 5 files changed, 15 insertions(+), 32 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index b9bb4a9e3f7..1bd28203523 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -304,10 +304,6 @@ class User < ActiveRecord::Base personal_access_token.user if personal_access_token end - def by_username_or_id(name_or_id) - find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i) - end - # Returns a user for the given SSH key. def find_by_ssh_key_id(key_id) find_by(id: Key.unscoped.select(:user_id).where(id: key_id)) diff --git a/changelogs/unreleased/25482-fix-api-sudo.yml b/changelogs/unreleased/25482-fix-api-sudo.yml index 3b23bfd3a21..4c11fe1622e 100644 --- a/changelogs/unreleased/25482-fix-api-sudo.yml +++ b/changelogs/unreleased/25482-fix-api-sudo.yml @@ -1,4 +1,4 @@ --- -title: 'API: Memoize the current_user so that the sudo can work properly' +title: 'API: Memoize the current_user so that sudo can work properly' merge_request: 8017 -author: +author: diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 2041f0dac6b..8260fcab80a 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -34,6 +34,14 @@ module API @available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute end + def find_user(id) + if id =~ /^\d+$/ + User.find_by(id: id) + else + User.find_by(username: id) + end + end + def find_project(id) if id =~ /^\d+$/ Project.find_by(id: id) @@ -349,7 +357,7 @@ module API def sudo! return unless sudo_identifier - return unless initial_current_user.is_a?(User) + return unless initial_current_user unless initial_current_user.is_admin? forbidden!('Must be admin to use sudo') @@ -360,7 +368,7 @@ module API forbidden!('Private token must be specified in order to use sudo') end - sudoed_user = User.by_username_or_id(sudo_identifier) + sudoed_user = find_user(sudo_identifier) if sudoed_user @current_user = sudoed_user @@ -370,17 +378,7 @@ module API end def sudo_identifier - return @sudo_identifier if defined?(@sudo_identifier) - - identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER] - - # Regex for integers - @sudo_identifier = - if !!(identifier =~ /\A[0-9]+\z/) - identifier.to_i - else - identifier - end + @sudo_identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER] end def add_pagination_headers(paginated_data) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index bad6ed9e146..8b20ee81614 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -727,17 +727,6 @@ describe User, models: true do end end - describe 'by_username_or_id' do - let(:user1) { create(:user, username: 'foo') } - - it "gets the correct user" do - expect(User.by_username_or_id(user1.id)).to eq(user1) - expect(User.by_username_or_id('foo')).to eq(user1) - expect(User.by_username_or_id(-1)).to be_nil - expect(User.by_username_or_id('bar')).to be_nil - end - end - describe '.find_by_ssh_key_id' do context 'using an existing SSH key ID' do let(:user) { create(:user) } diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 573154f69b4..4035fd97af5 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -16,14 +16,14 @@ describe API::Helpers, api: true do clear_env clear_param env[API::Helpers::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token - env[API::Helpers::SUDO_HEADER] = identifier + env[API::Helpers::SUDO_HEADER] = identifier.to_s end def set_param(user_or_token, identifier) clear_env clear_param params[API::Helpers::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token - params[API::Helpers::SUDO_PARAM] = identifier + params[API::Helpers::SUDO_PARAM] = identifier.to_s end def clear_env -- cgit v1.2.1 From d806230f9f95a2d08253f6c534ba69d1b9a498ea Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Tue, 13 Dec 2016 16:59:49 +0200 Subject: Add parents method to Namespace Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- app/models/namespace.rb | 25 ++++++++++++++++++++----- spec/models/namespace_spec.rb | 8 ++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 464be910f5a..b3cefc01b99 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -163,11 +163,26 @@ class Namespace < ActiveRecord::Base end def full_name - if parent - parent.full_name + ' / ' + name - else - name - end + @full_name ||= + if parent + parent.full_name + ' / ' + name + else + name + end + end + + def parents + @parents ||= + begin + parents = [] + + if parent + parents << parent + parents += parent.parents + end + + parents + end end private diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 7f82e85563b..265ffc330e3 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -132,4 +132,12 @@ describe Namespace, models: true do it { expect(group.full_path).to eq(group.path) } it { expect(nested_group.full_path).to eq("#{group.path}/#{nested_group.path}") } end + + describe '#parents' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + let(:deep_nested_group) { create(:group, parent: nested_group) } + + it { expect(deep_nested_group.parents).to eq([nested_group, group]) } + end end -- cgit v1.2.1 From 78115dc13af2b72d87ddbe2c5e42b24d75cd1b17 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Tue, 13 Dec 2016 17:00:06 +0200 Subject: Use full_name for Group in UI Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- app/models/group.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/group.rb b/app/models/group.rb index 4248e1162d8..ac8a82c8c1e 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -83,7 +83,7 @@ class Group < Namespace end def human_name - name + full_name end def visibility_level_field -- cgit v1.2.1 From 1a81fcfbd8261862c9614f0ea317af02ae20b24a Mon Sep 17 00:00:00 2001 From: Robert Schilling <rschilling@student.tugraz.at> Date: Mon, 12 Dec 2016 13:04:13 +0100 Subject: API: Ability to cherry-pick a commit --- changelogs/unreleased/api-cherry-pick.yml | 4 ++ doc/api/commits.md | 39 +++++++++++++++++ lib/api/commits.rb | 36 +++++++++++++++- spec/requests/api/commits_spec.rb | 70 +++++++++++++++++++++++++++++++ 4 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/api-cherry-pick.yml diff --git a/changelogs/unreleased/api-cherry-pick.yml b/changelogs/unreleased/api-cherry-pick.yml new file mode 100644 index 00000000000..5f4cee450b9 --- /dev/null +++ b/changelogs/unreleased/api-cherry-pick.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Ability to cherry pick a commit' +merge_request: 8047 +author: Robert Schilling diff --git a/doc/api/commits.md b/doc/api/commits.md index 0170af00e0e..5c11d0f83bb 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -183,6 +183,44 @@ Example response: } ``` +## Cherry pick a commit + +> [Introduced][ce-8047] in GitLab 8.15. + +Cherry picks a commit to a given branch. + +``` +POST /projects/:id/repository/commits/:sha/cherry_pick +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user +| `sha` | string | yes | The commit hash | +| `branch` | string | yes | The name of the branch | + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "branch=master" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/cherry_pick" +``` + +Example response: + +```json +{ + "id": "8b090c1b79a14f2bd9e8a738f717824ff53aebad", + "short_id": "8b090c1b", + "title": "Feature added", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "created_at": "2016-12-12T20:10:39.000+01:00", + "committer_name": "Administrator", + "committer_email": "admin@example.com", + "message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n" +} +``` + ## Get the diff of a commit Get the diff of a commit in a project. @@ -438,3 +476,4 @@ Example response: ``` [ce-6096]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6096 "Multi-file commit" +[ce-8047]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8047 diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 2670a2d413a..cf2489dbb67 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -1,7 +1,6 @@ require 'mime/types' module API - # Projects commits API class Commits < Grape::API include PaginationParams @@ -121,6 +120,41 @@ module API present paginate(notes), with: Entities::CommitNote end + desc 'Cherry pick commit into a branch' do + detail 'This feature was introduced in GitLab 8.15' + success Entities::RepoCommit + end + params do + requires :sha, type: String, desc: 'A commit sha to be cherry picked' + requires :branch, type: String, desc: 'The name of the branch' + end + post ':id/repository/commits/:sha/cherry_pick' do + authorize! :push_code, user_project + + commit = user_project.commit(params[:sha]) + not_found!('Commit') unless commit + + branch = user_project.repository.find_branch(params[:branch]) + not_found!('Branch') unless branch + + commit_params = { + commit: commit, + create_merge_request: false, + source_project: user_project, + source_branch: commit.cherry_pick_branch_name, + target_branch: params[:branch] + } + + result = ::Commits::CherryPickService.new(user_project, current_user, commit_params).execute + + if result[:status] == :success + branch = user_project.repository.find_branch(params[:branch]) + present user_project.repository.commit(branch.dereferenced_target), with: Entities::RepoCommit + else + render_api_error!(result[:message], 400) + end + end + desc 'Post comment to commit' do success Entities::CommitNote end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index e497bce6943..fd600798eeb 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -456,6 +456,76 @@ describe API::Commits, api: true do end end + describe 'POST :id/repository/commits/:sha/cherry_pick' do + let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') } + + context 'authorized user' do + it 'cherry picks a commit' do + post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'master' + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq(master_pickable_commit.title) + expect(json_response['message']).to eq(master_pickable_commit.message) + expect(json_response['author_name']).to eq(master_pickable_commit.author_name) + expect(json_response['committer_name']).to eq(user.name) + end + + it 'returns 400 if commit is already included in the target branch' do + post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'markdown' + + expect(response).to have_http_status(400) + expect(json_response['message']).to eq('Sorry, we cannot cherry-pick this commit automatically. + It may have already been cherry-pick, or a more recent commit may have updated some of its content.') + end + + it 'returns 400 if you are not allowed to push to the target branch' do + project.team << [user2, :developer] + protected_branch = create(:protected_branch, project: project, name: 'feature') + + post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user2), branch: 'feature' + + expect(response).to have_http_status(400) + expect(json_response['message']).to eq('You are not allowed to push into this branch') + end + + it 'returns 400 for missing parameters' do + post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user) + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('branch is missing') + end + + it 'returns 404 if commit is not found' do + post api("/projects/#{project.id}/repository/commits/abcd0123/cherry_pick", user), branch: 'master' + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Commit Not Found') + end + + it 'returns 404 if branch is not found' do + post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'foo' + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Branch Not Found') + end + + it 'returns 400 for missing parameters' do + post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user) + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('branch is missing') + end + end + + context 'unauthorized user' do + it 'does not cherry pick the commit' do + post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick"), branch: 'master' + + expect(response).to have_http_status(401) + end + end + end + describe 'Post comment to commit' do context 'authorized user' do it 'returns comment' do -- cgit v1.2.1 From 51cfd554aff461a4e8649459a53562e990321b3a Mon Sep 17 00:00:00 2001 From: Robert Schilling <rschilling@student.tugraz.at> Date: Tue, 13 Dec 2016 08:26:14 +0100 Subject: Make rubocop happy --- spec/requests/api/commits_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index fd600798eeb..5ce229a8cf2 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -482,7 +482,7 @@ describe API::Commits, api: true do project.team << [user2, :developer] protected_branch = create(:protected_branch, project: project, name: 'feature') - post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user2), branch: 'feature' + post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user2), branch: protected_branch.name expect(response).to have_http_status(400) expect(json_response['message']).to eq('You are not allowed to push into this branch') -- cgit v1.2.1 From 44085354620458ceea3a941b49dc926e38f84a01 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Tue, 13 Dec 2016 12:32:27 +0000 Subject: Prevent overflow with vertical scroll when we have space to show content Adds changelog --- app/assets/stylesheets/pages/environments.scss | 8 +++++--- app/assets/stylesheets/pages/pipelines.scss | 17 +++++++---------- changelogs/unreleased/20052-actions-table-vscroll.yml | 4 ++++ 3 files changed, 16 insertions(+), 13 deletions(-) create mode 100644 changelogs/unreleased/20052-actions-table-vscroll.yml diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index de3d2ba549f..e716f24c8e2 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -1,6 +1,8 @@ -.deployments-container { - width: 100%; - overflow: auto; +@media (max-width: $screen-md-max) { + .deployments-container { + width: 100%; + overflow: auto; + } } .environments-list-loading { diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 08062b85504..6822f916cc5 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -37,12 +37,13 @@ } } -.content-list { - - &.pipelines, - &.builds-content-list { - width: 100%; - overflow: auto; +@media (max-width: $screen-md-max) { + .content-list { + &.pipelines, + &.builds-content-list { + width: 100%; + overflow: auto; + } } } @@ -666,10 +667,6 @@ min-width: 900px; } - .content-list.pipelines { - overflow: auto; - } - .stage { max-width: 100px; width: 100px; diff --git a/changelogs/unreleased/20052-actions-table-vscroll.yml b/changelogs/unreleased/20052-actions-table-vscroll.yml new file mode 100644 index 00000000000..779cd08de09 --- /dev/null +++ b/changelogs/unreleased/20052-actions-table-vscroll.yml @@ -0,0 +1,4 @@ +--- +title: Prevent overflow with vertical scroll when we have space to show content +merge_request: 8061 +author: -- cgit v1.2.1 From 43af4e5577e5ea338ea5cf072cd0635e3dabcc9e Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Tue, 13 Dec 2016 16:49:17 +0100 Subject: Encode when migrating ProcessCommitWorker jobs If the source encoding is not UTF-8 we need to encode the data as `JSON.dump` may throw an error if the input can not be converted to UTF-8. We only encode when necessary to reduce the overhead. Fixes gitlab-org/gitlab-ce#25489 --- .../process-commit-worker-migration-encoding.yml | 4 ++++ ...124141322_migrate_process_commit_worker_jobs.rb | 20 ++++++++++++---- .../migrate_process_commit_worker_jobs_spec.rb | 27 ++++++++++++++++++++-- 3 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 changelogs/unreleased/process-commit-worker-migration-encoding.yml diff --git a/changelogs/unreleased/process-commit-worker-migration-encoding.yml b/changelogs/unreleased/process-commit-worker-migration-encoding.yml new file mode 100644 index 00000000000..26aabd9b647 --- /dev/null +++ b/changelogs/unreleased/process-commit-worker-migration-encoding.yml @@ -0,0 +1,4 @@ +--- +title: Encode input when migrating ProcessCommitWorker jobs to prevent migration errors +merge_request: +author: diff --git a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb index 453a44e271a..77e0c40d850 100644 --- a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb +++ b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb @@ -47,14 +47,14 @@ class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration hash = { id: commit.oid, - message: commit.message, + message: encode(commit.message), parent_ids: commit.parent_ids, authored_date: commit.author[:time], - author_name: commit.author[:name], - author_email: commit.author[:email], + author_name: encode(commit.author[:name]), + author_email: encode(commit.author[:email]), committed_date: commit.committer[:time], - committer_email: commit.committer[:email], - committer_name: commit.committer[:name] + committer_email: encode(commit.committer[:email]), + committer_name: encode(commit.committer[:name]) } payload['args'][2] = hash @@ -89,4 +89,14 @@ class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration end end end + + def encode(data) + encoding = Encoding::UTF_8 + + if data.encoding == encoding + data + else + data.encode(encoding, invalid: :replace, undef: :replace) + end + end end diff --git a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb index 52428547a9f..6a93deb5412 100644 --- a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb +++ b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb @@ -1,3 +1,5 @@ +# encoding: utf-8 + require 'spec_helper' require Rails.root.join('db', 'migrate', '20161124141322_migrate_process_commit_worker_jobs.rb') @@ -59,6 +61,10 @@ describe MigrateProcessCommitWorkerJobs do Sidekiq.redis { |r| r.llen('queue:process_commit') } end + def pop_job + JSON.load(Sidekiq.redis { |r| r.lpop('queue:process_commit') }) + end + before do Sidekiq.redis do |redis| job = JSON.dump(args: [project.id, user.id, commit.oid]) @@ -92,11 +98,28 @@ describe MigrateProcessCommitWorkerJobs do expect(job_count).to eq(1) end + it 'encodes data to UTF-8' do + allow_any_instance_of(Rugged::Repository).to receive(:lookup). + with(commit.oid). + and_return(commit) + + allow(commit).to receive(:message). + and_return('김치'.force_encoding('BINARY')) + + migration.up + + job = pop_job + + # We don't care so much about what is being stored, instead we just want + # to make sure the encoding is right so that JSON encoding the data + # doesn't produce any errors. + expect(job['args'][2]['message'].encoding).to eq(Encoding::UTF_8) + end + context 'a migrated job' do let(:job) do migration.up - - JSON.load(Sidekiq.redis { |r| r.lpop('queue:process_commit') }) + pop_job end let(:commit_hash) do -- cgit v1.2.1 From 0057ed1e69bc203d82fd3e8dfa6db7ea6a9b1de7 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Tue, 13 Dec 2016 17:59:21 +0200 Subject: BB importer: Fixed after code review[ci skip] --- lib/bitbucket/paginator.rb | 2 +- spec/lib/bitbucket/collection_spec.rb | 1 + spec/lib/bitbucket/connection_spec.rb | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/bitbucket/paginator.rb b/lib/bitbucket/paginator.rb index b38cd99855c..135d0d55674 100644 --- a/lib/bitbucket/paginator.rb +++ b/lib/bitbucket/paginator.rb @@ -29,7 +29,7 @@ module Bitbucket end def fetch_next_page - parsed_response = connection.get(next_url, { pagelen: PAGE_LENGTH, sort: :created_on }) + parsed_response = connection.get(next_url, pagelen: PAGE_LENGTH, sort: :created_on) Page.new(parsed_response, type) end end diff --git a/spec/lib/bitbucket/collection_spec.rb b/spec/lib/bitbucket/collection_spec.rb index eeed61b0488..015a7f80e03 100644 --- a/spec/lib/bitbucket/collection_spec.rb +++ b/spec/lib/bitbucket/collection_spec.rb @@ -18,6 +18,7 @@ end describe Bitbucket::Collection do it "iterates paginator" do collection = described_class.new(TestPaginator.new) + expect(collection.to_a).to match(["result_1_page_1", "result_2_page_1", "result_1_page_2", "result_2_page_2"]) end end diff --git a/spec/lib/bitbucket/connection_spec.rb b/spec/lib/bitbucket/connection_spec.rb index 5242c6fac34..6be681a8b47 100644 --- a/spec/lib/bitbucket/connection_spec.rb +++ b/spec/lib/bitbucket/connection_spec.rb @@ -4,7 +4,9 @@ describe Bitbucket::Connection do describe '#get' do it 'calls OAuth2::AccessToken::get' do expect_any_instance_of(OAuth2::AccessToken).to receive(:get).and_return(double(parsed: true)) + connection = described_class.new({}) + connection.get('/users') end end @@ -12,6 +14,7 @@ describe Bitbucket::Connection do describe '#expired?' do it 'calls connection.expired?' do expect_any_instance_of(OAuth2::AccessToken).to receive(:expired?).and_return(true) + expect(described_class.new({}).expired?).to be_truthy end end @@ -19,7 +22,9 @@ describe Bitbucket::Connection do describe '#refresh!' do it 'calls connection.refresh!' do response = double(token: nil, expires_at: nil, expires_in: nil, refresh_token: nil) + expect_any_instance_of(OAuth2::AccessToken).to receive(:refresh!).and_return(response) + described_class.new({}).refresh! end end -- cgit v1.2.1 From 0469e7dffd13646a211a456341c9459b8ff5f7ad Mon Sep 17 00:00:00 2001 From: jurre <jurrestender+github@gmail.com> Date: Tue, 13 Dec 2016 11:17:32 +0100 Subject: Move admin settings spinach feature to rspec --- features/admin/settings.feature | 19 --------- features/steps/admin/settings.rb | 62 ------------------------------ features/steps/shared/paths.rb | 4 -- spec/features/admin/admin_settings_spec.rb | 53 +++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 85 deletions(-) delete mode 100644 features/admin/settings.feature delete mode 100644 features/steps/admin/settings.rb create mode 100644 spec/features/admin/admin_settings_spec.rb diff --git a/features/admin/settings.feature b/features/admin/settings.feature deleted file mode 100644 index e38eea2cfed..00000000000 --- a/features/admin/settings.feature +++ /dev/null @@ -1,19 +0,0 @@ -@admin -Feature: Admin Settings - Background: - Given I sign in as an admin - And I visit admin settings page - - Scenario: Change application settings - When I modify settings and save form - Then I should see application settings saved - - Scenario: Change Slack Service Template settings - When I click on "Service Templates" - And I click on "Slack" service - And I fill out Slack settings - Then I check all events and submit form - And I should see service template settings saved - Then I click on "Slack" service - And I should see all checkboxes checked - And I should see Slack settings saved diff --git a/features/steps/admin/settings.rb b/features/steps/admin/settings.rb deleted file mode 100644 index 11dc7f580f0..00000000000 --- a/features/steps/admin/settings.rb +++ /dev/null @@ -1,62 +0,0 @@ -class Spinach::Features::AdminSettings < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedAdmin - include Gitlab::CurrentSettings - - step 'I modify settings and save form' do - uncheck 'Gravatar enabled' - fill_in 'Home page URL', with: 'https://about.gitlab.com/' - fill_in 'Help page text', with: 'Example text' - click_button 'Save' - end - - step 'I should see application settings saved' do - expect(current_application_settings.gravatar_enabled).to be_falsey - expect(current_application_settings.home_page_url).to eq "https://about.gitlab.com/" - expect(page).to have_content "Application settings saved successfully" - end - - step 'I click on "Service Templates"' do - click_link 'Service Templates' - end - - step 'I click on "Slack" service' do - click_link 'Slack' - end - - step 'I check all events and submit form' do - page.check('Active') - page.check('Push') - page.check('Tag push') - page.check('Note') - page.check('Issue') - page.check('Merge request') - page.check('Build') - page.check('Pipeline') - click_on 'Save' - end - - step 'I fill out Slack settings' do - fill_in 'Webhook', with: 'http://localhost' - fill_in 'Username', with: 'test_user' - fill_in 'service_push_channel', with: '#test_channel' - page.check('Notify only broken builds') - end - - step 'I should see service template settings saved' do - expect(page).to have_content 'Application settings saved successfully' - end - - step 'I should see all checkboxes checked' do - page.all('input[type=checkbox]').each do |checkbox| - expect(checkbox).to be_checked - end - end - - step 'I should see Slack settings saved' do - expect(find_field('Webhook').value).to eq 'http://localhost' - expect(find_field('Username').value).to eq 'test_user' - expect(find('#service_push_channel').value).to eq '#test_channel' - end -end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index 2bd8ea745e4..d36eff5cf16 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -203,10 +203,6 @@ module SharedPaths visit admin_teams_path end - step 'I visit admin settings page' do - visit admin_application_settings_path - end - step 'I visit spam logs page' do visit admin_spam_logs_path end diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb new file mode 100644 index 00000000000..8cd66f189be --- /dev/null +++ b/spec/features/admin/admin_settings_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +feature 'Admin updates settings', feature: true do + before(:each) do + login_as :admin + visit admin_application_settings_path + end + + scenario 'Change application settings' do + uncheck 'Gravatar enabled' + fill_in 'Home page URL', with: 'https://about.gitlab.com/' + fill_in 'Help page text', with: 'Example text' + click_button 'Save' + + expect(current_application_settings.gravatar_enabled).to be_falsey + expect(current_application_settings.home_page_url).to eq "https://about.gitlab.com/" + expect(page).to have_content "Application settings saved successfully" + end + + scenario 'Change Slack Service template settings' do + click_link 'Service Templates' + click_link 'Slack' + fill_in 'Webhook', with: 'http://localhost' + fill_in 'Username', with: 'test_user' + fill_in 'service_push_channel', with: '#test_channel' + page.check('Notify only broken builds') + + check_all_events + click_on 'Save' + + expect(page).to have_content 'Application settings saved successfully' + + click_link 'Slack' + + page.all('input[type=checkbox]').each do |checkbox| + expect(checkbox).to be_checked + end + expect(find_field('Webhook').value).to eq 'http://localhost' + expect(find_field('Username').value).to eq 'test_user' + expect(find('#service_push_channel').value).to eq '#test_channel' + end + + def check_all_events + page.check('Active') + page.check('Push') + page.check('Tag push') + page.check('Note') + page.check('Issue') + page.check('Merge request') + page.check('Build') + page.check('Pipeline') + end +end -- cgit v1.2.1 From 3792e747a7dad9d68b8c6e575929ee17cd852c77 Mon Sep 17 00:00:00 2001 From: Semyon Pupkov <mail@semyonpupkov.com> Date: Tue, 13 Dec 2016 22:46:06 +0500 Subject: Use build instead create in group spec --- spec/models/group_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 850b1a3cf1e..c1ffba4d12c 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -273,7 +273,7 @@ describe Group, models: true do end describe 'nested group' do - subject { create(:group, :nested) } + subject { build(:group, :nested) } it { is_expected.to be_valid } it { expect(subject.parent).to be_kind_of(Group) } -- cgit v1.2.1 From f8a17a383562a1ce623e64d7b5f2902052432b2f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Tue, 13 Dec 2016 20:27:08 +0200 Subject: Add tests for Namespace#full_name method Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- spec/models/namespace_spec.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 265ffc330e3..90e8f6b7227 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -133,6 +133,14 @@ describe Namespace, models: true do it { expect(nested_group.full_path).to eq("#{group.path}/#{nested_group.path}") } end + describe '#full_name' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + + it { expect(group.full_name).to eq(group.name) } + it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") } + end + describe '#parents' do let(:group) { create(:group) } let(:nested_group) { create(:group, parent: group) } -- cgit v1.2.1 From c5d12e3706ee6bc41d6be897e923b6c20a267078 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Mon, 12 Dec 2016 15:31:11 -0600 Subject: Remove duplicate shades of gray and black --- app/assets/stylesheets/framework/buttons.scss | 2 +- app/assets/stylesheets/framework/dropdowns.scss | 10 +++++----- app/assets/stylesheets/framework/forms.scss | 2 +- app/assets/stylesheets/framework/header.scss | 6 +++--- app/assets/stylesheets/framework/nav.scss | 2 +- app/assets/stylesheets/framework/page-header.scss | 2 +- app/assets/stylesheets/framework/variables.scss | 10 ++-------- app/assets/stylesheets/framework/zen.scss | 2 +- app/assets/stylesheets/pages/boards.scss | 2 +- app/assets/stylesheets/pages/builds.scss | 4 ++-- app/assets/stylesheets/pages/commits.scss | 4 ++-- app/assets/stylesheets/pages/environments.scss | 8 ++++---- app/assets/stylesheets/pages/issuable.scss | 4 ++-- app/assets/stylesheets/pages/merge_requests.scss | 2 +- app/assets/stylesheets/pages/milestone.scss | 2 +- app/assets/stylesheets/pages/pipelines.scss | 10 +++++----- app/assets/stylesheets/pages/settings.scss | 2 +- app/assets/stylesheets/pages/status.scss | 8 ++++---- 18 files changed, 38 insertions(+), 44 deletions(-) diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 1c7b2f4df7c..d87a2620376 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -331,7 +331,7 @@ margin-left: 10px; i { - color: $gl-icon-color; + color: $gl-gray-light; } } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index d5914b900e2..628e426891e 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -35,7 +35,7 @@ .dropdown-toggle { padding: 6px 8px 6px 10px; background-color: $dropdown-toggle-bg; - color: $dropdown-toggle-color; + color: $gl-text-color; font-size: 14px; text-align: left; border: 1px solid $border-color; @@ -131,7 +131,7 @@ font-size: 14px; font-weight: normal; padding: 8px 0; - background-color: $dropdown-bg; + background-color: $white-light; border: 1px solid $dropdown-border-color; border-radius: $border-radius-base; box-shadow: 0 2px 4px $dropdown-shadow-color; @@ -202,7 +202,7 @@ } .icon-play { - fill: $table-text-gray; + fill: $gl-gray-light; margin-right: 6px; height: 12px; width: 11px; @@ -621,11 +621,11 @@ .dropdown-menu-inner-content { display: block; - color: $gl-placeholder-color; + color: $gl-gray-light; } .dropdown-toggle-text { &.is-default { - color: $gl-placeholder-color; + color: $gl-gray-light; } } diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 25a2b38baaa..89a51959571 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -149,7 +149,7 @@ label { } .form-control::-webkit-input-placeholder { - color: $gl-placeholder-color; + color: $gl-gray-light; } .input-group { diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index cc2286038c0..74fd97f811e 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -45,7 +45,7 @@ header { padding: 0; .nav > li > a { - color: $gl-icon-color; + color: $gl-gray-light; font-size: 18px; padding: 0; margin: ($header-height - 28) / 2 0; @@ -63,7 +63,7 @@ header { &:focus, &:active { background-color: $background-color; - color: darken($gl-icon-color, 30%); + color: darken($gl-gray-light, 30%); .todos-pending-count { background: darken($todo-alert-blue, 10%); @@ -88,7 +88,7 @@ header { } &.active { - color: $gl-icon-color; + color: $gl-gray-light; } } } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index ea77348633d..ea86961eabc 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -317,7 +317,7 @@ .fa-caret-down { margin-left: 5px; - color: $gl-icon-color; + color: $gl-gray-light; } .dropdown { diff --git a/app/assets/stylesheets/framework/page-header.scss b/app/assets/stylesheets/framework/page-header.scss index 85c1385d5d9..fff7d7f7524 100644 --- a/app/assets/stylesheets/framework/page-header.scss +++ b/app/assets/stylesheets/framework/page-header.scss @@ -14,7 +14,7 @@ .header-action-buttons { i { - color: $gl-icon-color; + color: $gl-gray-light; font-size: 13px; margin-right: 3px; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index a1d5f6427f4..4b178a75f58 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -93,7 +93,6 @@ $focus-border-color: #3aabf0; $table-border-color: #f0f0f0; $background-color: $gray-light; $dark-background-color: #f5f5f5; -$table-text-gray: #8f8f8f; $well-expand-item: #e8f2f7; $well-inner-border: #eef0f2; $well-light-border: #f1f1f1; @@ -113,12 +112,11 @@ $gl-text-orange: #d90; $gl-link-color: #3777b0; $gl-diff-text-color: #555; $gl-dark-link-color: #333; -$gl-placeholder-color: #8f8f8f; -$gl-icon-color: $gl-placeholder-color; +$gl-gray-light: #8f8f8f; $gl-grayish-blue: #7f8fa4; $gl-gray: $gl-text-color; $gl-gray-dark: #313236; -$gl-gray-light: $gl-placeholder-color; +$gl-gray-light: $gl-gray-light; $gl-header-color: #4c4e54; /* @@ -272,7 +270,6 @@ $regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-San * Dropdowns */ $dropdown-width: 300px; -$dropdown-bg: #fff; $dropdown-link-color: #555; $dropdown-link-hover-bg: $row-hover; $dropdown-empty-row-bg: rgba(#000, .04); @@ -289,7 +286,6 @@ $dropdown-loading-bg: rgba(#fff, .6); $dropdown-chevron-size: 10px; $dropdown-toggle-bg: #fff; -$dropdown-toggle-color: #5c5c5c; $dropdown-toggle-border-color: #e5e5e5; $dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 13%); $dropdown-toggle-active-border-color: darken($dropdown-toggle-border-color, 14%); @@ -344,7 +340,6 @@ $note-line2-border: #ddd; * Zen */ $zen-control-color: #555; -$zen-control-hover-color: #111; /* * Calendar @@ -373,7 +368,6 @@ $personal-access-tokens-disabled-label-color: #bbb; /* * CI */ -$ci-output-bg: #1d1f21; $ci-text-color: #c5c8c6; $ci-skipped-color: #888; diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index e5c7d70d45a..a1e3038b8de 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -57,6 +57,6 @@ font-size: 36px; &:hover { - color: $zen-control-hover-color; + color: $black; } } diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 0d9cf679e7c..59ba0939785 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -253,7 +253,7 @@ .board-list-count { padding: 10px 0; - color: $gl-placeholder-color; + color: $gl-gray-light; font-size: 13px; > .fa { diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index dcc13f6d74a..bf00b23d787 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -96,7 +96,7 @@ } .build-trace { - background: $ci-output-bg; + background: $black; color: $ci-text-color; white-space: pre; overflow-x: auto; @@ -257,7 +257,7 @@ } .build-light-text { - color: $gl-placeholder-color; + color: $gl-gray-light; } .build-gutter-toggle { diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index c29b5fdea78..c4ee9a51ed5 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -38,7 +38,7 @@ .text-expander { display: inline-block; background: $gray-light; - color: $gl-placeholder-color; + color: $gl-gray-light; padding: 0 5px; cursor: pointer; border: 1px solid $border-gray-dark; @@ -174,7 +174,7 @@ height: 14px; width: 14px; vertical-align: middle; - fill: $table-text-gray; + fill: $gl-gray-light; } } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index de3d2ba549f..96aee3a300a 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -64,14 +64,14 @@ .external-url, .dropdown-new { - color: $table-text-gray; + color: $gl-gray-light; } .dropdown-menu { .fa { margin-right: 6px; - color: $table-text-gray; + color: $gl-gray-light; } } @@ -82,7 +82,7 @@ .stop-env-link, .external-url { - color: $table-text-gray; + color: $gl-gray-light; .stop-env-icon { font-size: 14px; @@ -117,7 +117,7 @@ .badge { font-weight: normal; background-color: $gray-darker; - color: $gl-placeholder-color; + color: $gl-gray-light; vertical-align: baseline; } } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 407c0afbac8..6ef7f9c2c1e 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -169,7 +169,7 @@ } .no-value { - color: $gl-placeholder-color; + color: $gl-gray-light; } .sidebar-collapsed-icon { @@ -333,7 +333,7 @@ margin-left: 5px; a { - color: $gl-placeholder-color; + color: $gl-gray-light; } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 6234779ac19..156724f44a8 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -359,7 +359,7 @@ th { background-color: $white-light; - color: $gl-placeholder-color; + color: $gl-gray-light; } } } diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index dfc6079bd15..77c523d7310 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -108,7 +108,7 @@ margin-top: 7px; .issuable-number { - color: $gl-placeholder-color; + color: $gl-gray-light; margin-right: 5px; } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 08062b85504..656dcd92a06 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -117,7 +117,7 @@ height: 14px; width: 14px; vertical-align: middle; - fill: $table-text-gray; + fill: $gl-gray-light; } .fa { @@ -200,7 +200,7 @@ .duration, .finished-at { - color: $table-text-gray; + color: $gl-gray-light; margin: 4px 0; .fa { @@ -221,7 +221,7 @@ .btn { margin: 0; - color: $table-text-gray; + color: $gl-gray-light; } .cancel-retry-btns { @@ -234,10 +234,10 @@ .dropdown-toggle, .dropdown-menu { - color: $table-text-gray; + color: $gl-gray-light; .fa { - color: $table-text-gray; + color: $gl-gray-light; font-size: 14px; } diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 51c926608f9..ddee2c95247 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -1,5 +1,5 @@ .settings-list-icon { - color: $gl-placeholder-color; + color: $gl-gray-light; font-size: $settings-icon-size; line-height: 42px; } diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 5084b466722..f3b0608e545 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -102,15 +102,15 @@ &.ci-created, &.ci-skipped { - color: $table-text-gray; - border-color: $table-text-gray; + color: $gl-gray-light; + border-color: $gl-gray-light; &:not(span):hover { - background-color: rgba( $table-text-gray, .07); + background-color: rgba( $gl-gray-light, .07); } svg { - fill: $table-text-gray; + fill: $gl-gray-light; } } } -- cgit v1.2.1 From e4a000478f327885fd9dcbc7d47ba77045cef139 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Mon, 12 Dec 2016 15:49:12 -0600 Subject: Combining more grays --- app/assets/stylesheets/framework/buttons.scss | 6 +++--- app/assets/stylesheets/framework/dropdowns.scss | 14 ++++++------- app/assets/stylesheets/framework/issue_box.scss | 2 +- app/assets/stylesheets/framework/lists.scss | 2 +- .../stylesheets/framework/markdown_area.scss | 2 +- app/assets/stylesheets/framework/nav.scss | 4 ++-- app/assets/stylesheets/framework/variables.scss | 23 ++-------------------- app/assets/stylesheets/framework/zen.scss | 2 +- app/assets/stylesheets/pages/builds.scss | 2 +- app/assets/stylesheets/pages/note_form.scss | 2 +- app/assets/stylesheets/pages/notes.scss | 8 ++++---- app/assets/stylesheets/pages/pipelines.scss | 2 +- app/assets/stylesheets/pages/profile.scss | 2 +- app/assets/stylesheets/pages/search.scss | 4 ++-- app/assets/stylesheets/pages/xterm.scss | 2 +- 15 files changed, 29 insertions(+), 48 deletions(-) diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index d87a2620376..e6f03528d41 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -235,7 +235,7 @@ } .btn-transparent { - color: $btn-transparent-color; + color: $gl-gray-light; background-color: transparent; border: 0; @@ -309,7 +309,7 @@ text-align: left; padding: 6px 16px; border-color: $border-color; - color: $btn-placeholder-gray; + color: $gray-darkest; background-color: $background-color; &:hover, @@ -318,7 +318,7 @@ cursor: text; box-shadow: none; border-color: $border-color; - color: $btn-placeholder-gray; + color: $gray-darkest; background-color: $background-color; } } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 628e426891e..fd5f521e4a0 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -10,7 +10,7 @@ @mixin chevron-active { .fa-chevron-down { - color: $dropdown-toggle-hover-icon-color; + color: $gray-darkest; } } @@ -28,7 +28,7 @@ .dropdown-toggle, .dropdown-menu-toggle { @include chevron-active; - border-color: $dropdown-toggle-hover-border-color; + border-color: $gray-darkest; } } @@ -73,7 +73,7 @@ } .fa { - color: $dropdown-toggle-icon-color; + color: $gray-darkest; } .fa-chevron-down { @@ -85,7 +85,7 @@ &:hover { @include chevron-active; - border-color: $dropdown-toggle-hover-border-color; + border-color: $gray-darkest; } &:focus:active { @@ -210,7 +210,7 @@ } .dropdown-header { - color: $dropdown-header-color; + color: $gl-gray-light; font-size: 13px; line-height: 22px; padding: 0 10px; @@ -223,7 +223,7 @@ .unclickable { cursor: not-allowed; padding: 5px 8px; - color: $dropdown-header-color; + color: $gl-gray-light; } } @@ -602,7 +602,7 @@ th { padding: 2px 0; - color: $calendar-header-color; + color: $note-disabled-comment-color; font-weight: normal; text-transform: lowercase; border-top: 1px solid $calendar-border-color; diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index 44834a84234..298913108ee 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -41,6 +41,6 @@ } &.status-box-upcoming { - background: $issue-box-upcoming-bg; + background: $gl-gray-light; } } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index ed4b60faf92..389ead37366 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -224,7 +224,7 @@ ul.content-list { } .label-default { - color: $btn-transparent-color; + color: $gl-gray-light; } } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 59a30d31ac7..cf298773d58 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -135,7 +135,7 @@ .toolbar-btn { float: left; padding: 0 5px; - color: $note-toolbar-color; + color: $gl-gray-light; background: transparent; border: 0; outline: 0; diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index ea86961eabc..26d290976dc 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -51,7 +51,7 @@ margin-bottom: -1px; font-size: 14px; line-height: 28px; - color: $note-toolbar-color; + color: $gl-gray-light; border-bottom: 2px solid transparent; &:hover, @@ -80,7 +80,7 @@ .badge { font-weight: normal; background-color: $nav-badge-bg; - color: $btn-transparent-color; + color: $gl-gray-light; vertical-align: baseline; } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 4b178a75f58..00ac5e82181 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -26,7 +26,7 @@ $gray-lighter: #f9f9f9; $gray-normal: darken($gray-light, $darken-normal-factor); $gray-dark: darken($gray-light, $darken-dark-factor); $gray-darker: #eee; -$gray-darkest: #c9c9c9; +$gray-darkest: #c4c4c4; $green-light: #3cbd70; $green-normal: darken($green-light, $darken-normal-factor); @@ -116,7 +116,6 @@ $gl-gray-light: #8f8f8f; $gl-grayish-blue: #7f8fa4; $gl-gray: $gl-text-color; $gl-gray-dark: #313236; -$gl-gray-light: $gl-gray-light; $gl-header-color: #4c4e54; /* @@ -166,7 +165,6 @@ $header-height: 50px; $fixed-layout-width: 1280px; $error-exclamation-point: #e62958; $border-radius-default: 2px; -$btn-transparent-color: #8f8f8f; $settings-icon-size: 18px; $provider-btn-group-border: #e5e5e5; $provider-btn-not-active-color: #4688f1; @@ -191,7 +189,6 @@ $count-arrow-border: #dce0e5; $save-project-loader-color: #555; $divergence-graph-bar-bg: #ccc; $divergence-graph-separator-bg: #ccc; -$issue-box-upcoming-bg: #8f8f8f; /* * Common component specific colors @@ -276,7 +273,6 @@ $dropdown-empty-row-bg: rgba(#000, .04); $dropdown-border-color: $border-color; $dropdown-shadow-color: rgba(#000, .1); $dropdown-divider-color: rgba(#000, .1); -$dropdown-header-color: #959494; $dropdown-title-btn-color: #bfbfbf; $dropdown-input-color: #555; $dropdown-input-fa-color: #c7c7c7; @@ -287,18 +283,14 @@ $dropdown-chevron-size: 10px; $dropdown-toggle-bg: #fff; $dropdown-toggle-border-color: #e5e5e5; -$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 13%); $dropdown-toggle-active-border-color: darken($dropdown-toggle-border-color, 14%); -$dropdown-toggle-icon-color: #c4c4c4; -$dropdown-toggle-hover-icon-color: darken($dropdown-toggle-icon-color, 7%); + /* * Buttons */ $btn-active-gray: #ececec; $btn-active-gray-light: e4e7ed; -$btn-placeholder-gray: #c7c7c7; -$btn-white-active: #848484; $btn-gray-hover: #eee; /* @@ -315,22 +307,18 @@ $award-emoji-new-btn-icon-color: #dcdcdc; $search-input-border-color: rgba(#4688f1, .8); $search-input-focus-shadow-color: $dropdown-input-focus-shadow; $search-input-width: 220px; -$location-badge-color: #aaa; $location-badge-bg: $dark-background-color; $location-badge-active-bg: #4f91f8; $location-icon-color: #e7e9ed; -$location-icon-active-color: #807e7e; /* * Notes */ $notes-light-color: #8e8e8e; -$notes-action-color: #c3c3c3; $notes-role-color: #8e8e8e; $notes-role-border-color: #e4e4e4; $note-disabled-comment-color: #b2b2b2; $note-form-border-color: #e5e5e5; -$note-toolbar-color: #959494; $note-targe3-outside: #fffff0; $note-targe3-inside: #ffffd3; $note-line2-border: #ddd; @@ -344,7 +332,6 @@ $zen-control-color: #555; /* * Calendar */ -$calendar-header-color: #b8b8b8; $calendar-hover-bg: #ecf3fe; $calendar-border-color: rgba(#000, .1); $calendar-unselectable-bg: $gray-light; @@ -360,15 +347,9 @@ $cycle-analytics-dark-text: $gl-title-color; $cycle-analytics-light-gray: #bfbfbf; $cycle-analytics-dismiss-icon-color: #b2b2b2; -/* - * Personal Access Tokens - */ -$personal-access-tokens-disabled-label-color: #bbb; - /* * CI */ -$ci-text-color: #c5c8c6; $ci-skipped-color: #888; /* diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index a1e3038b8de..84b639fabf5 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -40,7 +40,7 @@ } .zen-control-full { - color: $note-toolbar-color; + color: $gl-gray-light; &:hover { color: $gl-link-color; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index bf00b23d787..e2d80723eff 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -97,7 +97,7 @@ .build-trace { background: $black; - color: $ci-text-color; + color: $gray-darkest; white-space: pre; overflow-x: auto; font-size: 12px; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index c35d71f9e7b..468f07d0534 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -204,7 +204,7 @@ .comment-toolbar { padding-top: $gl-padding-top; - color: $note-toolbar-color; + color: $gl-gray-light; border-top: 1px solid $border-color; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 10eb3d4203e..0d4db14dafb 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -372,7 +372,7 @@ ul.notes { .note-actions { float: right; margin-left: 10px; - color: $notes-action-color; + color: $gray-darkest; } .note-actions { @@ -411,7 +411,7 @@ ul.notes { } .fa { - color: $notes-action-color; + color: $gray-darkest; position: relative; font-size: 17px; } @@ -573,10 +573,10 @@ ul.notes { svg { position: relative; - color: $notes-action-color; + color: $gray-darkest; path { - fill: $notes-action-color; + fill: $gray-darkest; } } } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 656dcd92a06..acc9dae4745 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -654,7 +654,7 @@ .toggle-pipeline-btn { .fa { - color: $dropdown-header-color; + color: $gl-gray-light; } } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index f8677f93fe0..ffbda72b00e 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -198,7 +198,7 @@ } .personal-access-tokens-never-expires-label { - color: $personal-access-tokens-disabled-label-color; + color: $note-disabled-comment-color; } .datepicker.personal-access-tokens-expires-at .ui-state-disabled span { diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 63d0a34e610..b027ace6a1f 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -51,7 +51,7 @@ border-radius: $border-radius-default; font-size: 14px; font-style: normal; - color: $location-badge-color; + color: $note-disabled-comment-color; display: inline-block; background-color: $location-badge-bg; vertical-align: top; @@ -140,7 +140,7 @@ .search-input-wrap { i { - color: $location-icon-active-color; + color: $layout-link-gray; } } } diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss index 9f9d630978a..b085c56390d 100644 --- a/app/assets/stylesheets/pages/xterm.scss +++ b/app/assets/stylesheets/pages/xterm.scss @@ -18,7 +18,7 @@ $l-blue: #81a2be; $l-magenta: #b294bb; $l-cyan: #8abeb7; - $l-white: $ci-text-color; + $l-white: $gray-darkest; /* * xterm colors -- cgit v1.2.1 From 0b72554c1bb47c88361f9fbc6185f49767337368 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Mon, 12 Dec 2016 16:26:21 -0600 Subject: Finish refactoring grays --- app/assets/stylesheets/framework/awards.scss | 6 ++-- app/assets/stylesheets/framework/blocks.scss | 4 +-- app/assets/stylesheets/framework/buttons.scss | 14 ++++----- app/assets/stylesheets/framework/callout.scss | 2 +- app/assets/stylesheets/framework/dropdowns.scss | 4 +-- app/assets/stylesheets/framework/files.scss | 2 +- app/assets/stylesheets/framework/forms.scss | 2 +- app/assets/stylesheets/framework/header.scss | 12 ++++---- app/assets/stylesheets/framework/images.scss | 2 +- app/assets/stylesheets/framework/lists.scss | 10 +++---- .../stylesheets/framework/markdown_area.scss | 2 +- app/assets/stylesheets/framework/nav.scss | 16 +++++----- app/assets/stylesheets/framework/selects.scss | 4 +-- app/assets/stylesheets/framework/sidebar.scss | 2 +- app/assets/stylesheets/framework/tables.scss | 4 +-- app/assets/stylesheets/framework/timeline.scss | 2 +- .../framework/tw_bootstrap_variables.scss | 10 +++---- app/assets/stylesheets/framework/variables.scss | 35 ++++------------------ app/assets/stylesheets/framework/wells.scss | 4 +-- app/assets/stylesheets/highlight/white.scss | 6 ++-- .../mailers/highlighted_diff_email.scss | 6 ++-- app/assets/stylesheets/pages/boards.scss | 2 +- app/assets/stylesheets/pages/builds.scss | 2 +- app/assets/stylesheets/pages/commits.scss | 4 +-- app/assets/stylesheets/pages/diff.scss | 8 ++--- app/assets/stylesheets/pages/editor.scss | 4 +-- app/assets/stylesheets/pages/events.scss | 2 +- app/assets/stylesheets/pages/help.scss | 2 +- app/assets/stylesheets/pages/issuable.scss | 8 ++--- app/assets/stylesheets/pages/issues.scss | 8 ++--- app/assets/stylesheets/pages/merge_conflicts.scss | 2 +- app/assets/stylesheets/pages/merge_requests.scss | 4 +-- app/assets/stylesheets/pages/note_form.scss | 2 +- app/assets/stylesheets/pages/notes.scss | 12 ++++---- app/assets/stylesheets/pages/pipelines.scss | 4 +-- app/assets/stylesheets/pages/profile.scss | 8 ++--- app/assets/stylesheets/pages/projects.scss | 10 +++---- app/assets/stylesheets/pages/search.scss | 2 +- app/assets/stylesheets/pages/snippets.scss | 2 +- app/assets/stylesheets/pages/tree.scss | 10 +++---- 40 files changed, 110 insertions(+), 135 deletions(-) diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index dece5c3202b..9fc9bcebc44 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -12,8 +12,8 @@ z-index: 9; width: 300px; font-size: 14px; - background-color: $award-emoji-menu-bg; - border: 1px solid $award-emoji-menu-border; + background-color: $white-light; + border: 1px solid $border-white-light; border-radius: $border-radius-base; box-shadow: 0 6px 12px $award-emoji-menu-shadow; pointer-events: none; @@ -135,7 +135,7 @@ } .award-control-icon { - color: $award-emoji-new-btn-icon-color; + color: $border-gray-normal; margin-top: 1px; } } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 95c02499271..9f02749f5ab 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -24,7 +24,7 @@ .row-content-block { margin-top: 0; margin-bottom: -$gl-padding; - background-color: $background-color; + background-color: $gray-light; padding: $gl-padding; margin-bottom: 0; border-top: 1px solid $white-dark; @@ -118,7 +118,7 @@ .cover-block { text-align: center; - background: $background-color; + background: $gray-light; padding-top: 44px; position: relative; diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index e6f03528d41..59ff17ad2c1 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -88,11 +88,11 @@ } @mixin btn-gray { - @include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-gray-dark); + @include btn-color($gray-light, $border-gray-normal, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-gray-dark); } @mixin btn-white { - @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-dark, $gl-text-color); + @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-gray-dark, $gl-text-color); } @mixin btn-with-margin { @@ -289,7 +289,7 @@ .active { box-shadow: $gl-btn-active-background; - border: 1px solid $border-white-dark !important; + border: 1px solid $border-gray-dark !important; background-color: $btn-active-gray-light !important; } } @@ -310,7 +310,7 @@ padding: 6px 16px; border-color: $border-color; color: $gray-darkest; - background-color: $background-color; + background-color: $gray-light; &:hover, &:active, @@ -319,7 +319,7 @@ box-shadow: none; border-color: $border-color; color: $gray-darkest; - background-color: $background-color; + background-color: $gray-light; } } @@ -344,8 +344,8 @@ } .btn-static { - background-color: $background-color !important; - border: 1px solid $border-gray-light; + background-color: $gray-light !important; + border: 1px solid $border-gray-normal; cursor: default; &:active { diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss index 2a100980aca..e0e46dd73af 100644 --- a/app/assets/stylesheets/framework/callout.scss +++ b/app/assets/stylesheets/framework/callout.scss @@ -11,7 +11,7 @@ padding: $gl-padding; border-left: 3px solid $border-color; color: $text-color; - background: $background-color; + background: $gray-light; } .bs-callout h4 { diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index fd5f521e4a0..ecd540793d0 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -34,7 +34,7 @@ .dropdown-toggle { padding: 6px 8px 6px 10px; - background-color: $dropdown-toggle-bg; + background-color: $white-light; color: $gl-text-color; font-size: 14px; text-align: left; @@ -609,7 +609,7 @@ } .ui-datepicker-unselectable { - background-color: $calendar-unselectable-bg; + background-color: $gray-light; } } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index ab0b81f77f7..88ed0a4a17e 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -19,7 +19,7 @@ .file-title { position: relative; - background-color: $background-color; + background-color: $gray-light; border-bottom: 1px solid $border-color; margin: 0; text-align: left; diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 89a51959571..940807fc399 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -22,7 +22,7 @@ input[type='text'].danger { margin-top: 0; margin-bottom: -$gl-padding; padding: $gl-padding; - background-color: $background-color; + background-color: $gray-light; border-top: 1px solid $border-color; } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 74fd97f811e..971940773f7 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -9,7 +9,7 @@ header { &.navbar-empty { height: $header-height; background: $white-light; - border-bottom: 1px solid $btn-gray-hover; + border-bottom: 1px solid $white-normal; .center-logo { margin: 8px 0; @@ -27,7 +27,7 @@ header { z-index: 100; margin-bottom: 0; height: $header-height; - background-color: $background-color; + background-color: $gray-light; border: none; border-bottom: 1px solid $border-color; @@ -62,7 +62,7 @@ header { &:hover, &:focus, &:active { - background-color: $background-color; + background-color: $gray-light; color: darken($gl-gray-light, 30%); .todos-pending-count { @@ -84,7 +84,7 @@ header { padding: 6px 10px; &:hover { - background-color: $btn-gray-hover; + background-color: $white-normal; } &.active { @@ -100,10 +100,10 @@ header { font-size: 18px; padding: 6px 10px; border: none; - background-color: $background-color; + background-color: $gray-light; &:hover { - background-color: $btn-gray-hover; + background-color: $white-normal; } } } diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss index 878f44116ba..09a569ad415 100644 --- a/app/assets/stylesheets/framework/images.scss +++ b/app/assets/stylesheets/framework/images.scss @@ -4,7 +4,7 @@ } .appearance-light-logo-preview { - background-color: $background-color; + background-color: $gray-light; max-width: 72px; padding: 10px; margin-bottom: 10px; diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 389ead37366..e96cd671e34 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -36,7 +36,7 @@ color: $list-warning-row-color; } - &.smoke { background-color: $background-color; } + &.smoke { background-color: $gray-light; } &:not(.ui-sort-disabled):hover { background: $row-hover; @@ -46,7 +46,7 @@ border-bottom: none; &.bottom { - background: $background-color; + background: $gray-light; } } @@ -59,7 +59,7 @@ p { padding-top: 1px; margin: 0; - color: $gray-dark; + color: $white-normal; img { position: relative; @@ -113,7 +113,7 @@ ul.content-list { padding: 0; li { - border-color: $table-border-color; + border-color: $white-normal; font-size: $list-font-size; color: $list-text-color; @@ -186,7 +186,7 @@ ul.content-list { &.list-placeholder { background-color: $gray-light; - border: dotted 1px $gray-dark; + border: dotted 1px $white-normal; margin: 1px 0; min-height: 52px; } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index cf298773d58..e30d81d09f0 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -114,7 +114,7 @@ // Border around images in issue and MR comments. img:not(.emoji) { - border: 1px solid $table-border-gray; + border: 1px solid $white-normal; padding: 5px; margin: 5px 0; // Ensure that image does not exceed viewport diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 26d290976dc..e4affbb1be1 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -87,10 +87,10 @@ &.sub-nav { text-align: center; - background-color: $dark-background-color; + background-color: $gray-normal; .container-fluid { - background-color: $dark-background-color; + background-color: $gray-normal; margin-bottom: 0; } @@ -117,7 +117,7 @@ .top-area { @include clearfix; - border-bottom: 1px solid $btn-gray-hover; + border-bottom: 1px solid $white-normal; .nav-text { padding-top: 16px; @@ -289,7 +289,7 @@ top: $header-height; width: 100%; z-index: 11; - background: $background-color; + background: $gray-light; border-bottom: 1px solid $border-color; transition: padding $sidebar-transition-duration; text-align: center; @@ -352,7 +352,7 @@ } .fade-right { - @include fade(left, $background-color); + @include fade(left, $gray-light); right: -5px; .fa { @@ -361,7 +361,7 @@ } .fade-left { - @include fade(right, $background-color); + @include fade(right, $gray-light); left: -5px; .fa { @@ -372,7 +372,7 @@ &.sub-nav-scroll { .fade-right { - @include fade(left, $dark-background-color); + @include fade(left, $gray-normal); right: 0; .fa { @@ -381,7 +381,7 @@ } .fade-left { - @include fade(right, $dark-background-color); + @include fade(right, $gray-normal); left: 0; .fa { diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index fde1431b13e..9ab17e67d4c 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -39,7 +39,7 @@ } &:hover { - background-color: $gray-dark; + background-color: $white-normal; border-color: $border-white-normal; color: $gl-text-color; } @@ -108,7 +108,7 @@ border-color: $input-border; color: $gl-text-color; line-height: 15px; - background-color: $background-color; + background-color: $gray-light; background-image: none; .select2-search-choice-close { diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 0aa609b8dd5..46a06cd7eab 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -23,7 +23,7 @@ .sidebar-wrapper { z-index: 1000; - background: $background-color; + background: $gray-light; .nicescroll-rails-hr { // TODO: Figure out why nicescroll doesn't hide horizontal bar diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index 5d0ca63ea08..6d9fa74a030 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -31,7 +31,7 @@ table { } th { - background-color: $background-color; + background-color: $gray-light; font-weight: normal; border-bottom: none; @@ -41,7 +41,7 @@ table { } td { - border-color: $table-border-color; + border-color: $white-normal; } } } diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index 875cded8b4e..6078505807e 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -6,7 +6,7 @@ .timeline-entry { padding: $gl-padding $gl-btn-padding 11px; - border-color: $table-border-color; + border-color: $white-normal; color: $gl-gray; border-bottom: 1px solid $border-white-light; diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index c731a8f222f..876adf7f712 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -78,7 +78,7 @@ $pagination-active-bg: $white-light; $pagination-active-border: $border-color; $pagination-disabled-color: #cdcdcd; -$pagination-disabled-bg: $background-color; +$pagination-disabled-bg: $gray-light; $pagination-disabled-border: $border-color; @@ -117,8 +117,8 @@ $alert-border-radius: 0; $panel-border-radius: 2px; $panel-default-text: $text-color; $panel-default-border: $border-color; -$panel-default-heading-bg: $background-color; -$panel-footer-bg: $background-color; +$panel-default-heading-bg: $gray-light; +$panel-footer-bg: $gray-light; $panel-inner-border: $border-color; //== Wells @@ -153,8 +153,8 @@ $nav-link-padding: 13px $gl-padding; //== Code // //## -$pre-bg: $background-color !default; +$pre-bg: $gray-light !default; $pre-color: $gl-gray !default; $pre-border-color: $border-color; -$table-bg-accent: $background-color; +$table-bg-accent: $gray-light; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 00ac5e82181..936aaf38254 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -17,14 +17,13 @@ $darken-dark-factor: 10%; $darken-border-factor: 5%; $white-light: #fff; -$white-normal: darken($white-light, $darken-normal-factor); -$white-dark: darken($white-light, $darken-dark-factor); +$white-normal: #f0f0f0; +$white-dark: #eaeaea; $gray-lightest: #fdfdfd; $gray-light: #fafafa; $gray-lighter: #f9f9f9; -$gray-normal: darken($gray-light, $darken-normal-factor); -$gray-dark: darken($gray-light, $darken-dark-factor); +$gray-normal: #f5f5f5; $gray-darker: #eee; $gray-darkest: #c4c4c4; @@ -55,11 +54,9 @@ $black-transparent: rgba(0, 0, 0, 0.3); $border-white-light: darken($white-light, $darken-border-factor); $border-white-normal: darken($white-normal, $darken-border-factor); -$border-white-dark: darken($white-dark, $darken-border-factor); -$border-gray-light: darken($gray-light, $darken-border-factor); $border-gray-normal: darken($gray-normal, $darken-border-factor); -$border-gray-dark: darken($gray-dark, $darken-border-factor); +$border-gray-dark: darken($white-normal, $darken-border-factor); $border-green-extra-light: #9adb84; $border-green-light: darken($green-light, $darken-border-factor); @@ -78,9 +75,6 @@ $border-red-light: darken($red-light, $darken-border-factor); $border-red-normal: darken($red-normal, $darken-border-factor); $border-red-dark: darken($red-dark, $darken-border-factor); -$help-well-bg: $gray-light; -$help-well-border: #e5e5e5; - $warning-message-bg: #fbf2d9; $warning-message-color: #9e8e60; $warning-message-border: #f0e2bb; @@ -90,9 +84,6 @@ $warning-message-border: #f0e2bb; */ $border-color: #e5e5e5; $focus-border-color: #3aabf0; -$table-border-color: #f0f0f0; -$background-color: $gray-light; -$dark-background-color: #f5f5f5; $well-expand-item: #e8f2f7; $well-inner-border: #eef0f2; $well-light-border: #f1f1f1; @@ -166,7 +157,6 @@ $fixed-layout-width: 1280px; $error-exclamation-point: #e62958; $border-radius-default: 2px; $settings-icon-size: 18px; -$provider-btn-group-border: #e5e5e5; $provider-btn-not-active-color: #4688f1; $link-underline-blue: #4a8bee; $active-item-blue: #4a8bee; @@ -241,8 +231,6 @@ $line-removed-dark: #fac5cd; $line-number-old: #f9d7dc; $line-number-new: #ddfbe6; $line-number-select: #fbf2da; -$match-line: $gray-light; -$table-border-gray: #f0f0f0; $line-target-blue: #f6faff; $line-select-yellow: #fcf8e7; $line-select-yellow-dark: #f0e2bd; @@ -252,7 +240,6 @@ $file-mode-changed: #777; $file-mode-changed: #777; $diff-image-bg: #ddd; $diff-image-info-color: grey; -$diff-image-img-bg: #e5e5e5; $diff-swipe-border: #999; $diff-view-modes-color: grey; $diff-view-modes-border: #c1c1c1; @@ -280,10 +267,7 @@ $dropdown-input-focus-border: $focus-border-color; $dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, .4); $dropdown-loading-bg: rgba(#fff, .6); $dropdown-chevron-size: 10px; - -$dropdown-toggle-bg: #fff; -$dropdown-toggle-border-color: #e5e5e5; -$dropdown-toggle-active-border-color: darken($dropdown-toggle-border-color, 14%); +$dropdown-toggle-active-border-color: darken($border-color, 14%); /* @@ -291,15 +275,11 @@ $dropdown-toggle-active-border-color: darken($dropdown-toggle-border-color, 14%) */ $btn-active-gray: #ececec; $btn-active-gray-light: e4e7ed; -$btn-gray-hover: #eee; /* * Award emoji */ -$award-emoji-menu-bg: #fff; -$award-emoji-menu-border: #f1f2f4; $award-emoji-menu-shadow: rgba(0,0,0,.175); -$award-emoji-new-btn-icon-color: #dcdcdc; /* * Search Box @@ -307,7 +287,6 @@ $award-emoji-new-btn-icon-color: #dcdcdc; $search-input-border-color: rgba(#4688f1, .8); $search-input-focus-shadow-color: $dropdown-input-focus-shadow; $search-input-width: 220px; -$location-badge-bg: $dark-background-color; $location-badge-active-bg: #4f91f8; $location-icon-color: #e7e9ed; @@ -316,9 +295,7 @@ $location-icon-color: #e7e9ed; */ $notes-light-color: #8e8e8e; $notes-role-color: #8e8e8e; -$notes-role-border-color: #e4e4e4; $note-disabled-comment-color: #b2b2b2; -$note-form-border-color: #e5e5e5; $note-targe3-outside: #fffff0; $note-targe3-inside: #ffffd3; $note-line2-border: #ddd; @@ -334,7 +311,6 @@ $zen-control-color: #555; */ $calendar-hover-bg: #ecf3fe; $calendar-border-color: rgba(#000, .1); -$calendar-unselectable-bg: $gray-light; $calendar-user-contrib-text: #959494; /* @@ -445,7 +421,6 @@ $help-shortcut-header-color: #333; /* * Issues */ -$issues-border: #e5e5e5; $issues-today-bg: #f3fff2; $issues-today-border: #e1e8d5; diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss index f2860dfe84d..f9c850ebdc8 100644 --- a/app/assets/stylesheets/framework/wells.scss +++ b/app/assets/stylesheets/framework/wells.scss @@ -1,5 +1,5 @@ .info-well { - background: $background-color; + background: $gray-light; color: $gl-gray; border: 1px solid $border-color; border-radius: $border-radius-default; @@ -45,7 +45,7 @@ } .light-well { - background-color: $background-color; + background-color: $gray-light; padding: 15px; } diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 1adab3ffd94..54a5664a874 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -69,14 +69,14 @@ $white-gc-bg: #eaf2f5; @mixin matchLine { color: $black-transparent; - background-color: $match-line; + background-color: $gray-light; } .code.white { // Line numbers .line-numbers, .diff-line-num { - background-color: $background-color; + background-color: $gray-light; } .diff-line-num, @@ -87,7 +87,7 @@ $white-gc-bg: #eaf2f5; // Code itself pre.code, .diff-line-num { - border-color: $table-border-gray; + border-color: $white-normal; } &, diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss index 024b4df6bd0..60ff72c703e 100644 --- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss +++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss @@ -91,9 +91,9 @@ $highlighted-gc-bg: #eaf2f5; padding: 0 5px; text-align: right; width: 35px; - background-color: $background-color; + background-color: $gray-light; color: $black-transparent; - border-right: 1px solid $table-border-gray; + border-right: 1px solid $white-normal; &.old { background-color: $line-number-old; @@ -130,7 +130,7 @@ $highlighted-gc-bg: #eaf2f5; &.match { color: $black-transparent; - background-color: $match-line; + background-color: $gray-light; } } diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 59ba0939785..c735f104c20 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -98,7 +98,7 @@ .board-inner { height: 100%; font-size: $issue-boards-font-size; - background: $background-color; + background: $gray-light; border: 1px solid $border-color; border-radius: $border-radius-default; } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index e2d80723eff..66f7e7f97c8 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -42,7 +42,7 @@ } .environment-information { - background-color: $background-color; + background-color: $gray-light; border: 1px solid $border-color; padding: 12px $gl-padding; border-radius: $border-radius-default; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index c4ee9a51ed5..e76e1a73b25 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -7,7 +7,7 @@ .commit-header { padding: 5px 10px; - background-color: $background-color; + background-color: $gray-light; border-top: 1px solid $gray-darker; border-bottom: 1px solid $gray-darker; font-size: 14px; @@ -117,7 +117,7 @@ .commit-row-description { font-size: 14px; - border-left: 1px solid $btn-gray-hover; + border-left: 1px solid $white-normal; padding: 10px 15px; margin: 10px 0; background: $gray-light; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 737f6e0f4be..f30795fd2c2 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -11,7 +11,7 @@ .diff-header { position: relative; - background: $background-color; + background: $gray-light; border-bottom: 1px solid $border-color; padding: 10px 16px; color: $gl-diff-text-color; @@ -38,7 +38,7 @@ cursor: pointer; &:hover { - background-color: $dark-background-color; + background-color: $gray-normal; } .diff-toggle-caret { @@ -187,8 +187,8 @@ img { border: 1px solid $white-light; - background-image: linear-gradient(45deg, $diff-image-img-bg 25%, transparent 25%, transparent 75%, $diff-image-img-bg 75%, $diff-image-img-bg 100%), - linear-gradient(45deg, $diff-image-img-bg 25%, transparent 25%, transparent 75%, $diff-image-img-bg 75%, $diff-image-img-bg 100%); + background-image: linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%), + linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%); background-size: 10px 10px; background-position: 0 0, 5px 5px; max-width: 100%; diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 6cde9c592de..4b2e96dff8e 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -10,7 +10,7 @@ } .ace_gutter-cell { - background-color: $background-color; + background-color: $gray-light; } .cancel-btn { @@ -34,7 +34,7 @@ } .editor-ref { - background: $background-color; + background: $gray-light; padding-right: $gl-padding; border-right: 1px solid $border-color; display: block; diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index dc67d411c71..98925c2d0cb 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -5,7 +5,7 @@ .event-item { font-size: $gl-font-size; padding: $gl-padding-top 0 $gl-padding-top ($gl-avatar-size + $gl-padding-top); - border-bottom: 1px solid $table-border-color; + border-bottom: 1px solid $white-normal; color: $list-text-color; &.event-inline { diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index e2e644dc23b..dae8ccdef6c 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -60,7 +60,7 @@ // Border around images in the help pages. img:not(.emoji) { - border: 1px solid $table-border-gray; + border: 1px solid $white-normal; padding: 5px; margin: 5px; max-height: calc(100vh - 100px); diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 6ef7f9c2c1e..0234f2d49e7 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -7,7 +7,7 @@ // Border around images in issue and MR descriptions. .description img:not(.emoji) { - border: 1px solid $table-border-gray; + border: 1px solid $white-normal; padding: 5px; margin: 5px; max-height: calc(100vh - 100px); @@ -51,7 +51,7 @@ .block { @include clearfix; padding: $gl-padding 0; - border-bottom: 1px solid $border-gray-light; + border-bottom: 1px solid $border-gray-normal; // This prevents the mess when resizing the sidebar // of elements repositioning themselves.. width: $gutter_inner_width; @@ -178,7 +178,7 @@ .gutter-toggle { margin-top: 7px; - border-left: 1px solid $border-gray-light; + border-left: 1px solid $border-gray-normal; } .assignee .avatar { @@ -216,7 +216,7 @@ } .participants { - border-bottom: 1px solid $border-gray-light; + border-bottom: 1px solid $border-gray-normal; } .hide-collapsed { diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 3b47f99df2c..8734a3b1598 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -88,12 +88,12 @@ ul.related-merge-requests > li { &.closed { background: $gray-light; - border-color: $issues-border; + border-color: $border-color; } &.merged { background: $gray-light; - border-color: $issues-border; + border-color: $border-color; } } @@ -144,7 +144,7 @@ ul.related-merge-requests > li { } .btn { - background-color: $background-color; - border: 1px solid $border-gray-light; + background-color: $gray-light; + border: 1px solid $border-gray-normal; } } diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index 7a90713dd3f..5a9f199fb34 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -274,7 +274,7 @@ $colors: ( } .discard-changes-alert { - background-color: $background-color; + background-color: $gray-light; text-align: right; padding: $gl-padding-top $gl-padding; color: $gl-text-color; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 156724f44a8..e779e65eca3 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -3,7 +3,7 @@ * */ .mr-state-widget { - background: $background-color; + background: $gray-light; color: $gl-gray; border: 1px solid $border-color; border-radius: 2px; @@ -375,7 +375,7 @@ } .mr-version-controls { - background: $background-color; + background: $gray-light; border-bottom: 1px solid $border-color; color: $gl-text-color; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 468f07d0534..074abec7692 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -62,7 +62,7 @@ .common-note-form { .md-area { padding: $gl-padding-top $gl-padding; - border: 1px solid $note-form-border-color; + border: 1px solid $border-color; border-radius: $border-radius-base; transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 0d4db14dafb..1b83b40486e 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -166,7 +166,7 @@ ul.notes { .note { display: block; position: relative; - border-bottom: 1px solid $table-border-gray; + border-bottom: 1px solid $white-normal; &.note-discussion { &.timeline-entry { @@ -291,14 +291,14 @@ ul.notes { font-family: $regular_font; td { - border: 1px solid $table-border-gray; + border: 1px solid $white-normal; border-left: none; &.notes_line { vertical-align: middle; text-align: center; padding: 10px 0; - background: $background-color; + background: $gray-light; color: $text-color; } @@ -309,7 +309,7 @@ ul.notes { } &.notes_content { - background-color: $background-color; + background-color: $gray-light; border-width: 1px 0; padding: 0; vertical-align: top; @@ -448,7 +448,7 @@ ul.notes { color: $notes-role-color; font-size: 12px; line-height: 20px; - border: 1px solid $notes-role-border-color; + border: 1px solid $border-color; border-radius: $border-radius-base; } @@ -529,7 +529,7 @@ ul.notes { .line-resolve-all { display: inline-block; padding: 5px 10px; - background-color: $background-color; + background-color: $gray-light; border: 1px solid $border-color; border-radius: $border-radius-default; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index acc9dae4745..b9099cb6796 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -289,7 +289,7 @@ // Pipeline visualization .toggle-pipeline-btn { - background-color: $gray-dark; + background-color: $white-normal; &.graph-collapsed { background-color: $white-light; @@ -298,7 +298,7 @@ .pipeline-graph { width: 100%; - background-color: $background-color; + background-color: $gray-light; padding: $gl-padding; overflow: auto; white-space: nowrap; diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index ffbda72b00e..8a5b0e20a86 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -60,8 +60,8 @@ .account-well { padding: 10px; - background-color: $help-well-bg; - border: 1px solid $help-well-border; + background-color: $gray-light; + border: 1px solid $border-color; border-radius: $border-radius-base; ul { @@ -136,7 +136,7 @@ .provider-btn-group { display: inline-block; margin-right: 10px; - border: 1px solid $provider-btn-group-border; + border: 1px solid $border-color; border-radius: 3px; &:last-child { @@ -147,7 +147,7 @@ .provider-btn-image { display: inline-block; padding: 5px 10px; - border-right: 1px solid $provider-btn-group-border; + border-right: 1px solid $border-color; > img { width: 20px; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 6e0f6b1cd81..9c3dbc58ae0 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -420,13 +420,13 @@ a.deploy-project-label { width: 100px; height: 100px; background-color: $gray-light; - border: 1px solid $gray-dark; + border: 1px solid $white-normal; margin: 0 auto; border-radius: 50%; i { font-size: 100px; - color: $gray-dark; + color: $white-normal; } } @@ -536,7 +536,7 @@ a.deploy-project-label { } li.missing { - border: 1px dashed $border-gray-light; + border: 1px dashed $border-gray-normal; border-radius: $border-radius-default; a { @@ -591,7 +591,7 @@ pre.light-well { @include basic-list; .project-row { - border-color: $table-border-color; + border-color: $white-normal; .project-full-name { @include str-truncated; @@ -643,7 +643,7 @@ pre.light-well { &.container-fluid { padding-top: 12px; padding-bottom: 12px; - background-color: $background-color; + background-color: $gray-light; border: 1px solid $border-color; border-right-width: 0; border-left-width: 0; diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index b027ace6a1f..cedd4cb2987 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -53,7 +53,7 @@ font-style: normal; color: $note-disabled-comment-color; display: inline-block; - background-color: $location-badge-bg; + background-color: $gray-normal; vertical-align: top; cursor: default; } diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss index ff13b86acf0..a6037d76797 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/pages/snippets.scss @@ -30,7 +30,7 @@ } .project-snippets .awards { - border-bottom: 1px solid $table-border-color; + border-bottom: 1px solid $white-normal; padding-bottom: $gl-padding; } diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 20ad63be045..c0341db7289 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -20,8 +20,8 @@ margin-bottom: 0; tr { - border-bottom: 1px solid $table-border-gray; - border-top: 1px solid $table-border-gray; + border-bottom: 1px solid $white-normal; + border-top: 1px solid $white-normal; td, th { @@ -39,7 +39,7 @@ .commit-history-link-spacer { margin: 0 10px; - color: $table-border-color; + color: $white-normal; } &:hover { @@ -53,7 +53,7 @@ &.selected { td { - background: $gray-dark; + background: $white-normal; border-top: 1px solid $border-gray-dark; border-bottom: 1px solid $border-gray-dark; } @@ -134,7 +134,7 @@ .blob-commit-info { list-style: none; padding: $gl-padding; - background: $background-color; + background: $gray-light; border: 1px solid $border-color; border-bottom: none; margin: 0; -- cgit v1.2.1 From e39f024029b46322c1bf24409fd5ce7bfcef2da5 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Tue, 13 Dec 2016 21:28:04 +0200 Subject: BB importer: Adding created_by only when used is not found[ci skip] --- lib/bitbucket/representation/base.rb | 4 ++++ lib/bitbucket/representation/comment.rb | 2 +- lib/bitbucket/representation/issue.rb | 2 +- lib/bitbucket/representation/pull_request.rb | 2 +- lib/bitbucket/representation/user.rb | 6 +++++- lib/gitlab/bitbucket_import/importer.rb | 23 +++++++++++++++++------ 6 files changed, 29 insertions(+), 10 deletions(-) diff --git a/lib/bitbucket/representation/base.rb b/lib/bitbucket/representation/base.rb index 94adaacc9b5..fd622d333da 100644 --- a/lib/bitbucket/representation/base.rb +++ b/lib/bitbucket/representation/base.rb @@ -5,6 +5,10 @@ module Bitbucket @raw = raw end + def user_representation(raw) + User.new(raw) + end + def self.decorate(entries) entries.map { |entry| new(entry)} end diff --git a/lib/bitbucket/representation/comment.rb b/lib/bitbucket/representation/comment.rb index 94bc18cbfab..bc40f891cd3 100644 --- a/lib/bitbucket/representation/comment.rb +++ b/lib/bitbucket/representation/comment.rb @@ -2,7 +2,7 @@ module Bitbucket module Representation class Comment < Representation::Base def author - user.fetch('username', 'Anonymous') + user_representation(user) end def note diff --git a/lib/bitbucket/representation/issue.rb b/lib/bitbucket/representation/issue.rb index 6c8e9a4c244..90adfa3331a 100644 --- a/lib/bitbucket/representation/issue.rb +++ b/lib/bitbucket/representation/issue.rb @@ -12,7 +12,7 @@ module Bitbucket end def author - raw.dig('reporter', 'username') || 'Anonymous' + user_representation(raw.fetch('reporter', {})) end def description diff --git a/lib/bitbucket/representation/pull_request.rb b/lib/bitbucket/representation/pull_request.rb index e7b1f99e9a6..96992003d24 100644 --- a/lib/bitbucket/representation/pull_request.rb +++ b/lib/bitbucket/representation/pull_request.rb @@ -2,7 +2,7 @@ module Bitbucket module Representation class PullRequest < Representation::Base def author - raw.fetch('author', {}).fetch('username', 'Anonymous') + user_representation(raw.fetch('author', {})) end def description diff --git a/lib/bitbucket/representation/user.rb b/lib/bitbucket/representation/user.rb index ba6b7667b49..6025a9f0653 100644 --- a/lib/bitbucket/representation/user.rb +++ b/lib/bitbucket/representation/user.rb @@ -2,7 +2,11 @@ module Bitbucket module Representation class User < Representation::Base def username - raw['username'] + raw['username'] || 'Anonymous' + end + + def uuid + raw['uuid'] end end end diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index a0a17333185..519a109c0c8 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -24,15 +24,23 @@ module Gitlab private - def gitlab_user_id(project, bitbucket_id) - if bitbucket_id - user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s) + def gitlab_user_id(project, user) + if user.uuid + user = find_user_by_uuid(user.uuid) (user && user.id) || project.creator_id else project.creator_id end end + def find_user_by_uuid(uuid) + User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", uuid) + end + + def existing_gitlab_user?(user) + user.uuid && find_user_by_uuid(user.uuid) + end + def repo @repo ||= client.repo(project.import_source) end @@ -43,7 +51,8 @@ module Gitlab create_labels client.issues(repo).each do |issue| - description = @formatter.author_line(issue.author) + description = '' + description += @formatter.author_line(issue.author.username) unless existing_gitlab_user?(issue.author) description += issue.description label_name = issue.kind @@ -69,7 +78,8 @@ module Gitlab # we do this check. next unless comment.note.present? - note = @formatter.author_line(comment.author) + note = '' + note += @formatter.author_line(comment.author.username) unless existing_gitlab_user?(comment.author) note += comment.note issue.notes.create!( @@ -97,7 +107,8 @@ module Gitlab pull_requests.each do |pull_request| begin - description = @formatter.author_line(pull_request.author) + description = '' + description += @formatter.author_line(pull_request.author.username) unless existing_gitlab_user?(pull_request.author) description += pull_request.description merge_request = project.merge_requests.create( -- cgit v1.2.1 From 1fdd5d682368b6807b89e8a399d7751c519737bd Mon Sep 17 00:00:00 2001 From: Nick Thomas <nick@gitlab.com> Date: Tue, 13 Dec 2016 19:21:30 +0000 Subject: Introduce $CI_BUILD_REF_SLUG --- app/models/ci/build.rb | 12 +++++++ changelogs/unreleased/22849-ci-build-ref-slug.yml | 4 +++ doc/ci/environments.md | 38 +++++++++++------------ doc/ci/variables/README.md | 1 + doc/ci/yaml/README.md | 15 +++++---- spec/models/build_spec.rb | 19 ++++++++++++ 6 files changed, 62 insertions(+), 27 deletions(-) create mode 100644 changelogs/unreleased/22849-ci-build-ref-slug.yml diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 88c46076df6..8a7c9e70dcf 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -184,6 +184,17 @@ module Ci project.build_timeout end + # A slugified version of the build ref, suitable for inclusion in URLs and + # domain names. Rules: + # + # * Lowercased + # * Anything not matching [a-z0-9-] is replaced with a - + # * Maximum length is 63 bytes + def ref_slug + slugified = ref.to_s.downcase + slugified.gsub(/[^a-z0-9]/, '-')[0..62] + end + def variables variables = predefined_variables variables += project.predefined_variables @@ -518,6 +529,7 @@ module Ci { key: 'CI_BUILD_REF', value: sha, public: true }, { key: 'CI_BUILD_BEFORE_SHA', value: before_sha, public: true }, { key: 'CI_BUILD_REF_NAME', value: ref, public: true }, + { key: 'CI_BUILD_REF_SLUG', value: ref_slug, public: true }, { key: 'CI_BUILD_NAME', value: name, public: true }, { key: 'CI_BUILD_STAGE', value: stage, public: true }, { key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, diff --git a/changelogs/unreleased/22849-ci-build-ref-slug.yml b/changelogs/unreleased/22849-ci-build-ref-slug.yml new file mode 100644 index 00000000000..b159ecca6d8 --- /dev/null +++ b/changelogs/unreleased/22849-ci-build-ref-slug.yml @@ -0,0 +1,4 @@ +--- +title: Introduce $CI_BUILD_REF_SLUG +merge_request: 8072 +author: diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 9dd84a5ff81..705bca6cc1f 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -248,7 +248,7 @@ deploy_review: - echo "Deploy a review app" environment: name: review/$CI_BUILD_REF_NAME - url: https://$CI_BUILD_REF_NAME.example.com + url: https://$CI_BUILD_REF_SLUG.example.com only: - branches except: @@ -259,13 +259,14 @@ Let's break it down in pieces. The job's name is `deploy_review` and it runs on the `deploy` stage. The `script` at this point is fictional, you'd have to use your own based on your deployment. Then, we set the `environment` with the `environment:name` being `review/$CI_BUILD_REF_NAME`. Now that's an interesting -one. Since the [environment name][env-name] can contain also slashes (`/`), we -can use this pattern to distinguish between dynamic environments and the regular +one. Since the [environment name][env-name] can contain slashes (`/`), we can +use this pattern to distinguish between dynamic environments and the regular ones. So, the first part is `review`, followed by a `/` and then `$CI_BUILD_REF_NAME` -which takes the value of the branch name. We also use the same -`$CI_BUILD_REF_NAME` value in the `environment:url` so that the environment +which takes the value of the branch name. Since `$CI_BUILD_REF_NAME` itself may +also contain `/`, or other characters that would be invalid in a domain name or +URL, we use `$CI_BUILD_REF_SLUG` in the `environment:url` so that the environment can get a specific and distinct URL for each branch. Again, the way you set up the webserver to serve these requests is based on your setup. @@ -299,7 +300,7 @@ deploy_review: - echo "Deploy a review app" environment: name: review/$CI_BUILD_REF_NAME - url: https://$CI_BUILD_REF_NAME.example.com + url: https://$CI_BUILD_REF_SLUG.example.com only: - branches except: @@ -329,16 +330,16 @@ deploy_prod: A more realistic example would include copying files to a location where a webserver (NGINX) could then read and serve. The example below will copy the -`public` directory to `/srv/nginx/$CI_BUILD_REF_NAME/public`: +`public` directory to `/srv/nginx/$CI_BUILD_REF_SLUG/public`: ```yaml review_app: stage: deploy script: - - rsync -av --delete public /srv/nginx/$CI_BUILD_REF_NAME + - rsync -av --delete public /srv/nginx/$CI_BUILD_REF_SLUG environment: name: review/$CI_BUILD_REF_NAME - url: https://$CI_BUILD_REF_NAME.example.com + url: https://$CI_BUILD_REF_SLUG.example.com ``` It is assumed that the user has already setup NGINX and GitLab Runner in the @@ -346,7 +347,7 @@ server this job will run on. >**Note:** Be sure to check out the [limitations](#limitations) section for some edge -cases regarding naming of you branches and Review Apps. +cases regarding naming of your branches and Review Apps. --- @@ -418,7 +419,7 @@ deploy_review: - echo "Deploy a review app" environment: name: review/$CI_BUILD_REF_NAME - url: https://$CI_BUILD_REF_NAME.example.com + url: https://$CI_BUILD_REF_SLUG.example.com on_stop: stop_review only: - branches @@ -480,9 +481,8 @@ exist, you should see something like: ## Checkout deployments locally -Since 8.13, a reference in the git repository is saved for each deployment. So -knowing what the state is of your current environments is only a `git fetch` -away. +Since 8.13, a reference in the git repository is saved for each deployment, so +knowing the state of your current environments is only a `git fetch` away. In your git config, append the `[remote "<your-remote>"]` block with an extra fetch line: @@ -493,10 +493,10 @@ fetch = +refs/environments/*:refs/remotes/origin/environments/* ## Limitations -1. If the branch name contains special characters (`/`), and you use the - `$CI_BUILD_REF_NAME` variable to dynamically create environments, there might - be complications during your Review Apps deployment. Follow the - [issue 22849][ce-22849] for more information. +1. `$CI_BUILD_REF_SLUG` is not *guaranteed* to be unique, so there is a small + chance of collisions between similarly-named branches (`fix-foo` would + conflict with `fix/foo`, for instance). Following a well-defined workflow + such as [GitLab Flow][gitlab-flow] can keep this from being a problem. 1. You are limited to use only the [CI predefined variables][variables] in the `environment: name`. If you try to re-use variables defined inside `script` as part of the environment name, it will not work. @@ -520,6 +520,6 @@ Below are some links you may find interesting: [only]: yaml/README.md#only-and-except [onstop]: yaml/README.md#environment-on_stop [ce-7015]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7015 -[ce-22849]: https://gitlab.com/gitlab-org/gitlab-ce/issues/22849 +[gitlab-flow]: ../workflow/gitlab_flow.md [gitlab runner]: https://docs.gitlab.com/runner/ [git-strategy]: yaml/README.md#git-strategy diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index d142fe266a2..e0ff9756868 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -40,6 +40,7 @@ version of Runner required. | **CI_BUILD_NAME** | all | 0.5 | The name of the build as defined in `.gitlab-ci.yml` | | **CI_BUILD_STAGE** | all | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` | | **CI_BUILD_REF_NAME** | all | all | The branch or tag name for which project is built | +| **CI_BUILD_REF_SLUG** | 8.15 | all | `$CI_BUILD_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. | | **CI_BUILD_REPO** | all | all | The URL to clone the Git repository | | **CI_BUILD_TRIGGERED** | all | 0.5 | The flag to indicate that build was [triggered] | | **CI_BUILD_MANUAL** | 8.12 | all | The flag to indicate that build was manually started | diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 5f88974d360..5e8d888e555 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -690,18 +690,12 @@ The `stop_review_app` job is **required** to have the following keywords defined #### dynamic environments > [Introduced][ce-6323] in GitLab 8.12 and GitLab Runner 1.6. + `$CI_BUILD_REF_SLUG` was [introduced][ce-8072] in GitLab 8.15. `environment` can also represent a configuration hash with `name` and `url`. These parameters can use any of the defined [CI variables](#variables) (including predefined, secure variables and `.gitlab-ci.yml` variables). ->**Note:** -Be aware than if the branch name contains special characters and you use the -`$CI_BUILD_REF_NAME` variable to dynamically create environments, there might -be complications during deployment. Follow the -[issue 22849](https://gitlab.com/gitlab-org/gitlab-ce/issues/22849) for more -information. - For example: ``` @@ -710,7 +704,7 @@ deploy as review app: script: make deploy environment: name: review-apps/$CI_BUILD_REF_NAME - url: https://$CI_BUILD_REF_NAME.review.example.com/ + url: https://$CI_BUILD_REF_SLUG.review.example.com/ ``` The `deploy as review app` job will be marked as deployment to dynamically @@ -726,6 +720,10 @@ The common use case is to create dynamic environments for branches and use them as Review Apps. You can see a simple example using Review Apps at https://gitlab.com/gitlab-examples/review-apps-nginx/. +`$CI_BUILD_REF_SLUG` is another environment variable set by the runner, based on +`$CI_BUILD_REF_NAME` but lower-cased, and with some characters replaced with +`-`, making it suitable for use in URLs and domain names. + ### artifacts >**Notes:** @@ -1245,4 +1243,5 @@ CI with various languages. [ce-6323]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6323 [environment]: ../environments.md [ce-6669]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6669 +[ce-8072]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/xxxx [variables]: ../variables/README.md diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index d4970e38f7c..4d6790ddbd4 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -254,6 +254,24 @@ describe Ci::Build, models: true do end end + describe '#ref_slug' do + { + 'master' => 'master', + '1-foo' => '1-foo', + 'fix/1-foo' => 'fix-1-foo', + 'fix-1-foo' => 'fix-1-foo', + 'a' * 63 => 'a' * 63, + 'a' * 64 => 'a' * 63, + 'FOO' => 'foo', + }.each do |ref, slug| + it "transforms #{ref} to #{slug}" do + build.ref = ref + + expect(build.ref_slug).to eq(slug) + end + end + end + describe '#variables' do let(:container_registry_enabled) { false } let(:predefined_variables) do @@ -265,6 +283,7 @@ describe Ci::Build, models: true do { key: 'CI_BUILD_REF', value: build.sha, public: true }, { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true }, { key: 'CI_BUILD_REF_NAME', value: 'master', public: true }, + { key: 'CI_BUILD_REF_SLUG', value: 'master', public: true }, { key: 'CI_BUILD_NAME', value: 'test', public: true }, { key: 'CI_BUILD_STAGE', value: 'test', public: true }, { key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, -- cgit v1.2.1 From 4d37b24614413e4f3e31d08296753fd07ec4bcad Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Lopez <jvargas@gitlab.com> Date: Mon, 28 Nov 2016 15:42:38 -0600 Subject: Fixed file template dropdown for the "New File" editor for smaller/zoomed screens --- app/assets/stylesheets/pages/editor.scss | 51 +++++++++++++++++++++- .../file-template-dropwdown-proper-position.yml | 4 ++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/file-template-dropwdown-proper-position.yml diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 778126bcfb7..935f157b33d 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -51,8 +51,16 @@ .new-file-name { display: inline-block; - width: 450px; + max-width: 450px; float: left; + + @media(max-width: $screen-md-max) { + width: 280px; + } + + @media(max-width: $screen-sm-max) { + width: 180px; + } } .file-buttons { @@ -114,3 +122,44 @@ } } } + +@media(max-width: $screen-xs-max){ + .file-editor { + .file-title { + .pull-right { + height: auto; + } + } + + .new-file-name { + margin-bottom: 0; + max-width: none; + width: 100%; + } + + .file-buttons { + display: flex; + flex-direction: column; + width: 100%; + margin-bottom: 7px; + + .soft-wrap-toggle { + margin: 7px 0 0; + } + + .encoding-selector, + .license-selector, + .gitignore-selector, + .gitlab-ci-yml-selector { + margin: 7px 0 0; + + button { + width: 100%; + } + } + + } + + } + +} diff --git a/changelogs/unreleased/file-template-dropwdown-proper-position.yml b/changelogs/unreleased/file-template-dropwdown-proper-position.yml new file mode 100644 index 00000000000..cf2a622b7e6 --- /dev/null +++ b/changelogs/unreleased/file-template-dropwdown-proper-position.yml @@ -0,0 +1,4 @@ +--- +title: Fixed file template dropdown for the "New File" editor for smaller/zoomed screens +merge_request: +author: -- cgit v1.2.1 From 23f5865e184c1738df86893d31392faf4bc2bad7 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 13 Dec 2016 21:01:05 -0600 Subject: expand remaining non-explicit eslint-disable blocks and factor out globals when no-undef encountered --- app/assets/javascripts/abuse_reports.js.es6 | 3 +- app/assets/javascripts/blob/blob_ci_yaml.js.es6 | 4 ++- .../javascripts/blob/blob_license_selectors.js.es6 | 4 ++- .../javascripts/blob/template_selector.js.es6 | 3 +- app/assets/javascripts/boards/boards_bundle.js.es6 | 5 ++- .../javascripts/boards/components/board.js.es6 | 5 ++- .../boards/components/board_blank_state.js.es6 | 5 ++- .../boards/components/board_card.js.es6 | 4 ++- .../boards/components/board_delete.js.es6 | 4 ++- .../boards/components/board_list.js.es6 | 5 ++- .../boards/components/board_new_issue.js.es6 | 5 ++- .../boards/components/board_sidebar.js.es6 | 8 ++++- .../boards/components/new_list_dropdown.js.es6 | 11 +++--- .../boards/filters/due_date_filters.js.es6 | 3 +- .../boards/mixins/sortable_default_options.js.es6 | 4 ++- app/assets/javascripts/boards/models/issue.js.es6 | 7 +++- app/assets/javascripts/boards/models/label.js.es6 | 3 +- app/assets/javascripts/boards/models/list.js.es6 | 5 ++- .../javascripts/boards/models/milestone.js.es6 | 5 +-- app/assets/javascripts/boards/models/user.js.es6 | 5 +-- .../boards/services/board_service.js.es6 | 4 ++- .../javascripts/boards/stores/boards_store.js.es6 | 5 ++- .../boards/vue_resource_interceptor.js.es6 | 4 ++- app/assets/javascripts/build_variables.js.es6 | 3 +- app/assets/javascripts/compare_autocomplete.js.es6 | 3 +- app/assets/javascripts/create_label.js.es6 | 4 ++- .../components/comment_resolve_btn.js.es6 | 5 ++- .../components/jump_to_discussion.js.es6 | 6 +++- .../diff_notes/components/resolve_btn.js.es6 | 7 +++- .../diff_notes/components/resolve_count.js.es6 | 6 +++- .../components/resolve_discussion_btn.js.es6 | 6 +++- .../diff_notes/diff_notes_bundle.js.es6 | 5 ++- .../diff_notes/mixins/discussion.js.es6 | 3 +- .../diff_notes/models/discussion.js.es6 | 7 ++-- .../javascripts/diff_notes/models/note.js.es6 | 5 +-- .../javascripts/diff_notes/services/resolve.js.es6 | 6 +++- .../javascripts/diff_notes/stores/comments.js.es6 | 5 ++- app/assets/javascripts/dispatcher.js.es6 | 40 +++++++++++++++++++++- app/assets/javascripts/due_date_select.js.es6 | 3 +- app/assets/javascripts/gfm_auto_complete.js.es6 | 3 +- app/assets/javascripts/gl_field_errors.js.es6 | 2 +- .../javascripts/group_label_subscription.js.es6 | 3 +- app/assets/javascripts/issuable.js.es6 | 5 ++- .../javascripts/issues_bulk_assignment.js.es6 | 5 ++- app/assets/javascripts/label_manager.js.es6 | 5 +-- app/assets/javascripts/lib/ace.js | 1 - .../components/diff_file_editor.js.es6 | 6 +++- .../components/inline_conflict_lines.js.es6 | 4 ++- .../components/parallel_conflict_lines.js.es6 | 6 ++-- .../merge_conflicts/merge_conflict_service.js.es6 | 3 +- .../merge_conflicts/merge_conflict_store.js.es6 | 5 ++- .../merge_conflicts/merge_conflicts_bundle.js.es6 | 5 ++- .../mixins/line_conflict_actions.js.es6 | 3 +- .../mixins/line_conflict_utils.js.es6 | 3 +- app/assets/javascripts/merge_request_widget.js.es6 | 23 ++++++++----- app/assets/javascripts/pipelines.js.es6 | 3 +- app/assets/javascripts/profile/gl_crop.js.es6 | 3 +- app/assets/javascripts/profile/profile.js.es6 | 4 ++- .../javascripts/project_label_subscription.js.es6 | 3 +- .../protected_branch_access_dropdown.js.es6 | 3 +- .../protected_branch_create.js.es6 | 4 ++- .../protected_branch_dropdown.js.es6 | 3 +- .../protected_branch_edit.js.es6 | 6 ++-- .../protected_branch_edit_list.js.es6 | 3 +- .../protected_branches_bundle.js | 1 - app/assets/javascripts/search_autocomplete.js.es6 | 3 +- app/assets/javascripts/sidebar.js.es6 | 4 ++- app/assets/javascripts/snippets_list.js.es6 | 3 +- .../templates/issuable_template_selector.js.es6 | 4 ++- .../templates/issuable_template_selectors.js.es6 | 3 +- app/assets/javascripts/todos.js.es6 | 5 ++- app/assets/javascripts/user.js.es6 | 4 ++- app/assets/javascripts/user_tabs.js.es6 | 3 +- app/assets/javascripts/username_validator.js.es6 | 3 +- spec/javascripts/abuse_reports_spec.js.es6 | 4 +-- spec/javascripts/activities_spec.js.es6 | 3 +- spec/javascripts/boards/boards_store_spec.js.es6 | 9 ++++- spec/javascripts/boards/issue_spec.js.es6 | 5 ++- spec/javascripts/boards/list_spec.js.es6 | 8 ++++- spec/javascripts/boards/mock_data.js.es6 | 3 +- spec/javascripts/dashboard_spec.js.es6 | 3 +- spec/javascripts/datetime_utility_spec.js.es6 | 2 +- spec/javascripts/diff_comments_store_spec.js.es6 | 5 ++- spec/javascripts/gl_dropdown_spec.js.es6 | 4 ++- spec/javascripts/gl_field_errors_spec.js.es6 | 3 +- spec/javascripts/labels_issue_sidebar_spec.js.es6 | 3 +- spec/javascripts/subbable_resource_spec.js.es6 | 3 +- 87 files changed, 320 insertions(+), 107 deletions(-) diff --git a/app/assets/javascripts/abuse_reports.js.es6 b/app/assets/javascripts/abuse_reports.js.es6 index 82e526ae0ef..8a260aae1b1 100644 --- a/app/assets/javascripts/abuse_reports.js.es6 +++ b/app/assets/javascripts/abuse_reports.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-param-reassign */ + ((global) => { const MAX_MESSAGE_LENGTH = 500; const MESSAGE_CELL_SELECTOR = '.abuse-reports .message'; diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js.es6 index 37531aaec9b..57bd13eecf8 100644 --- a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 +++ b/app/assets/javascripts/blob/blob_ci_yaml.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */ +/* global Api */ + /*= require blob/template_selector */ ((global) => { diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.es6 b/app/assets/javascripts/blob/blob_license_selectors.js.es6 index adeb8ba1318..268640681d4 100644 --- a/app/assets/javascripts/blob/blob_license_selectors.js.es6 +++ b/app/assets/javascripts/blob/blob_license_selectors.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars, no-param-reassign, padded-blocks */ +/* global BlobLicenseSelector */ + ((global) => { class BlobLicenseSelectors { constructor({ $dropdowns, editor }) { diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js.es6 index 0ff5c0fab05..7a1ee9998c8 100644 --- a/app/assets/javascripts/blob/template_selector.js.es6 +++ b/app/assets/javascripts/blob/template_selector.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable indent, comma-dangle, object-shorthand, func-names, space-before-function-paren, arrow-parens, no-unused-vars, class-methods-use-this, no-var, consistent-return, prefer-const, no-param-reassign, space-in-parens, max-len */ + ((global) => { class TemplateSelector { constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) { diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index 7ba918a05f8..ab2343c72fc 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable one-var, indent, quote-props, comma-dangle, space-before-function-paren */ +/* global Vue */ +/* global BoardService */ + //= require vue //= require vue-resource //= require Sortable diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6 index 31de3b25284..d1fb0ec48e0 100644 --- a/app/assets/javascripts/boards/components/board.js.es6 +++ b/app/assets/javascripts/boards/components/board.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, space-before-function-paren, one-var, indent, radix */ +/* global Vue */ +/* global Sortable */ + //= require ./board_blank_state //= require ./board_delete //= require ./board_list diff --git a/app/assets/javascripts/boards/components/board_blank_state.js.es6 b/app/assets/javascripts/boards/components/board_blank_state.js.es6 index 691487b272a..0a47a22fad2 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.js.es6 +++ b/app/assets/javascripts/boards/components/board_blank_state.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable space-before-function-paren, comma-dangle, semi */ +/* global Vue */ +/* global ListLabel */ + (() => { const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6 index 2299dafd217..5fc50280811 100644 --- a/app/assets/javascripts/boards/components/board_card.js.es6 +++ b/app/assets/javascripts/boards/components/board_card.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, space-before-function-paren, dot-notation */ +/* global Vue */ + (() => { const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/boards/components/board_delete.js.es6 b/app/assets/javascripts/boards/components/board_delete.js.es6 index c45e1926c5c..861600424a5 100644 --- a/app/assets/javascripts/boards/components/board_delete.js.es6 +++ b/app/assets/javascripts/boards/components/board_delete.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, space-before-function-paren, no-alert */ +/* global Vue */ + (() => { window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index 43ebeef39c4..6711930622b 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, space-before-function-paren, max-len, no-plusplus */ +/* global Vue */ +/* global Sortable */ + //= require ./board_card //= require ./board_new_issue diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6 index a7989a2ff4c..2386d3a613c 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.js.es6 +++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, no-unused-vars */ +/* global Vue */ +/* global ListIssue */ + (() => { const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6 index 1644a772737..02459722bbf 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js.es6 +++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6 @@ -1,4 +1,10 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, space-before-function-paren, no-new */ +/* global Vue */ +/* global IssuableContext */ +/* global MilestoneSelect */ +/* global LabelsSelect */ +/* global Sidebar */ + (() => { const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 index 10ce746deb5..3f5cf8420a8 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var, indent */ + (() => { window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; @@ -45,10 +46,10 @@ return $li.append($a.prepend($labelColor)); }, - search: { - fields: ['title'] - }, - filterable: true, + search: { + fields: ['title'] + }, + filterable: true, selectable: true, multiSelect: true, clicked (label, $el, e) { diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 b/app/assets/javascripts/boards/filters/due_date_filters.js.es6 index 9eceac4eddd..7e192e90fe6 100644 --- a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 +++ b/app/assets/javascripts/boards/filters/due_date_filters.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* global Vue */ + Vue.filter('due-date', (value) => { const date = new Date(value); return $.datepicker.formatDate('M d, yy', date); diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 index 5f99de39122..a5e62ed775d 100644 --- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 +++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars, no-mixed-operators, prefer-const, comma-dangle, semi */ +/* global DocumentTouch */ + ((w) => { window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6 index 21d735e8231..1199e022ff1 100644 --- a/app/assets/javascripts/boards/models/issue.js.es6 +++ b/app/assets/javascripts/boards/models/issue.js.es6 @@ -1,4 +1,9 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, space-in-parens, arrow-parens, comma-dangle, max-len */ +/* global Vue */ +/* global ListLabel */ +/* global ListMilestone */ +/* global ListUser */ + class ListIssue { constructor (obj) { this.id = obj.iid; diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js.es6 index 0910fe9a854..8f20a1bbec7 100644 --- a/app/assets/javascripts/boards/models/label.js.es6 +++ b/app/assets/javascripts/boards/models/label.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars, space-before-function-paren */ + class ListLabel { constructor (obj) { this.id = obj.id; diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index 429bd27c3fb..a8d38c16485 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-plusplus, prefer-const, space-in-parens, no-shadow, no-param-reassign, max-len, no-unused-vars */ +/* global ListIssue */ +/* global ListLabel */ + class List { constructor (obj) { this.id = obj.id; diff --git a/app/assets/javascripts/boards/models/milestone.js.es6 b/app/assets/javascripts/boards/models/milestone.js.es6 index a48969e19c9..9c173c1b70b 100644 --- a/app/assets/javascripts/boards/models/milestone.js.es6 +++ b/app/assets/javascripts/boards/models/milestone.js.es6 @@ -1,6 +1,7 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars */ + class ListMilestone { - constructor (obj) { + constructor(obj) { this.id = obj.id; this.title = obj.title; } diff --git a/app/assets/javascripts/boards/models/user.js.es6 b/app/assets/javascripts/boards/models/user.js.es6 index 583a973fc46..a8a3892e2ad 100644 --- a/app/assets/javascripts/boards/models/user.js.es6 +++ b/app/assets/javascripts/boards/models/user.js.es6 @@ -1,6 +1,7 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars */ + class ListUser { - constructor (user) { + constructor(user) { this.id = user.id; this.name = user.name; this.username = user.username; diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 index f59a2ed7937..189a8703198 100644 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ b/app/assets/javascripts/boards/services/board_service.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, prefer-const, no-extra-semi, max-len, no-unused-vars */ +/* global Vue */ + class BoardService { constructor (root, boardId) { this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, { diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index bb2a4de8b18..e7a14ea5bca 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, space-before-function-paren, one-var, indent, space-in-parens, no-shadow, radix, dot-notation, semi, max-len */ +/* global Cookies */ +/* global List */ + (() => { window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 index 80f137ca12e..3723a2039f9 100644 --- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 +++ b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars, no-plusplus */ +/* global Vue */ + Vue.http.interceptors.push((request, next) => { Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; diff --git a/app/assets/javascripts/build_variables.js.es6 b/app/assets/javascripts/build_variables.js.es6 index 0ecd20bc11e..993424d422f 100644 --- a/app/assets/javascripts/build_variables.js.es6 +++ b/app/assets/javascripts/build_variables.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable func-names, prefer-arrow-callback, space-before-blocks, space-before-function-paren, comma-spacing, max-len */ + $(function(){ $('.reveal-variables').off('click').on('click',function(){ $('.js-build').toggle().niceScroll(); diff --git a/app/assets/javascripts/compare_autocomplete.js.es6 b/app/assets/javascripts/compare_autocomplete.js.es6 index bd980f87e72..45c974b2b68 100644 --- a/app/assets/javascripts/compare_autocomplete.js.es6 +++ b/app/assets/javascripts/compare_autocomplete.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, no-dupe-keys, wrap-iife, padded-blocks, max-len */ + (function() { this.CompareAutocomplete = (function() { function CompareAutocomplete() { diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js.es6 index 744aa0afa03..947c129d5b5 100644 --- a/app/assets/javascripts/create_label.js.es6 +++ b/app/assets/javascripts/create_label.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-param-reassign, wrap-iife, max-len */ +/* global Api */ + (function (w) { class CreateLabelDropdown { constructor ($el, namespacePath, projectPath) { diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 index 52e2846d279..c59d3996fab 100644 --- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 +++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, semi, max-len */ +/* global Vue */ +/* global CommentsStore */ + (() => { const CommentAndResolveBtn = Vue.extend({ props: { diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 index 983e554b9c1..fd1d1cad272 100644 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 +++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 @@ -1,4 +1,8 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, indent, space-before-function-paren, no-plusplus, no-lonely-if, no-continue, brace-style, max-len, quotes, no-undef, semi */ +/* global Vue */ +/* global DiscussionMixins */ +/* global CommentsStore */ + (() => { JumpToDiscussion = Vue.extend({ mixins: [DiscussionMixins], diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 index 27af9fc96ad..88a19fc6e1d 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 @@ -1,4 +1,9 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, no-new, max-len */ +/* global Vue */ +/* global CommentsStore */ +/* global ResolveService */ +/* global Flash */ + (() => { const ResolveBtn = Vue.extend({ props: { diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 index 9522ccb49da..72cdae812bc 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 @@ -1,4 +1,8 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, object-shorthand, func-names, no-param-reassign */ +/* global Vue */ +/* global DiscussionMixins */ +/* global CommentsStore */ + ((w) => { w.ResolveCount = Vue.extend({ mixins: [DiscussionMixins], diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 index b945a09fcbe..ee5f62b2d9e 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 @@ -1,4 +1,8 @@ -/* eslint-disable */ +/* eslint-disable object-shorthand, func-names, space-before-function-paren, comma-dangle, no-else-return, quotes, max-len */ +/* global Vue */ +/* global CommentsStore */ +/* global ResolveService */ + (() => { const ResolveDiscussionBtn = Vue.extend({ props: { diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 index bd4c20aed8b..840b5aa922e 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable func-names, comma-dangle, new-cap, no-new */ +/* global Vue */ +/* global ResolveCount */ + //= require vue //= require vue-resource //= require_directory ./models diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 index 7a929017f36..a9ea18bf82b 100644 --- a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 +++ b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-plusplus, no-param-reassign, max-len */ + ((w) => { w.DiscussionMixins = { computed: { diff --git a/app/assets/javascripts/diff_notes/models/discussion.js.es6 b/app/assets/javascripts/diff_notes/models/discussion.js.es6 index badcdccc840..efcd46680a7 100644 --- a/app/assets/javascripts/diff_notes/models/discussion.js.es6 +++ b/app/assets/javascripts/diff_notes/models/discussion.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable space-before-function-paren, camelcase, guard-for-in, no-restricted-syntax, no-unused-vars, max-len */ +/* global Vue */ +/* global NoteModel */ + class DiscussionModel { constructor (discussionId) { this.id = discussionId; @@ -69,7 +72,7 @@ class DiscussionModel { gl.utils.localTimeAgo($('.js-timeago', `${discussionSelector}`)); } else { - $discussionHeadline.remove(); + $discussionHeadline.remove(); } } diff --git a/app/assets/javascripts/diff_notes/models/note.js.es6 b/app/assets/javascripts/diff_notes/models/note.js.es6 index d0541b02632..e3bce1d2038 100644 --- a/app/assets/javascripts/diff_notes/models/note.js.es6 +++ b/app/assets/javascripts/diff_notes/models/note.js.es6 @@ -1,6 +1,7 @@ -/* eslint-disable */ +/* eslint-disable camelcase, no-unused-vars */ + class NoteModel { - constructor (discussionId, noteId, canResolve, resolved, resolved_by) { + constructor(discussionId, noteId, canResolve, resolved, resolved_by) { this.discussionId = discussionId; this.id = noteId; this.canResolve = canResolve; diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6 index 86953ce7ffb..78c74985f78 100644 --- a/app/assets/javascripts/diff_notes/services/resolve.js.es6 +++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6 @@ -1,4 +1,8 @@ -/* eslint-disable */ +/* eslint-disable class-methods-use-this, one-var, indent, camelcase, no-new, comma-dangle, semi, no-param-reassign, max-len */ +/* global Vue */ +/* global Flash */ +/* global CommentsStore */ + ((w) => { class ResolveServiceClass { constructor() { diff --git a/app/assets/javascripts/diff_notes/stores/comments.js.es6 b/app/assets/javascripts/diff_notes/stores/comments.js.es6 index f42ca406bb1..1a7abbc6f75 100644 --- a/app/assets/javascripts/diff_notes/stores/comments.js.es6 +++ b/app/assets/javascripts/diff_notes/stores/comments.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable object-shorthand, func-names, camelcase, prefer-const, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */ +/* global Vue */ +/* global DiscussionModel */ + ((w) => { w.CommentsStore = { state: {}, diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 413117c2226..7d588e8eee6 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -1,4 +1,42 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len, padded-blocks */ +/* global UsernameValidator */ +/* global ActiveTabMemoizer */ +/* global ShortcutsNavigation */ +/* global Build */ +/* global Issuable */ +/* global Issue */ +/* global ShortcutsIssuable */ +/* global ZenMode */ +/* global Milestone */ +/* global GLForm */ +/* global IssuableForm */ +/* global LabelsSelect */ +/* global MilestoneSelect */ +/* global MergedButtons */ +/* global Commit */ +/* global NotificationsForm */ +/* global TreeView */ +/* global NotificationsDropdown */ +/* global UsersSelect */ +/* global GroupAvatar */ +/* global LineHighlighter */ +/* global ShortcutsBlob */ +/* global ProjectFork */ +/* global BuildArtifacts */ +/* global GroupsSelect */ +/* global Search */ +/* global Admin */ +/* global NamespaceSelects */ +/* global ShortcutsDashboardNavigation */ +/* global Project */ +/* global ProjectAvatar */ +/* global CompareAutocomplete */ +/* global ProjectNew */ +/* global Star */ +/* global ProjectShow */ +/* global Labels */ +/* global Shortcuts */ + (function() { var Dispatcher; diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6 index e84f5ac9183..2b7d57d86c6 100644 --- a/app/assets/javascripts/due_date_select.js.es6 +++ b/app/assets/javascripts/due_date_select.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, prefer-const, padded-blocks, no-unused-vars, no-underscore-dangle, no-new, max-len, semi, no-sequences, no-unused-expressions, no-param-reassign */ + (function(global) { class DueDateSelect { constructor({ $dropdown, $loading } = {}) { diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 2f3da745119..79501aa4ab9 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, padded-blocks, vars-on-top, indent, no-extra-semi, no-multi-spaces, semi, no-undef, max-len */ + // Creates the variables for setting up GFM auto-completion (function() { if (window.GitLab == null) { diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6 index 6ce392d2a5b..63f9cafa8d0 100644 --- a/app/assets/javascripts/gl_field_errors.js.es6 +++ b/app/assets/javascripts/gl_field_errors.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign, padded-blocks */ //= require gl_field_error diff --git a/app/assets/javascripts/group_label_subscription.js.es6 b/app/assets/javascripts/group_label_subscription.js.es6 index eea6cd40859..8e10e424412 100644 --- a/app/assets/javascripts/group_label_subscription.js.es6 +++ b/app/assets/javascripts/group_label_subscription.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, padded-blocks, max-len */ + (function(global) { class GroupLabelSubscription { constructor(container) { diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 index 46503c290ae..b174eb2ff96 100644 --- a/app/assets/javascripts/issuable.js.es6 +++ b/app/assets/javascripts/issuable.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, prefer-const, padded-blocks, wrap-iife, max-len */ +/* global Issuable */ +/* global Turbolinks */ + (function() { var issuable_created; diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6 index 9697fb33566..ad25104152c 100644 --- a/app/assets/javascripts/issues_bulk_assignment.js.es6 +++ b/app/assets/javascripts/issues_bulk_assignment.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, radix, max-len, padded-blocks, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */ +/* global Issuable */ +/* global Flash */ + ((global) => { class IssuableBulkActions { diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js.es6 index 175623e7448..33c5e35324d 100644 --- a/app/assets/javascripts/label_manager.js.es6 +++ b/app/assets/javascripts/label_manager.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, padded-blocks, max-len */ +/* global Flash */ + ((global) => { class LabelManager { @@ -104,4 +106,3 @@ gl.LabelManager = LabelManager; })(window.gl || (window.gl = {})); - diff --git a/app/assets/javascripts/lib/ace.js b/app/assets/javascripts/lib/ace.js index b1718e89d3d..4cdf99cae72 100644 --- a/app/assets/javascripts/lib/ace.js +++ b/app/assets/javascripts/lib/ace.js @@ -1,3 +1,2 @@ -/* eslint-disable */ /*= require ace-rails-ap */ /*= require ace/ext-searchbox */ diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 index 9e4ffd07dbd..f95b079c972 100644 --- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 @@ -1,4 +1,8 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, prefer-const, no-new, padded-blocks, no-param-reassign, semi, max-len */ +/* global Vue */ +/* global ace */ +/* global Flash */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 index 23c4618af70..74544b7d0c7 100644 --- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */ +/* global Vue */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 index 4ccbdcd6daa..78c00c31c16 100644 --- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */ +/* global Vue */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; @@ -19,7 +21,7 @@ </td> <td class="diff-line-num old_line" :class="lineCssClass(line)" v-if="!line.isHeader">{{line.lineNumber}}</td> <td class="line_content parallel" :class="lineCssClass(line)" v-if="!line.isHeader" v-html="line.richText"></td> - </template> + </template> </tr> </table> `, diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 index 8a7519b0786..8df3170edac 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-param-reassign, comma-dangle, no-extra-semi, padded-blocks */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 index f94e51e783c..53b44007510 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, object-shorthand, no-dupe-keys, no-param-reassign, no-plusplus, camelcase, prefer-const, no-nested-ternary, no-continue, semi, func-call-spacing, no-spaced-func, padded-blocks, max-len */ +/* global Cookies */ +/* global Vue */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 index 815443fb54e..83520702f9b 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable new-cap, comma-dangle, no-new, semi */ +/* global Vue */ +/* global Flash */ + //= require vue //= require ./merge_conflict_store //= require ./merge_conflict_service diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 index c8de586aa21..e89b35d5407 100644 --- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 +++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-param-reassign, comma-dangle, padded-blocks */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 index 88c3a20ce13..a4aca85d460 100644 --- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 +++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-param-reassign, quote-props, comma-dangle, padded-blocks */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index 7022aa1263b..e47047c4cca 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -1,5 +1,10 @@ -/* eslint-disable */ - ((global) => { +/* eslint-disable max-len, no-var, func-names, space-before-function-paren, vars-on-top, no-plusplus, comma-dangle, no-return-assign, consistent-return, no-param-reassign, one-var, one-var-declaration-per-line, quotes, prefer-template, no-else-return, prefer-arrow-callback, no-unused-vars, no-underscore-dangle, no-shadow, no-mixed-operators, template-curly-spacing, camelcase, default-case, wrap-iife, semi, padded-blocks */ +/* global notify */ +/* global notifyPermissions */ +/* global merge_request_widget */ +/* global Turbolinks */ + +((global) => { var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; const DEPLOYMENT_TEMPLATE = `<div class="mr-widget-heading" id="<%- id %>"> @@ -27,7 +32,7 @@ </div> </div>`; - global.MergeRequestWidget = (function() { + global.MergeRequestWidget = (function() { function MergeRequestWidget(opts) { // Initialize MergeRequestWidget behavior // @@ -82,10 +87,10 @@ }; MergeRequestWidget.prototype.retrieveSuccessIcon = function() { - const $ciSuccessIcon = $('.js-success-icon'); - this.$ciSuccessIcon = $ciSuccessIcon.html(); - $ciSuccessIcon.remove(); - } + const $ciSuccessIcon = $('.js-success-icon'); + this.$ciSuccessIcon = $ciSuccessIcon.html(); + $ciSuccessIcon.remove(); + } MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) { if (deleteSourceBranch == null) { @@ -204,7 +209,7 @@ const template = _.template(templateString)(environment) this.$widgetBody.before(template); } - }; + }; MergeRequestWidget.prototype.showCIStatus = function(state) { var allowed_states; @@ -246,4 +251,4 @@ })(); - })(window.gl || (window.gl = {})); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 index fb95648e1c7..0b09ad113a3 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 @@ -1,6 +1,7 @@ +/* eslint-disable no-new, guard-for-in, no-restricted-syntax, no-continue, padded-blocks, no-param-reassign, max-len */ + //= require lib/utils/bootstrap_linked_tabs -/* eslint-disable */ ((global) => { class Pipelines { diff --git a/app/assets/javascripts/profile/gl_crop.js.es6 b/app/assets/javascripts/profile/gl_crop.js.es6 index 6da6c1d0295..b4b6da41f63 100644 --- a/app/assets/javascripts/profile/gl_crop.js.es6 +++ b/app/assets/javascripts/profile/gl_crop.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-useless-escape, max-len, padded-blocks, quotes, no-var, no-underscore-dangle, func-names, space-before-function-paren, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, no-plusplus, new-parens, semi */ + ((global) => { // Matches everything but the file name diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6 index 3eb81808bd6..eb2fe3477a2 100644 --- a/app/assets/javascripts/profile/profile.js.es6 +++ b/app/assets/javascripts/profile/profile.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len, padded-blocks */ +/* global Flash */ + ((global) => { class Profile { diff --git a/app/assets/javascripts/project_label_subscription.js.es6 b/app/assets/javascripts/project_label_subscription.js.es6 index 03a115cb35b..b8d6a198996 100644 --- a/app/assets/javascripts/project_label_subscription.js.es6 +++ b/app/assets/javascripts/project_label_subscription.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable wrap-iife, func-names, space-before-function-paren, object-shorthand, comma-dangle, one-var, one-var-declaration-per-line, no-restricted-syntax, prefer-const, max-len, no-param-reassign, padded-blocks */ + (function(global) { class ProjectLabelSubscription { constructor(container) { diff --git a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 index 2d60947a666..4aef1c84b56 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable arrow-parens, no-param-reassign, no-irregular-whitespace, object-shorthand, no-else-return, comma-dangle, semi, padded-blocks, max-len */ + (global => { global.gl = global.gl || {}; diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 index c45c9d8ff22..f26fba979a4 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable no-new, arrow-parens, no-param-reassign, no-irregular-whitespace, comma-dangle, padded-blocks, semi, max-len */ +/* global ProtectedBranchDropdown */ + (global => { global.gl = global.gl || {}; diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 index e3f226e9a2a..08264ad3d2f 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, no-unused-vars */ + class ProtectedBranchDropdown { constructor(options) { this.onSelect = options.onSelect; diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 index ac3142ffb07..4ff2fa5a80f 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable no-new, arrow-parens, no-param-reassign, no-irregular-whitespace, padded-blocks, comma-dangle, no-trailing-spaces, semi, max-len */ +/* global Flash */ + (global => { global.gl = global.gl || {}; @@ -33,7 +35,7 @@ const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`); // Do not update if one dropdown has not selected any option - if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return; + if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return; $.ajax({ type: 'POST', diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 index 705378a364d..b6972ef2e16 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable arrow-parens, no-param-reassign, no-irregular-whitespace, no-new, comma-dangle, semi, padded-blocks, max-len */ + (global => { global.gl = global.gl || {}; diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js index 17e34163831..15b3affd469 100644 --- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js +++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js @@ -1,2 +1 @@ -/* eslint-disable */ /*= require_tree . */ diff --git a/app/assets/javascripts/search_autocomplete.js.es6 b/app/assets/javascripts/search_autocomplete.js.es6 index 5fa94556501..437f5dbbf7d 100644 --- a/app/assets/javascripts/search_autocomplete.js.es6 +++ b/app/assets/javascripts/search_autocomplete.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, no-plusplus, prefer-template, quotes, class-methods-use-this, no-unused-expressions, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, padded-blocks, no-extra-semi, indent, max-len */ + ((global) => { const KEYCODE = { diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6 index a23ca449c4b..9790a44972d 100644 --- a/app/assets/javascripts/sidebar.js.es6 +++ b/app/assets/javascripts/sidebar.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign, padded-blocks */ +/* global Cookies */ + ((global) => { let singleton; diff --git a/app/assets/javascripts/snippets_list.js.es6 b/app/assets/javascripts/snippets_list.js.es6 index c3afc3f2246..6f913326a3a 100644 --- a/app/assets/javascripts/snippets_list.js.es6 +++ b/app/assets/javascripts/snippets_list.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable arrow-parens, no-param-reassign, space-before-function-paren, func-names, no-var, semi, max-len */ + (global => { global.gl = global.gl || {}; diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6 index 93a3d67ee9f..d2b152045b4 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js.es6 +++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable prefer-const, comma-dangle, max-len, no-useless-return, object-curly-spacing, no-param-reassign, max-len */ +/* global Api */ + /*= require ../blob/template_selector */ ((global) => { diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js.es6 index 0a3890e85fe..7310b9de074 100644 --- a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 +++ b/app/assets/javascripts/templates/issuable_template_selectors.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-new, comma-dangle, class-methods-use-this, prefer-const, no-param-reassign */ + ((global) => { class IssuableTemplateSelectors { constructor({ $dropdowns, editor } = {}) { diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6 index 213e80825b7..b777d966a43 100644 --- a/app/assets/javascripts/todos.js.es6 +++ b/app/assets/javascripts/todos.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable padded-blocks, class-methods-use-this, no-new, func-names, prefer-template, no-unneeded-ternary, object-shorthand, space-before-function-paren, comma-dangle, quote-props, consistent-return, no-else-return, semi, no-param-reassign, max-len, no-undef */ +/* global UsersSelect */ +/* global Turbolinks */ + ((global) => { class Todos { diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js.es6 index 5e869e99fdb..0a2db7c05fe 100644 --- a/app/assets/javascripts/user.js.es6 +++ b/app/assets/javascripts/user.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable class-methods-use-this, comma-dangle, arrow-parens, no-param-reassign, semi */ +/* global Cookies */ + ((global) => { global.User = class { constructor({ action }) { diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6 index 5a625611987..b9c23b51b4d 100644 --- a/app/assets/javascripts/user_tabs.js.es6 +++ b/app/assets/javascripts/user_tabs.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable max-len, space-before-function-paren, no-underscore-dangle, array-bracket-spacing, consistent-return, comma-dangle, no-unused-vars, dot-notation, no-new, no-return-assign, camelcase, semi, no-param-reassign */ + /* UserTabs diff --git a/app/assets/javascripts/username_validator.js.es6 b/app/assets/javascripts/username_validator.js.es6 index c4dde575c6e..26c64caf68a 100644 --- a/app/assets/javascripts/username_validator.js.es6 +++ b/app/assets/javascripts/username_validator.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, consistent-return, class-methods-use-this, arrow-parens, no-param-reassign, max-len */ + ((global) => { const debounceTimeoutDuration = 1000; const invalidInputClass = 'gl-field-error-outline'; diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6 index a3171353bfb..9e94c9d1d74 100644 --- a/spec/javascripts/abuse_reports_spec.js.es6 +++ b/spec/javascripts/abuse_reports_spec.js.es6 @@ -1,6 +1,6 @@ -/* eslint-disable */ -/*= require abuse_reports */ +/* eslint-disable space-before-function-paren, no-new, padded-blocks */ +/*= require abuse_reports */ /*= require jquery */ ((global) => { diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6 index 8640cd44085..192da4ee8d9 100644 --- a/spec/javascripts/activities_spec.js.es6 +++ b/spec/javascripts/activities_spec.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-unused-expressions, comma-spacing, prefer-const, no-prototype-builtins, semi, no-new, keyword-spacing, no-plusplus, no-shadow, max-len */ + /*= require js.cookie.js */ /*= require jquery.endless-scroll.js */ /*= require pager */ diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6 index b84dfc8197b..b3a1afa28a5 100644 --- a/spec/javascripts/boards/boards_store_spec.js.es6 +++ b/spec/javascripts/boards/boards_store_spec.js.es6 @@ -1,4 +1,11 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, one-var, no-unused-vars, indent */ +/* global Vue */ +/* global BoardService */ +/* global boardsMockInterceptor */ +/* global Cookies */ +/* global listObj */ +/* global listObjDuplicate */ + //= require jquery //= require jquery_ujs //= require js.cookie diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6 index 90cb8926545..c8a61a0a9b5 100644 --- a/spec/javascripts/boards/issue_spec.js.es6 +++ b/spec/javascripts/boards/issue_spec.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle */ +/* global BoardService */ +/* global ListIssue */ + //= require jquery //= require jquery_ujs //= require js.cookie diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6 index dfbcbe3a7c1..7d942ec3d65 100644 --- a/spec/javascripts/boards/list_spec.js.es6 +++ b/spec/javascripts/boards/list_spec.js.es6 @@ -1,4 +1,10 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle */ +/* global Vue */ +/* global boardsMockInterceptor */ +/* global BoardService */ +/* global List */ +/* global listObj */ + //= require jquery //= require jquery_ujs //= require js.cookie diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6 index fcb3d8f17d8..8d3e2237fda 100644 --- a/spec/javascripts/boards/mock_data.js.es6 +++ b/spec/javascripts/boards/mock_data.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, no-unused-vars, quote-props */ + const listObj = { id: 1, position: 0, diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6 index 93f73fa0e9a..aadf6f518a8 100644 --- a/spec/javascripts/dashboard_spec.js.es6 +++ b/spec/javascripts/dashboard_spec.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-new, padded-blocks */ + /*= require sidebar */ /*= require jquery */ /*= require js.cookie */ diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js.es6 index 9fdbab3a9e9..8ece24555c5 100644 --- a/spec/javascripts/datetime_utility_spec.js.es6 +++ b/spec/javascripts/datetime_utility_spec.js.es6 @@ -1,5 +1,5 @@ -/* eslint-disable */ //= require lib/utils/datetime_utility + (() => { describe('Date time utils', () => { describe('get day name', () => { diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6 index 9b2845af608..18805d26ac0 100644 --- a/spec/javascripts/diff_comments_store_spec.js.es6 +++ b/spec/javascripts/diff_comments_store_spec.js.es6 @@ -1,8 +1,11 @@ -/* eslint-disable */ +/* eslint-disable no-extra-semi, jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown, max-len */ +/* global CommentsStore */ + //= require vue //= require diff_notes/models/discussion //= require diff_notes/models/note //= require diff_notes/stores/comments + (() => { function createDiscussion(noteId = 1, resolved = true) { CommentsStore.create('a', noteId, true, resolved, 'test'); diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6 index 8ba238018cd..bfaf90e2aee 100644 --- a/spec/javascripts/gl_dropdown_spec.js.es6 +++ b/spec/javascripts/gl_dropdown_spec.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, prefer-const, no-param-reassign, no-plusplus, semi, no-unused-expressions, arrow-spacing, max-len */ +/* global Turbolinks */ + /*= require jquery */ /*= require gl_dropdown */ /*= require turbolinks */ diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6 index 0713e30e485..5018e87ad6c 100644 --- a/spec/javascripts/gl_field_errors_spec.js.es6 +++ b/spec/javascripts/gl_field_errors_spec.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable space-before-function-paren, arrow-body-style, indent, padded-blocks */ + //= require jquery //= require gl_field_errors diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6 index 49687048eb5..9399166c80a 100644 --- a/spec/javascripts/labels_issue_sidebar_spec.js.es6 +++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-new, no-undef, no-plusplus, object-curly-spacing, prefer-const, semi */ + //= require lib/utils/type_utility //= require jquery //= require bootstrap diff --git a/spec/javascripts/subbable_resource_spec.js.es6 b/spec/javascripts/subbable_resource_spec.js.es6 index df395296791..6a70dd856a7 100644 --- a/spec/javascripts/subbable_resource_spec.js.es6 +++ b/spec/javascripts/subbable_resource_spec.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable max-len, arrow-parens, comma-dangle, no-plusplus */ + //= vue //= vue-resource //= require jquery -- cgit v1.2.1 From 309580bd41a178a02894d399e3cd45659a8ba7a7 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Wed, 14 Dec 2016 08:36:03 +0100 Subject: Remove trailing blank line from Allowable module --- lib/gitlab/allowable.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/gitlab/allowable.rb b/lib/gitlab/allowable.rb index 3cc218f9db4..f48abcc86d5 100644 --- a/lib/gitlab/allowable.rb +++ b/lib/gitlab/allowable.rb @@ -5,4 +5,3 @@ module Gitlab end end end - -- cgit v1.2.1 From 8e0e902b133f33b38ffaa743ac3f3297b4bceb2d Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 13 Dec 2016 23:26:26 -0600 Subject: resolve all instances of no-undef eslint rule violations --- app/assets/javascripts/admin.js | 4 +++- app/assets/javascripts/api.js | 6 ++++-- app/assets/javascripts/application.js | 9 ++++++++- app/assets/javascripts/awards_handler.js | 4 +++- app/assets/javascripts/behaviors/autosize.js | 3 ++- app/assets/javascripts/behaviors/quick_submit.js | 3 ++- app/assets/javascripts/blob/blob_file_dropzone.js | 4 +++- app/assets/javascripts/blob/blob_gitignore_selector.js | 3 ++- app/assets/javascripts/blob/blob_gitignore_selectors.js | 4 +++- app/assets/javascripts/blob/blob_license_selector.js | 3 ++- app/assets/javascripts/blob_edit/blob_edit_bundle.js | 5 ++++- app/assets/javascripts/blob_edit/edit_blob.js | 5 ++++- app/assets/javascripts/breakpoints.js | 6 ++++-- app/assets/javascripts/build.js | 6 +++++- app/assets/javascripts/commit.js | 4 +++- app/assets/javascripts/commit/file.js | 4 +++- app/assets/javascripts/commits.js | 8 +++++--- app/assets/javascripts/copy_to_clipboard.js | 3 ++- .../diff_notes/components/jump_to_discussion.js.es6 | 4 ++-- app/assets/javascripts/dropzone_input.js | 3 ++- app/assets/javascripts/files_comment_button.js | 4 +++- app/assets/javascripts/gfm_auto_complete.js.es6 | 6 +++--- app/assets/javascripts/gl_dropdown.js | 7 +++++-- app/assets/javascripts/gl_form.js | 6 +++++- app/assets/javascripts/graphs/stat_graph_contributors.js | 7 ++++++- app/assets/javascripts/graphs/stat_graph_contributors_graph.js | 4 +++- app/assets/javascripts/groups_select.js | 6 ++++-- app/assets/javascripts/importer_status.js | 7 ++++--- app/assets/javascripts/issuable_context.js | 4 +++- app/assets/javascripts/issuable_form.js | 7 ++++++- app/assets/javascripts/issue.js | 3 ++- app/assets/javascripts/labels_select.js | 5 ++++- app/assets/javascripts/lib/utils/notify.js | 3 ++- app/assets/javascripts/line_highlighter.js | 3 ++- app/assets/javascripts/merge_request.js | 3 ++- app/assets/javascripts/merged_buttons.js | 3 ++- app/assets/javascripts/milestone.js | 4 +++- app/assets/javascripts/milestone_select.js | 6 +++++- app/assets/javascripts/namespace_select.js | 10 ++++++---- app/assets/javascripts/network/branch_graph.js | 4 +++- app/assets/javascripts/network/network.js | 4 +++- app/assets/javascripts/network/network_bundle.js | 5 ++++- app/assets/javascripts/notes.js | 8 ++++++-- app/assets/javascripts/notifications_dropdown.js | 4 +++- app/assets/javascripts/preview_markdown.js | 7 ++++--- app/assets/javascripts/project.js | 6 +++++- app/assets/javascripts/project_find_file.js | 4 +++- app/assets/javascripts/project_import.js | 4 +++- app/assets/javascripts/project_select.js | 4 +++- app/assets/javascripts/projects_list.js | 7 ++++--- app/assets/javascripts/right_sidebar.js | 4 +++- app/assets/javascripts/search.js | 4 +++- app/assets/javascripts/shortcuts.js | 6 +++++- app/assets/javascripts/shortcuts_blob.js | 4 +++- app/assets/javascripts/shortcuts_dashboard_navigation.js | 4 +++- app/assets/javascripts/shortcuts_find_file.js | 4 +++- app/assets/javascripts/shortcuts_issuable.js | 6 +++++- app/assets/javascripts/shortcuts_navigation.js | 4 +++- app/assets/javascripts/shortcuts_network.js | 4 +++- app/assets/javascripts/single_file_diff.js | 7 ++++--- app/assets/javascripts/snippet/snippet_bundle.js | 4 +++- app/assets/javascripts/star.js | 4 +++- app/assets/javascripts/syntax_highlight.js | 3 ++- app/assets/javascripts/todos.js.es6 | 4 ++-- app/assets/javascripts/u2f/authenticate.js | 6 +++++- app/assets/javascripts/u2f/error.js | 4 +++- app/assets/javascripts/u2f/register.js | 6 +++++- app/assets/javascripts/users/calendar.js | 5 ++++- app/assets/javascripts/users_select.js | 6 +++++- app/assets/javascripts/zen_mode.js | 5 ++++- app/views/shared/issuable/_sidebar.html.haml | 2 +- spec/javascripts/awards_handler_spec.js | 3 ++- spec/javascripts/behaviors/quick_submit_spec.js | 2 +- spec/javascripts/graphs/stat_graph_contributors_graph_spec.js | 6 +++++- spec/javascripts/graphs/stat_graph_contributors_util_spec.js | 4 +++- spec/javascripts/graphs/stat_graph_spec.js | 4 +++- spec/javascripts/issue_spec.js | 5 +++-- spec/javascripts/labels_issue_sidebar_spec.js.es6 | 4 +++- spec/javascripts/line_highlighter_spec.js | 3 ++- spec/javascripts/merge_request_spec.js | 3 ++- spec/javascripts/new_branch_spec.js | 3 ++- spec/javascripts/notes_spec.js | 4 +++- spec/javascripts/project_title_spec.js | 4 +++- spec/javascripts/right_sidebar_spec.js | 3 ++- spec/javascripts/shortcuts_issuable_spec.js | 3 ++- spec/javascripts/u2f/authenticate_spec.js | 4 +++- spec/javascripts/u2f/register_spec.js | 4 +++- spec/javascripts/zen_mode_spec.js | 5 ++++- 88 files changed, 295 insertions(+), 111 deletions(-) diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index 31852e4750c..5a7d823e84c 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, padded-blocks, max-len */ +/* global Turbolinks */ + (function() { this.Admin = (function() { function Admin() { diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 1c625e2f2b1..81c1e01901e 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -1,6 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, no-undef, comma-dangle, prefer-arrow-callback, indent, object-curly-spacing, quote-props, no-param-reassign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, comma-dangle, prefer-arrow-callback, indent, object-curly-spacing, quote-props, no-param-reassign, padded-blocks, max-len */ + (function() { - this.Api = { + var Api = { groupsPath: "/api/:version/groups.json", groupPath: "/api/:version/groups/:id.json", namespacesPath: "/api/:version/namespaces.json", @@ -140,4 +141,5 @@ } }; + window.Api = Api; }).call(this); diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index b7c4673c8e3..043c6a11c4f 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -1,4 +1,11 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, no-undef, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len */ +/* global bp */ +/* global Cookies */ +/* global Flash */ +/* global ConfirmDangerModal */ +/* global AwardsHandler */ +/* global Aside */ + // This is a manifest file that'll be compiled into including all the files listed below. // Add new JavaScript code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index f4302e2e9f6..107a7978a87 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, spaced-comment, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, radix, keyword-spacing, space-before-blocks, brace-style, no-underscore-dangle, no-undef, no-plusplus, no-return-assign, camelcase, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, spaced-comment, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, radix, keyword-spacing, space-before-blocks, brace-style, no-underscore-dangle, no-plusplus, no-return-assign, camelcase, padded-blocks */ +/* global Cookies */ + (function() { this.AwardsHandler = (function() { var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js index a5d62f881fe..c62a4c5a456 100644 --- a/app/assets/javascripts/behaviors/autosize.js +++ b/app/assets/javascripts/behaviors/autosize.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, padded-blocks, max-len */ +/* global autosize */ /*= require jquery.ba-resize */ /*= require autosize */ diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index 4edcaa15fe5..586f941a6e3 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-undef, prefer-arrow-callback, camelcase, max-len, consistent-return, quotes, object-shorthand, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, camelcase, consistent-return, quotes, object-shorthand, comma-dangle, padded-blocks, max-len */ + // Quick Submit behavior // // When a child field of a form with a `js-quick-submit` class receives a diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index e0a2e8ac12e..eab686c45c3 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, camelcase, no-undef, object-shorthand, quotes, comma-dangle, prefer-arrow-callback, no-unused-vars, prefer-template, no-useless-escape, no-alert, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, camelcase, object-shorthand, quotes, comma-dangle, prefer-arrow-callback, no-unused-vars, prefer-template, no-useless-escape, no-alert, padded-blocks, max-len */ +/* global Dropzone */ + (function() { this.BlobFileDropzone = (function() { function BlobFileDropzone(form, method) { diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js index 7e8f1062ab3..15563e429a0 100644 --- a/app/assets/javascripts/blob/blob_gitignore_selector.js +++ b/app/assets/javascripts/blob/blob_gitignore_selector.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, padded-blocks */ +/* global Api */ /*= require blob/template_selector */ diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js index 9a694daa010..d7f95093688 100644 --- a/app/assets/javascripts/blob/blob_gitignore_selectors.js +++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-cond-assign, no-sequences, no-undef, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-cond-assign, no-sequences, comma-dangle, padded-blocks, max-len */ +/* global BlobGitignoreSelector */ + (function() { this.BlobGitignoreSelectors = (function() { function BlobGitignoreSelectors(opts) { diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js index 9a77fe35d55..d9c6f65a083 100644 --- a/app/assets/javascripts/blob/blob_license_selector.js +++ b/app/assets/javascripts/blob/blob_license_selector.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, comma-dangle, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, comma-dangle, padded-blocks */ +/* global Api */ /*= require blob/template_selector */ diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js index b8eb0f60a8e..8c40e36a80a 100644 --- a/app/assets/javascripts/blob_edit/blob_edit_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js @@ -1,4 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-undef, no-new, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, padded-blocks, max-len */ +/* global EditBlob */ +/* global NewCommitForm */ + /*= require_tree . */ (function() { diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index 0c74aaaa852..33a1ddcaf09 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -1,4 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, no-param-reassign, no-undef, quotes, prefer-template, no-new, comma-dangle, one-var, one-var-declaration-per-line, prefer-arrow-callback, no-else-return, no-unused-vars, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, no-param-reassign, quotes, prefer-template, no-new, comma-dangle, one-var, one-var-declaration-per-line, prefer-arrow-callback, no-else-return, no-unused-vars, padded-blocks, max-len */ +/* global ace */ +/* global BlobGitignoreSelectors */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js index e7ceb602601..a7e72430141 100644 --- a/app/assets/javascripts/breakpoints.js +++ b/app/assets/javascripts/breakpoints.js @@ -1,6 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, padded-blocks, no-return-assign, new-parens, no-param-reassign, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, padded-blocks, no-return-assign, new-parens, no-param-reassign, max-len */ + (function() { - this.Breakpoints = (function() { + var Breakpoints = (function() { var BreakpointInstance, instance; function Breakpoints() {} @@ -68,4 +69,5 @@ }; })(this)); + window.Breakpoints = Breakpoints; }).call(this); diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 116a47b0907..824febe3fd3 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -1,4 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, no-undef, quotes, yoda, no-else-return, consistent-return, comma-dangle, semi, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, semi, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top, padded-blocks */ +/* global Breakpoints */ +/* global Turbolinks */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; @@ -193,6 +196,7 @@ }; Build.prototype.stepTrace = function(e) { + var $currentTarget; e.preventDefault(); $currentTarget = $(e.currentTarget); $.scrollTo($currentTarget.attr('href'), { diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js index 67509ea7d91..67b33a4d7ee 100644 --- a/app/assets/javascripts/commit.js +++ b/app/assets/javascripts/commit.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-undef, padded-blocks */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, padded-blocks */ +/* global CommitFile */ + (function() { this.Commit = (function() { function Commit() { diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js index 600bac8834a..27512312c7c 100644 --- a/app/assets/javascripts/commit/file.js +++ b/app/assets/javascripts/commit/file.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, padded-blocks */ +/* global ImageFile */ + (function() { this.CommitFile = (function() { function CommitFile(file) { diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index 3627aaf5080..24a6e4ff0e9 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-undef, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, padded-blocks, max-len, prefer-arrow-callback */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, padded-blocks, max-len, prefer-arrow-callback */ +/* global Pager */ + (function() { this.CommitsList = (function() { function CommitsList() {} @@ -6,8 +8,8 @@ CommitsList.timer = null; CommitsList.init = function(limit) { - $("body").on("click", ".day-commits-table li.commit", function(event) { - if (event.target.nodeName !== "A") { + $("body").on("click", ".day-commits-table li.commit", function(e) { + if (e.target.nodeName !== "A") { location.href = $(this).attr("url"); e.stopPropagation(); return false; diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js index efa228a75d9..6a13f38588d 100644 --- a/app/assets/javascripts/copy_to_clipboard.js +++ b/app/assets/javascripts/copy_to_clipboard.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-undef, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, padded-blocks, max-len */ +/* global Clipboard */ /*= require clipboard */ diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 index fd1d1cad272..f47867fc3b0 100644 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 +++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 @@ -1,10 +1,10 @@ -/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, indent, space-before-function-paren, no-plusplus, no-lonely-if, no-continue, brace-style, max-len, quotes, no-undef, semi */ +/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, indent, space-before-function-paren, no-plusplus, no-lonely-if, no-continue, brace-style, max-len, quotes, semi */ /* global Vue */ /* global DiscussionMixins */ /* global CommentsStore */ (() => { - JumpToDiscussion = Vue.extend({ + const JumpToDiscussion = Vue.extend({ mixins: [DiscussionMixins], props: { discussionId: String diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index e1e76bca6ad..56cb39be642 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, no-undef, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, no-plusplus, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, no-plusplus, prefer-arrow-callback, padded-blocks */ +/* global Dropzone */ /*= require preview_markdown */ diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index 0122e847161..785f2869970 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, padded-blocks, consistent-return, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, padded-blocks, consistent-return */ +/* global FilesCommentButton */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 79501aa4ab9..245383438d1 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, padded-blocks, vars-on-top, indent, no-extra-semi, no-multi-spaces, semi, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, padded-blocks, vars-on-top, indent, no-extra-semi, no-multi-spaces, semi, max-len */ // Creates the variables for setting up GFM auto-completion (function() { @@ -10,7 +10,7 @@ return str.replace(/<(?:.|\n)*?>/gm, ''); } - GitLab.GfmAutoComplete = { + window.GitLab.GfmAutoComplete = { dataLoading: false, dataLoaded: false, cachedData: {}, @@ -57,7 +57,7 @@ var withoutAt = value.substring(1); if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"'; } - if (!GitLab.GfmAutoComplete.dataLoaded) { + if (!window.GitLab.GfmAutoComplete.dataLoaded) { return this.at; } else { return value; diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 9a91018a8e4..68a345d83f9 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -1,4 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, space-before-blocks, prefer-rest-params, max-len, vars-on-top, no-plusplus, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, semi, no-return-assign, no-else-return, camelcase, no-undef, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, padded-blocks, prefer-template, no-param-reassign, no-loop-func, no-extra-semi, keyword-spacing, no-mixed-operators, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, space-before-blocks, prefer-rest-params, max-len, vars-on-top, no-plusplus, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, semi, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, padded-blocks, prefer-template, no-param-reassign, no-loop-func, no-extra-semi, keyword-spacing, no-mixed-operators */ +/* global fuzzaldrinPlus */ +/* global Turbolinks */ + (function() { var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, @@ -188,7 +191,7 @@ })(); GitLabDropdown = (function() { - var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, currentIndex; + var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex; LOADING_CLASS = "is-loading"; diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index db5d9e75b3a..56a33eeaad5 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -1,4 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-undef, no-new, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, padded-blocks, max-len */ +/* global GitLab */ +/* global DropzoneInput */ +/* global autosize */ + (function() { this.GLForm = (function() { function GLForm(form) { diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js index c3a132b3c75..2d08a7c6ac3 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js @@ -1,4 +1,9 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, no-undef, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, padded-blocks */ +/* global ContributorsGraph */ +/* global ContributorsAuthorGraph */ +/* global ContributorsMasterGraph */ +/* global ContributorsStatGraphUtil */ +/* global d3 */ /*= require d3 */ diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js index cb2448e8cc7..9c5e9381e52 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, padded-blocks, no-undef, newline-per-chained-call, no-else-return, max-len */ +/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, padded-blocks, newline-per-chained-call, no-else-return */ +/* global d3 */ +/* global ContributorsGraph */ /*= require d3 */ diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index 3dc6f05ca20..99700e7562a 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, no-undef, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, padded-blocks, max-len */ +/* global Api */ + (function() { var slice = [].slice; @@ -14,7 +16,7 @@ multiple: $(select).hasClass('multiselect'), minimumInputLength: 0, query: function(query) { - options = { all_available: all_available, skip_groups: skip_groups }; + var options = { all_available: all_available, skip_groups: skip_groups }; return Api.groups(query.term, options, function(groups) { var data; data = { diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js index 9425b6ed9d4..fa795be07ed 100644 --- a/app/assets/javascripts/importer_status.js +++ b/app/assets/javascripts/importer_status.js @@ -1,6 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, camelcase, no-var, one-var, one-var-declaration-per-line, prefer-template, quotes, object-shorthand, comma-dangle, no-unused-vars, prefer-arrow-callback, no-else-return, padded-blocks, vars-on-top, no-new, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, camelcase, no-var, one-var, one-var-declaration-per-line, prefer-template, quotes, object-shorthand, comma-dangle, no-unused-vars, prefer-arrow-callback, no-else-return, padded-blocks, vars-on-top, no-new, max-len */ + (function() { - this.ImporterStatus = (function() { + window.ImporterStatus = (function() { function ImporterStatus(jobs_url, import_url) { this.jobs_url = jobs_url; this.import_url = import_url; @@ -75,7 +76,7 @@ var jobsImportPath = $('.js-importer-status').data('jobs-import-path'); var importPath = $('.js-importer-status').data('import-path'); - new ImporterStatus(jobsImportPath, importPath); + new window.ImporterStatus(jobsImportPath, importPath); } }); }).call(this); diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js index 317818951fd..4aaad111082 100644 --- a/app/assets/javascripts/issuable_context.js +++ b/app/assets/javascripts/issuable_context.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, no-undef, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, padded-blocks, max-len */ +/* global UsersSelect */ + (function() { this.IssuableContext = (function() { function IssuableContext(currentUser) { diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 50fdbc89c7c..2f3cad13cc0 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -1,4 +1,9 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-undef, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, radix, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, radix, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, padded-blocks, max-len */ +/* global GitLab */ +/* global UsersSelect */ +/* global ZenMode */ +/* global Autosave */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 8540b199aba..63b70d4be17 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-undef, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, padded-blocks, max-len */ +/* global Flash */ /*= require flash */ /*= require jquery.waitforimages */ diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index f334f35594d..d29a5edb9ad 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -1,4 +1,7 @@ -/* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, no-undef, semi, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread, padded-blocks, max-len */ +/* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, semi, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread, padded-blocks */ +/* global Issuable */ +/* global ListLabel */ + (function() { this.LabelsSelect = (function() { function LabelsSelect() { diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js index d0fe69260a5..3c9ad0e67c8 100644 --- a/app/assets/javascripts/lib/utils/notify.js +++ b/app/assets/javascripts/lib/utils/notify.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, no-undef, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, max-len */ + (function() { (function(w) { var notificationGranted, notifyMe, notifyPermissions; diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index b0f834705c3..9af89b79f84 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-param-reassign, no-undef, prefer-template, quotes, comma-dangle, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, spaced-comment, radix, no-else-return, max-len, no-plusplus, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-param-reassign, prefer-template, quotes, comma-dangle, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, spaced-comment, radix, no-else-return, max-len, no-plusplus, padded-blocks */ + // LineHighlighter // // Handles single- and multi-line selection and highlight for blob views. diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 88c3636be6c..70f9a8d1955 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-undef, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, padded-blocks, max-len */ +/* global MergeRequestTabs */ /*= require jquery.waitforimages */ /*= require task_list */ diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js index 15a12c3d985..9f8af46c715 100644 --- a/app/assets/javascripts/merged_buttons.js +++ b/app/assets/javascripts/merged_buttons.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, padded-blocks, max-len */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index db7561a3a75..42152362e60 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, no-undef, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, padded-blocks, max-len */ +/* global Flash */ + (function() { this.Milestone = (function() { Milestone.updateIssue = function(li, issue_url, data) { diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 67796083790..28054b78249 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -1,4 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-undef, no-param-reassign, no-shadow, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow, padded-blocks */ +/* global Vue */ +/* global Issuable */ +/* global ListMilestone */ + (function() { this.MilestoneSelect = (function() { function MilestoneSelect(currentProject) { diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js index 87c903ec576..6633f2c2709 100644 --- a/app/assets/javascripts/namespace_select.js +++ b/app/assets/javascripts/namespace_select.js @@ -1,8 +1,10 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, no-undef, prefer-arrow-callback, padded-blocks, no-param-reassign, no-cond-assign, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, padded-blocks, no-param-reassign, no-cond-assign, max-len */ +/* global Api */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - this.NamespaceSelect = (function() { + window.NamespaceSelect = (function() { function NamespaceSelect(opts) { this.onSelectItem = bind(this.onSelectItem, this); var fieldName, showAny; @@ -64,7 +66,7 @@ })(); - this.NamespaceSelects = (function() { + window.NamespaceSelects = (function() { function NamespaceSelects(opts) { var ref; if (opts == null) { @@ -74,7 +76,7 @@ this.$dropdowns.each(function(i, dropdown) { var $dropdown; $dropdown = $(dropdown); - return new NamespaceSelect({ + return new window.NamespaceSelect({ dropdown: $dropdown }); }); diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js index e3dc599b90a..64b19a54893 100644 --- a/app/assets/javascripts/network/branch_graph.js +++ b/app/assets/javascripts/network/branch_graph.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, new-cap, no-undef, no-plusplus, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, new-cap, no-plusplus, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len, padded-blocks */ +/* global Raphael */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js index 5a8f723a27b..2367d2497b2 100644 --- a/app/assets/javascripts/network/network.js +++ b/app/assets/javascripts/network/network.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, no-undef, quote-props, prefer-template, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, padded-blocks, max-len */ +/* global BranchGraph */ + (function() { this.Network = (function() { function Network(opts) { diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js index 732d92845cb..17833d3419a 100644 --- a/app/assets/javascripts/network/network_bundle.js +++ b/app/assets/javascripts/network/network_bundle.js @@ -1,4 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, no-undef, comma-dangle, consistent-return, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, padded-blocks, max-len */ +/* global Network */ +/* global ShortcutsNetwork */ + // This is a manifest file that'll be compiled into including all the files listed below. // Add new JavaScript code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 0ca0e255595..fca9b8f6c91 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1,4 +1,8 @@ -/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, no-undef, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, semi, indent, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape, radix, padded-blocks, max-len */ +/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, semi, indent, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape, radix, padded-blocks */ +/* global Flash */ +/* global GLForm */ +/* global Autosave */ +/* global ResolveService */ /*= require autosave */ /*= require autosize */ @@ -671,7 +675,7 @@ */ Notes.prototype.addDiffNote = function(e) { - var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, replyButton, row, rowCssToAdd, targetContent; + var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent; e.preventDefault(); $link = $(e.currentTarget); row = $link.closest("tr"); diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js index b152d26733f..324b68a7efc 100644 --- a/app/assets/javascripts/notifications_dropdown.js +++ b/app/assets/javascripts/notifications_dropdown.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, padded-blocks, max-len */ +/* global Flash */ + (function() { this.NotificationsDropdown = (function() { function NotificationsDropdown() { diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js index 3723aa24942..2dc8eb8b2de 100644 --- a/app/assets/javascripts/preview_markdown.js +++ b/app/assets/javascripts/preview_markdown.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, wrap-iife, no-else-return, consistent-return, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, no-undef, camelcase, prefer-arrow-callback, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, wrap-iife, no-else-return, consistent-return, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, camelcase, prefer-arrow-callback, max-len */ + // MarkdownPreview // // Handles toggling the "Write" and "Preview" tab clicks, rendering the preview, @@ -7,7 +8,7 @@ (function() { var lastTextareaPreviewed, markdownPreview, previewButtonSelector, writeButtonSelector; - this.MarkdownPreview = (function() { + window.MarkdownPreview = (function() { function MarkdownPreview() {} // Minimum number of users referenced before triggering a warning @@ -83,7 +84,7 @@ })(); - markdownPreview = new MarkdownPreview(); + markdownPreview = new window.MarkdownPreview(); previewButtonSelector = '.js-md-preview-button'; diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 016d999d77e..fcf3a4af956 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -1,4 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-undef, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, semi, vars-on-top, indent, prefer-template, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, semi, vars-on-top, indent, prefer-template, padded-blocks, max-len */ +/* global Cookies */ +/* global Turbolinks */ +/* global ProjectSelect */ + (function() { this.Project = (function() { function Project() { diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 804306a3293..1bd232314d0 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, no-undef, object-shorthand, no-param-reassign, comma-dangle, no-plusplus, prefer-template, no-unused-vars, no-return-assign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, no-plusplus, prefer-template, no-unused-vars, no-return-assign, padded-blocks */ +/* global fuzzaldrinPlus */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js index c99e55234cf..02dafcfb865 100644 --- a/app/assets/javascripts/project_import.js +++ b/app/assets/javascripts/project_import.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, padded-blocks, max-len */ +/* global Turbolinks */ + (function() { this.ProjectImport = (function() { function ProjectImport() { diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index fe1f96872f3..650996700ba 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-undef, no-else-return, quotes, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, padded-blocks, max-len */ +/* global Api */ + (function() { this.ProjectSelect = (function() { function ProjectSelect() { diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js index dbf530bed41..4548dc68fe1 100644 --- a/app/assets/javascripts/projects_list.js +++ b/app/assets/javascripts/projects_list.js @@ -1,6 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-var, one-var, one-var-declaration-per-line, no-undef, prefer-arrow-callback, consistent-return, no-unused-vars, camelcase, prefer-template, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-var, one-var, one-var-declaration-per-line, prefer-arrow-callback, consistent-return, no-unused-vars, camelcase, prefer-template, comma-dangle, padded-blocks, max-len */ + (function() { - this.ProjectsList = { + window.ProjectsList = { init: function() { $(".projects-list-filter").off('keyup'); this.initSearch(); @@ -9,7 +10,7 @@ initSearch: function() { var debounceFilter, projectsListFilter; projectsListFilter = $('.projects-list-filter'); - debounceFilter = _.debounce(ProjectsList.filterResults, 500); + debounceFilter = _.debounce(window.ProjectsList.filterResults, 500); return projectsListFilter.on('keyup', function(e) { if (projectsListFilter.val() !== '') { return debounceFilter(); diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 440b5da756d..b1e844b7302 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-unused-vars, semi, consistent-return, one-var, one-var-declaration-per-line, no-undef, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-unused-vars, semi, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, padded-blocks, max-len */ +/* global Cookies */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js index 1d208f1494c..4b6ebadeac7 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/search.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, no-undef, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, padded-blocks, max-len */ +/* global Api */ + (function() { this.Search = (function() { function Search() { diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index fa2168723be..5ea00f408f4 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -1,4 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-undef, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-plusplus, no-else-return, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-plusplus, no-else-return, comma-dangle, padded-blocks, max-len */ +/* global Mousetrap */ +/* global Turbolinks */ +/* global findFileURL */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js index 65305b8c22f..c26903038b4 100644 --- a/app/assets/javascripts/shortcuts_blob.js +++ b/app/assets/javascripts/shortcuts_blob.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, consistent-return, padded-blocks, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, consistent-return, padded-blocks */ +/* global Shortcuts */ +/* global Mousetrap */ /*= require shortcuts */ diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js index 1b9a265ba39..4549742bbcb 100644 --- a/app/assets/javascripts/shortcuts_dashboard_navigation.js +++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks */ +/* global Mousetrap */ +/* global Shortcuts */ /*= require shortcuts */ diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js index 68cd6fad04e..3a81380eef0 100644 --- a/app/assets/javascripts/shortcuts_find_file.js +++ b/app/assets/javascripts/shortcuts_find_file.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks */ +/* global Mousetrap */ +/* global ShortcutsNavigation */ /*= require shortcuts_navigation */ diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index c4899f3566a..b892fbc3393 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -1,4 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators, padded-blocks */ +/* global Mousetrap */ +/* global Turbolinks */ +/* global ShortcutsNavigation */ +/* global sidebar */ /*= require mousetrap */ /*= require shortcuts_navigation */ diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index 7d4d6364c70..0776d0a9b67 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks */ +/* global Mousetrap */ +/* global Shortcuts */ /*= require shortcuts */ diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js index a4095d2c06b..ecc3fab84c3 100644 --- a/app/assets/javascripts/shortcuts_network.js +++ b/app/assets/javascripts/shortcuts_network.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks, max-len */ +/* global Mousetrap */ +/* global ShortcutsNavigation */ /*= require shortcuts_navigation */ diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index 0d48e69cce9..ac8603ccd10 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -1,8 +1,9 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, padded-blocks, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, padded-blocks, max-len */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - this.SingleFileDiff = (function() { + window.SingleFileDiff = (function() { var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER; WRAPPER = '<div class="diff-content diff-wrap-lines"></div>'; @@ -93,7 +94,7 @@ $.fn.singleFileDiff = function(forceLoad, cb) { return this.each(function() { if (!$.data(this, 'singleFileDiff') || forceLoad) { - return $.data(this, 'singleFileDiff', new SingleFileDiff(this, forceLoad, cb)); + return $.data(this, 'singleFileDiff', new window.SingleFileDiff(this, forceLoad, cb)); } }); }; diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js index 2c8ecba7de4..18512d179b3 100644 --- a/app/assets/javascripts/snippet/snippet_bundle.js +++ b/app/assets/javascripts/snippet/snippet_bundle.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, no-undef, quotes, semi, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, semi, padded-blocks, max-len */ +/* global ace */ + /*= require_tree . */ (function() { diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js index 32803fa790b..f1fc526bf2e 100644 --- a/app/assets/javascripts/star.js +++ b/app/assets/javascripts/star.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, padded-blocks, max-len */ +/* global Flash */ + (function() { this.Star = (function() { function Star() { diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js index bd37d69165f..fabeb44f4b3 100644 --- a/app/assets/javascripts/syntax_highlight.js +++ b/app/assets/javascripts/syntax_highlight.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, padded-blocks, max-len */ + // Syntax Highlighter // // Applies a syntax highlighting color scheme CSS class to any element with the diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6 index b777d966a43..d8713600030 100644 --- a/app/assets/javascripts/todos.js.es6 +++ b/app/assets/javascripts/todos.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable padded-blocks, class-methods-use-this, no-new, func-names, prefer-template, no-unneeded-ternary, object-shorthand, space-before-function-paren, comma-dangle, quote-props, consistent-return, no-else-return, semi, no-param-reassign, max-len, no-undef */ +/* eslint-disable padded-blocks, class-methods-use-this, no-new, func-names, prefer-template, no-unneeded-ternary, object-shorthand, space-before-function-paren, comma-dangle, quote-props, consistent-return, no-else-return, semi, no-param-reassign, max-len */ /* global UsersSelect */ /* global Turbolinks */ @@ -75,7 +75,7 @@ allDoneClicked(e) { e.preventDefault(); e.stopImmediatePropagation(); - $target = $(e.currentTarget); + const $target = $(e.currentTarget); $target.disable(); return $.ajax({ type: 'POST', diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js index 5d991542b51..d2aa3c7a841 100644 --- a/app/assets/javascripts/u2f/authenticate.js +++ b/app/assets/javascripts/u2f/authenticate.js @@ -1,4 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, prefer-arrow-callback, no-undef, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, prefer-arrow-callback, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */ +/* global u2f */ +/* global U2FError */ +/* global U2FUtil */ + // Authenticate U2F (universal 2nd factor) devices for users to authenticate with. // // State Flow #1: setup -> in_progress -> authenticated -> POST to server diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js index 4c70a6e9bb6..69f98c9c0ad 100644 --- a/app/assets/javascripts/u2f/error.js +++ b/app/assets/javascripts/u2f/error.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, padded-blocks, max-len */ +/* global u2f */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js index 97d8993cac2..4f5d68f546b 100644 --- a/app/assets/javascripts/u2f/register.js +++ b/app/assets/javascripts/u2f/register.js @@ -1,4 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-undef, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */ +/* global u2f */ +/* global U2FError */ +/* global U2FUtil */ + // Register U2F (universal 2nd factor) devices for users to authenticate with. // // State Flow #1: setup -> in_progress -> registered -> POST to server diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js index ba7f533c349..578be7c3590 100644 --- a/app/assets/javascripts/users/calendar.js +++ b/app/assets/javascripts/users/calendar.js @@ -1,4 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, vars-on-top, semi, keyword-spacing, no-plusplus, no-undef, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, vars-on-top, semi, keyword-spacing, no-plusplus, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, padded-blocks, max-len */ +/* global d3 */ +/* global dateFormat */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index c6e18fad832..d4b5e03aa35 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -1,4 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, no-undef, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-plusplus, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, keyword-spacing, no-param-reassign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-plusplus, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, keyword-spacing, no-param-reassign, padded-blocks */ +/* global Vue */ +/* global Issuable */ +/* global ListUser */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, slice = [].slice; diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js index 82eb761442a..e09b59dd5aa 100644 --- a/app/assets/javascripts/zen_mode.js +++ b/app/assets/javascripts/zen_mode.js @@ -1,4 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, no-undef, camelcase, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, padded-blocks, max-len */ +/* global Dropzone */ +/* global Mousetrap */ + // Zen Mode (full screen) textarea // /*= provides zen_mode:enter */ diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 958f8413e1d..9fe1be5a597 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -167,4 +167,4 @@ new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}'); gl.Subscription.bindAll('.subscription'); new gl.DueDateSelectors(); - sidebar = new Sidebar(); + window.sidebar = new Sidebar(); diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 7b2e3db6218..89201c8cb8b 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, no-undef, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, padded-blocks, max-len */ +/* global AwardsHandler */ /*= require awards_handler */ /*= require jquery */ diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index efb1203eb2f..0f61000bc37 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, no-undef, jasmine/no-spec-dupes, new-cap, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, padded-blocks, max-len */ /*= require behaviors/quick_submit */ diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js index a406e6cc36a..bc5cbeb6a40 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js @@ -1,4 +1,8 @@ -/* eslint-disable quotes, no-undef, indent, semi, object-curly-spacing, jasmine/no-suite-dupes, vars-on-top, no-var, padded-blocks, spaced-comment, max-len */ +/* eslint-disable quotes, indent, semi, object-curly-spacing, jasmine/no-suite-dupes, vars-on-top, no-var, padded-blocks, spaced-comment, max-len */ +/* global d3 */ +/* global ContributorsGraph */ +/* global ContributorsMasterGraph */ + //= require graphs/stat_graph_contributors_graph describe("ContributorsGraph", function () { diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js index 96f39abe13e..751f3d175e2 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js @@ -1,4 +1,6 @@ -/* eslint-disable quotes, padded-blocks, no-var, camelcase, object-curly-spacing, semi, indent, object-property-newline, comma-dangle, comma-spacing, no-undef, spaced-comment, max-len, key-spacing, vars-on-top, quote-props, no-multi-spaces, max-len */ +/* eslint-disable quotes, padded-blocks, no-var, camelcase, object-curly-spacing, semi, indent, object-property-newline, comma-dangle, comma-spacing, spaced-comment, max-len, key-spacing, vars-on-top, quote-props, no-multi-spaces */ +/* global ContributorsStatGraphUtil */ + //= require graphs/stat_graph_contributors_util describe("ContributorsStatGraphUtil", function () { diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js index f78573b992b..0da124632ae 100644 --- a/spec/javascripts/graphs/stat_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_spec.js @@ -1,4 +1,6 @@ -/* eslint-disable quotes, padded-blocks, no-undef, semi */ +/* eslint-disable quotes, padded-blocks, semi */ +/* global StatGraph */ + //= require graphs/stat_graph describe("StatGraph", function () { diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 14af6644de1..faab5ae00c2 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, indent, no-undef, no-trailing-spaces, comma-dangle, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, indent, no-trailing-spaces, comma-dangle, padded-blocks, max-len */ +/* global Issue */ /*= require lib/utils/text_utility */ /*= require issue */ @@ -70,7 +71,7 @@ $('input[type=checkbox]').attr('checked', true).trigger('change'); expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); }); - + it('submits an ajax request on tasklist:changed', function() { spyOn(jQuery, 'ajax').and.callFake(function(req) { expect(req.type).toBe('PATCH'); diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6 index 9399166c80a..0c48d04776f 100644 --- a/spec/javascripts/labels_issue_sidebar_spec.js.es6 +++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable no-new, no-undef, no-plusplus, object-curly-spacing, prefer-const, semi */ +/* eslint-disable no-new, no-plusplus, object-curly-spacing, prefer-const, semi */ +/* global IssuableContext */ +/* global LabelsSelect */ //= require lib/utils/type_utility //= require jquery diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js index b8b174a2e53..decdf583410 100644 --- a/spec/javascripts/line_highlighter_spec.js +++ b/spec/javascripts/line_highlighter_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-undef, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, no-plusplus, jasmine/no-spec-dupes, no-underscore-dangle, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, no-plusplus, jasmine/no-spec-dupes, no-underscore-dangle, padded-blocks, max-len */ +/* global LineHighlighter */ /*= require line_highlighter */ diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index cbe2634d3a4..4cf1693af1b 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, no-return-assign, no-undef, padded-blocks */ +/* eslint-disable space-before-function-paren, no-return-assign, padded-blocks */ +/* global MergeRequest */ /*= require merge_request */ diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index 8828970d984..a6cb9e47744 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, no-undef, quotes, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, padded-blocks, max-len */ +/* global NewBranchForm */ /*= require jquery-ui/autocomplete */ /*= require new_branch_form */ diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 2db182d702b..d3bfa7730fa 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -1,4 +1,6 @@ -/* eslint-disable space-before-function-paren, no-unused-expressions, no-undef, no-var, object-shorthand, comma-dangle, semi, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, semi, padded-blocks, max-len */ +/* global Notes */ + /*= require notes */ /*= require autosize */ /*= require gl_form */ diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index 65de1756201..bb802a4b5e3 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -1,4 +1,6 @@ -/* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-undef, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, jasmine/no-expect-in-setup-teardown, padded-blocks, max-len */ + +/* global Project */ /*= require bootstrap */ /*= require select2 */ diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 0a9bc546144..a083dbf033a 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-undef, no-return-assign, new-cap, vars-on-top, semi, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, semi, padded-blocks, max-len */ +/* global Sidebar */ /*= require right_sidebar */ /*= require jquery */ diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index e37816b0a8c..7bc898aed5d 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, no-return-assign, no-undef, no-var, quotes, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes, padded-blocks */ +/* global ShortcutsIssuable */ /*= require shortcuts_issuable */ diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index 944df6d23f7..a8874ab12d3 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -1,4 +1,6 @@ -/* eslint-disable space-before-function-paren, new-parens, no-undef, quotes, comma-dangle, no-var, one-var, one-var-declaration-per-line, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, new-parens, quotes, comma-dangle, no-var, one-var, one-var-declaration-per-line, padded-blocks, max-len */ +/* global MockU2FDevice */ +/* global U2FAuthenticate */ /*= require u2f/authenticate */ /*= require u2f/util */ diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index 0c73c5772bd..189592ea87a 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -1,4 +1,6 @@ -/* eslint-disable space-before-function-paren, new-parens, no-undef, quotes, no-var, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, new-parens, quotes, no-var, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */ +/* global MockU2FDevice */ +/* global U2FRegister */ /*= require u2f/register */ /*= require u2f/util */ diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index b9acaaa5a0d..d80ce5a7f7e 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -1,4 +1,7 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-undef, object-shorthand, comma-dangle, no-return-assign, new-cap, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-return-assign, new-cap, padded-blocks, max-len */ +/* global Dropzone */ +/* global Mousetrap */ +/* global ZenMode */ /*= require zen_mode */ -- cgit v1.2.1 From de038bf755dcdea842264d9f90572ae92b5f845b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Wed, 14 Dec 2016 09:26:19 +0100 Subject: Fix wrong error message expectation in API::Commits spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was because the MR was a bit behind master and the error message got updated in the meantime by 79aad815. Signed-off-by: Rémy Coutable <remy@rymai.me> --- spec/requests/api/commits_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 5ce229a8cf2..964cded917c 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -475,7 +475,7 @@ describe API::Commits, api: true do expect(response).to have_http_status(400) expect(json_response['message']).to eq('Sorry, we cannot cherry-pick this commit automatically. - It may have already been cherry-pick, or a more recent commit may have updated some of its content.') + A cherry-pick may have already been performed with this commit, or a more recent commit may have updated some of its content.') end it 'returns 400 if you are not allowed to push to the target branch' do -- cgit v1.2.1 From ac115a9e1f106863112ca9daf463a8f5b8db7d0a Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Wed, 14 Dec 2016 10:21:16 +0100 Subject: Add some missing tests for detailed status methods --- spec/models/build_spec.rb | 9 +++++++++ spec/models/commit_status_spec.rb | 9 +++++++++ spec/models/generic_commit_status_spec.rb | 16 ++++++++++++++-- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index ac596ce4a91..7f39aff7639 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -1246,4 +1246,13 @@ describe Ci::Build, models: true do it { is_expected.to eq('review/master') } end end + + describe '#detailed_status' do + let(:user) { create(:user) } + + it 'returns a detailed status' do + expect(build.detailed_status(user)) + .to be_a Gitlab::Ci::Status::Build::Cancelable + end + end end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 1ec08c2a9d0..701f3323c0f 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -234,4 +234,13 @@ describe CommitStatus, models: true do end end end + + describe '#detailed_status' do + let(:user) { create(:user) } + + it 'returns a detailed status' do + expect(commit_status.detailed_status(user)) + .to be_a Gitlab::Ci::Status::Success + end + end end diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb index 615cfe3142b..6004bfdb7b7 100644 --- a/spec/models/generic_commit_status_spec.rb +++ b/spec/models/generic_commit_status_spec.rb @@ -1,8 +1,11 @@ require 'spec_helper' describe GenericCommitStatus, models: true do - let(:pipeline) { FactoryGirl.create :ci_pipeline } - let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, pipeline: pipeline } + let(:pipeline) { create(:ci_pipeline) } + + let(:generic_commit_status) do + create(:generic_commit_status, pipeline: pipeline) + end describe '#context' do subject { generic_commit_status.context } @@ -17,6 +20,15 @@ describe GenericCommitStatus, models: true do it { is_expected.to eq([:external]) } end + describe '#detailed_status' do + let(:user) { create(:user) } + + it 'returns detailed status object' do + expect(generic_commit_status.detailed_status(user)) + .to be_a Gitlab::Ci::Status::Success + end + end + describe 'set_default_values' do before do generic_commit_status.context = nil -- cgit v1.2.1 From f20ea1f5cb354db0afe18e4021e1d2fb439c2e06 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Wed, 14 Dec 2016 11:53:29 +0200 Subject: Fix BB authentication[ci skip] --- lib/omniauth/strategies/bitbucket.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/omniauth/strategies/bitbucket.rb b/lib/omniauth/strategies/bitbucket.rb index 475aad5970f..5a7d67c2390 100644 --- a/lib/omniauth/strategies/bitbucket.rb +++ b/lib/omniauth/strategies/bitbucket.rb @@ -11,10 +11,6 @@ module OmniAuth token_url: 'https://bitbucket.org/site/oauth2/access_token' } - def callback_url - full_host + script_name + callback_path - end - uid do raw_info['username'] end @@ -28,7 +24,7 @@ module OmniAuth end def raw_info - @raw_info ||= access_token.get('user').parsed + @raw_info ||= access_token.get('api/2.0/user').parsed end def primary_email @@ -37,7 +33,7 @@ module OmniAuth end def emails - email_response = access_token.get('user/emails').parsed + email_response = access_token.get('api/2.0/user/emails').parsed @emails ||= email_response && email_response['values'] || nil end end -- cgit v1.2.1 From 8f0cef0b6e5e950efdf3ebfe8f9f846095fff9d9 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Wed, 14 Dec 2016 12:35:10 +0200 Subject: BB importer: Refactoring user importing logic[ci skip] --- lib/bitbucket/representation/base.rb | 4 ---- lib/bitbucket/representation/comment.rb | 2 +- lib/bitbucket/representation/issue.rb | 2 +- lib/bitbucket/representation/pull_request.rb | 2 +- lib/bitbucket/representation/user.rb | 6 +----- lib/gitlab/bitbucket_import/importer.rb | 20 ++++++++++---------- 6 files changed, 14 insertions(+), 22 deletions(-) diff --git a/lib/bitbucket/representation/base.rb b/lib/bitbucket/representation/base.rb index fd622d333da..94adaacc9b5 100644 --- a/lib/bitbucket/representation/base.rb +++ b/lib/bitbucket/representation/base.rb @@ -5,10 +5,6 @@ module Bitbucket @raw = raw end - def user_representation(raw) - User.new(raw) - end - def self.decorate(entries) entries.map { |entry| new(entry)} end diff --git a/lib/bitbucket/representation/comment.rb b/lib/bitbucket/representation/comment.rb index bc40f891cd3..3c75e9368fa 100644 --- a/lib/bitbucket/representation/comment.rb +++ b/lib/bitbucket/representation/comment.rb @@ -2,7 +2,7 @@ module Bitbucket module Representation class Comment < Representation::Base def author - user_representation(user) + user['username'] end def note diff --git a/lib/bitbucket/representation/issue.rb b/lib/bitbucket/representation/issue.rb index 90adfa3331a..ffe8a65d839 100644 --- a/lib/bitbucket/representation/issue.rb +++ b/lib/bitbucket/representation/issue.rb @@ -12,7 +12,7 @@ module Bitbucket end def author - user_representation(raw.fetch('reporter', {})) + raw.dig('reporter', 'username') end def description diff --git a/lib/bitbucket/representation/pull_request.rb b/lib/bitbucket/representation/pull_request.rb index 96992003d24..e37c9a62c0e 100644 --- a/lib/bitbucket/representation/pull_request.rb +++ b/lib/bitbucket/representation/pull_request.rb @@ -2,7 +2,7 @@ module Bitbucket module Representation class PullRequest < Representation::Base def author - user_representation(raw.fetch('author', {})) + raw.dig('author', 'username') end def description diff --git a/lib/bitbucket/representation/user.rb b/lib/bitbucket/representation/user.rb index 6025a9f0653..ba6b7667b49 100644 --- a/lib/bitbucket/representation/user.rb +++ b/lib/bitbucket/representation/user.rb @@ -2,11 +2,7 @@ module Bitbucket module Representation class User < Representation::Base def username - raw['username'] || 'Anonymous' - end - - def uuid - raw['uuid'] + raw['username'] end end end diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 519a109c0c8..b6a0b122cdb 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -24,21 +24,21 @@ module Gitlab private - def gitlab_user_id(project, user) - if user.uuid - user = find_user_by_uuid(user.uuid) + def gitlab_user_id(project, username) + if username + user = find_user(username) (user && user.id) || project.creator_id else project.creator_id end end - def find_user_by_uuid(uuid) - User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", uuid) + def find_user(username) + User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", username) end - def existing_gitlab_user?(user) - user.uuid && find_user_by_uuid(user.uuid) + def existing_gitlab_user?(username) + username && find_user(username) end def repo @@ -52,7 +52,7 @@ module Gitlab client.issues(repo).each do |issue| description = '' - description += @formatter.author_line(issue.author.username) unless existing_gitlab_user?(issue.author) + description += @formatter.author_line(issue.author) unless existing_gitlab_user?(issue.author) description += issue.description label_name = issue.kind @@ -79,7 +79,7 @@ module Gitlab next unless comment.note.present? note = '' - note += @formatter.author_line(comment.author.username) unless existing_gitlab_user?(comment.author) + note += @formatter.author_line(comment.author) unless existing_gitlab_user?(comment.author) note += comment.note issue.notes.create!( @@ -108,7 +108,7 @@ module Gitlab pull_requests.each do |pull_request| begin description = '' - description += @formatter.author_line(pull_request.author.username) unless existing_gitlab_user?(pull_request.author) + description += @formatter.author_line(pull_request.author) unless existing_gitlab_user?(pull_request.author) description += pull_request.description merge_request = project.merge_requests.create( -- cgit v1.2.1 From 6757aaa120c375f15e09f4c428286e5dc1d95f84 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Wed, 14 Dec 2016 11:36:19 +0100 Subject: Improve build status specs contexts descriptions --- app/views/ci/status/_badge.html.haml | 2 ++ spec/lib/gitlab/ci/status/build/cancelable_spec.rb | 2 +- spec/lib/gitlab/ci/status/build/play_spec.rb | 2 +- spec/lib/gitlab/ci/status/build/retryable_spec.rb | 2 +- spec/lib/gitlab/ci/status/build/stop_spec.rb | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/views/ci/status/_badge.html.haml b/app/views/ci/status/_badge.html.haml index c00ddd183fb..f2135af2686 100644 --- a/app/views/ci/status/_badge.html.haml +++ b/app/views/ci/status/_badge.html.haml @@ -1,3 +1,5 @@ +- status = local_assigns.fetch(:status) + - if status.has_details? = link_to status.details_path, class: "ci-status ci-#{status}" do = custom_icon(status.icon) diff --git a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb index 989328c0719..9376bce17a1 100644 --- a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb +++ b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb @@ -65,7 +65,7 @@ describe Gitlab::Ci::Status::Build::Cancelable do describe '.matches?' do subject { described_class.matches?(build, user) } - context 'build is cancelable' do + context 'when build is cancelable' do let(:build) do create(:ci_build, :running) end diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb index 0fa4405c604..4ddf04a8e11 100644 --- a/spec/lib/gitlab/ci/status/build/play_spec.rb +++ b/spec/lib/gitlab/ci/status/build/play_spec.rb @@ -51,7 +51,7 @@ describe Gitlab::Ci::Status::Build::Play do describe '.matches?' do subject { described_class.matches?(build, user) } - context 'build is playable' do + context 'when build is playable' do context 'when build stops an environment' do let(:build) do create(:ci_build, :playable, :teardown_environment) diff --git a/spec/lib/gitlab/ci/status/build/retryable_spec.rb b/spec/lib/gitlab/ci/status/build/retryable_spec.rb index 59f6b78f99d..d61e5bbaa6b 100644 --- a/spec/lib/gitlab/ci/status/build/retryable_spec.rb +++ b/spec/lib/gitlab/ci/status/build/retryable_spec.rb @@ -65,7 +65,7 @@ describe Gitlab::Ci::Status::Build::Retryable do describe '.matches?' do subject { described_class.matches?(build, user) } - context 'build is retryable' do + context 'when build is retryable' do let(:build) do create(:ci_build, :success) end diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb index 984cd1e5007..59a85b55f90 100644 --- a/spec/lib/gitlab/ci/status/build/stop_spec.rb +++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb @@ -53,7 +53,7 @@ describe Gitlab::Ci::Status::Build::Stop do describe '.matches?' do subject { described_class.matches?(build, user) } - context 'build is playable' do + context 'when build is playable' do context 'when build stops an environment' do let(:build) do create(:ci_build, :playable, :teardown_environment) -- cgit v1.2.1 From 5886030f08bd41cba22509987fd5b232ec7ec965 Mon Sep 17 00:00:00 2001 From: Robert Schilling <rschilling@student.tugraz.at> Date: Tue, 13 Dec 2016 10:48:47 +0100 Subject: Update grape to 0.18.0 --- Gemfile | 2 +- Gemfile.lock | 15 +++++++++------ changelogs/unreleased/gem-update-grape.yml | 4 ++++ 3 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 changelogs/unreleased/gem-update-grape.yml diff --git a/Gemfile b/Gemfile index 2cc7764e6b8..dec0bea2cb0 100644 --- a/Gemfile +++ b/Gemfile @@ -67,7 +67,7 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false gem 'github-linguist', '~> 4.7.0', require: 'linguist' # API -gem 'grape', '~> 0.15.0' +gem 'grape', '~> 0.18.0' gem 'grape-entity', '~> 0.6.0' gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' diff --git a/Gemfile.lock b/Gemfile.lock index 3de1a7cbf26..4e5b79dd798 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -282,15 +282,15 @@ GEM json multi_json request_store (>= 1.0) - grape (0.15.0) + grape (0.18.0) activesupport builder hashie (>= 2.1.0) multi_json (>= 1.3.2) multi_xml (>= 0.5.2) + mustermann-grape (~> 0.4.0) rack (>= 1.3.0) rack-accept - rack-mount virtus (>= 1.0.0) grape-entity (0.6.0) activesupport @@ -385,6 +385,10 @@ GEM multi_json (1.12.1) multi_xml (0.5.5) multipart-post (2.0.0) + mustermann (0.4.0) + tool (~> 0.2) + mustermann-grape (0.4.0) + mustermann (= 0.4.0) mysql2 (0.3.20) net-ldap (0.12.1) net-ssh (3.0.1) @@ -489,14 +493,12 @@ GEM pry-rails (0.3.4) pry (>= 0.9.10) pyu-ruby-sasl (0.0.3.3) - rack (1.6.4) + rack (1.6.5) rack-accept (0.4.5) rack (>= 0.4) rack-attack (4.4.1) rack rack-cors (0.4.0) - rack-mount (0.8.3) - rack (>= 1.0.0) rack-oauth2 (1.2.3) activesupport (>= 2.3) attr_required (>= 0.0.5) @@ -722,6 +724,7 @@ GEM tilt (2.0.5) timecop (0.8.1) timfel-krb5-auth (0.8.3) + tool (0.2.3) truncato (0.7.8) htmlentities (~> 4.3.1) nokogiri (~> 1.6.1) @@ -840,7 +843,7 @@ DEPENDENCIES gollum-lib (~> 4.2) gollum-rugged_adapter (~> 0.4.2) gon (~> 6.1.0) - grape (~> 0.15.0) + grape (~> 0.18.0) grape-entity (~> 0.6.0) haml_lint (~> 0.18.2) hamlit (~> 2.6.1) diff --git a/changelogs/unreleased/gem-update-grape.yml b/changelogs/unreleased/gem-update-grape.yml new file mode 100644 index 00000000000..46b6702d9fd --- /dev/null +++ b/changelogs/unreleased/gem-update-grape.yml @@ -0,0 +1,4 @@ +--- +title: 'Gem update: Update grape to 0.18.0' +merge_request: +author: Robert Schilling -- cgit v1.2.1 From 6bbe2f118ee17ac8b1d43a77f4020c048c427b77 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Wed, 14 Dec 2016 15:18:30 +0200 Subject: BB importer: More advanced error handling --- lib/gitlab/bitbucket_import/importer.rb | 80 +++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 33 deletions(-) diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index b6a0b122cdb..567f2b314aa 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -6,24 +6,34 @@ module Gitlab { title: 'proposal', color: '#69D100' }, { title: 'task', color: '#7F8C8D' }].freeze - attr_reader :project, :client + attr_reader :project, :client, :errors def initialize(project) @project = project @client = Bitbucket::Client.new(project.import_data.credentials) @formatter = Gitlab::ImportFormatter.new @labels = {} + @errors = [] end def execute import_issues import_pull_requests + handle_errors true end private + def handle_errors + return unless errors.any? + project.update_column(:import_error, { + message: 'The remote data could not be fully imported.', + errors: errors + }.to_json) + end + def gitlab_user_id(project, username) if username user = find_user(username) @@ -51,21 +61,25 @@ module Gitlab create_labels client.issues(repo).each do |issue| - description = '' - description += @formatter.author_line(issue.author) unless existing_gitlab_user?(issue.author) - description += issue.description - - label_name = issue.kind - - issue = project.issues.create( - iid: issue.iid, - title: issue.title, - description: description, - state: issue.state, - author_id: gitlab_user_id(project, issue.author), - created_at: issue.created_at, - updated_at: issue.updated_at - ) + begin + description = '' + description += @formatter.author_line(issue.author) unless existing_gitlab_user?(issue.author) + description += issue.description + + label_name = issue.kind + + issue = project.issues.create!( + iid: issue.iid, + title: issue.title, + description: description, + state: issue.state, + author_id: gitlab_user_id(project, issue.author), + created_at: issue.created_at, + updated_at: issue.updated_at + ) + rescue StandardError => e + errors << { type: :issue, iid: issue.iid, errors: e.message } + end issue.labels << @labels[label_name] @@ -82,18 +96,20 @@ module Gitlab note += @formatter.author_line(comment.author) unless existing_gitlab_user?(comment.author) note += comment.note - issue.notes.create!( - project: project, - note: note, - author_id: gitlab_user_id(project, comment.author), - created_at: comment.created_at, - updated_at: comment.updated_at - ) + begin + issue.notes.create!( + project: project, + note: note, + author_id: gitlab_user_id(project, comment.author), + created_at: comment.created_at, + updated_at: comment.updated_at + ) + rescue StandardError => e + errors << { type: :issue_comment, iid: issue.iid, errors: e.message } + end end end end - rescue ActiveRecord::RecordInvalid => e - Rails.logger.error("Bitbucket importer ERROR in #{project.path_with_namespace}: Couldn't import record properly #{e.message}") end def create_labels @@ -129,8 +145,8 @@ module Gitlab ) import_pull_request_comments(pull_request, merge_request) if merge_request.persisted? - rescue ActiveRecord::RecordInvalid - Rails.logger.error("Bitbucket importer ERROR in #{project.path_with_namespace}: Invalid pull request #{e.message}") + rescue StandardError => e + errors << { type: :pull_request, iid: pull_request.iid, errors: e.message } end end end @@ -169,9 +185,8 @@ module Gitlab type: 'DiffNote') merge_request.notes.create!(attributes) - rescue ActiveRecord::RecordInvalid => e - Rails.logger.error("Bitbucket importer ERROR in #{project.path_with_namespace}: Invalid pull request comment #{e.message}") - nil + rescue StandardError => e + errors << { type: :pull_request, iid: comment.iid, errors: e.message } end end end @@ -192,9 +207,8 @@ module Gitlab pr_comments.each do |comment| begin merge_request.notes.create!(pull_request_comment_attributes(comment)) - rescue ActiveRecord::RecordInvalid => e - Rails.logger.error("Bitbucket importer ERROR in #{project.path_with_namespace}: Invalid standalone pull request comment #{e.message}") - nil + rescue StandardError => e + errors << { type: :pull_request, iid: comment.iid, errors: e.message } end end end -- cgit v1.2.1 From 5ac78a23693281ee693a383c9145b270d4602b09 Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Wed, 14 Dec 2016 08:49:32 +0100 Subject: fix transient timing failure adding timecop --- spec/serializers/analytics_build_entity_spec.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/serializers/analytics_build_entity_spec.rb b/spec/serializers/analytics_build_entity_spec.rb index 6b33fe66a63..86e703a6448 100644 --- a/spec/serializers/analytics_build_entity_spec.rb +++ b/spec/serializers/analytics_build_entity_spec.rb @@ -13,6 +13,14 @@ describe AnalyticsBuildEntity do subject { entity.as_json } + before do + Timecop.freeze + end + + after do + Timecop.return + end + it 'contains the URL' do expect(subject).to include(:url) end -- cgit v1.2.1 From 863146d42ed8fd44ab0c0f11c873fcdbbfcb28fc Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 14 Dec 2016 14:16:28 +0000 Subject: Remove unused file --- app/views/ci/status/_icon_with_description.html.haml | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 app/views/ci/status/_icon_with_description.html.haml diff --git a/app/views/ci/status/_icon_with_description.html.haml b/app/views/ci/status/_icon_with_description.html.haml deleted file mode 100644 index 34c923440d0..00000000000 --- a/app/views/ci/status/_icon_with_description.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -- detailed_status = subject.detailed_status(current_user) -- details_path = detailed_status.details_path if detailed_status.has_details? -- klass = "ci-status ci-#{detailed_status}" - -- if details_path - = link_to details_path, class: klass do - = custom_icon(detailed_status.icon) - = detailed_status.text -- else - %span{ class: klass } - = custom_icon(detailed_status.icon) - = detailed_status.text -- cgit v1.2.1 From 4ba48d6b05f66dfccc829db588312109b8b015a1 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 14 Dec 2016 14:17:02 +0000 Subject: Merge two partials into one. Rename it to match the other partials --- .../_badge_graph_icon_with_name_and_action.html.haml | 18 ++++++++++++++++++ app/views/ci/status/_icon_with_name.html.haml | 11 ----------- .../ci/status/_icon_with_name_and_action.html.haml | 8 -------- app/views/projects/stage/_graph.html.haml | 2 +- app/views/projects/stage/_in_stage_group.html.haml | 2 +- 5 files changed, 20 insertions(+), 21 deletions(-) create mode 100644 app/views/ci/status/_badge_graph_icon_with_name_and_action.html.haml delete mode 100644 app/views/ci/status/_icon_with_name.html.haml delete mode 100644 app/views/ci/status/_icon_with_name_and_action.html.haml diff --git a/app/views/ci/status/_badge_graph_icon_with_name_and_action.html.haml b/app/views/ci/status/_badge_graph_icon_with_name_and_action.html.haml new file mode 100644 index 00000000000..12a55735559 --- /dev/null +++ b/app/views/ci/status/_badge_graph_icon_with_name_and_action.html.haml @@ -0,0 +1,18 @@ +- detailed_status = subject.detailed_status(current_user) +- details_path = detailed_status.details_path if detailed_status.has_details? +- klass = "ci-status-icon ci-status-icon-#{detailed_status}" + +- if details_path + = link_to details_path, class: klass, + data: { toggle: 'tooltip', title: "#{subject.name} - #{detailed_status}" } do + %span{ class: klass }= custom_icon(detailed_status.icon) + .ci-status-text= subject.name +- else + %span{ class: klass }= custom_icon(detailed_status.icon) + .ci-status-text= subject.name + +- if detailed_status.has_action? + = link_to detailed_status.action_path, method: detailed_status.action_method, + title: "#{subject.name}: #{detailed_status.action_title}", class: 'ci-action-icon-container' do + %i.ci-action-icon-wrapper + = icon(detailed_status.action_icon, class: detailed_status.action_class) diff --git a/app/views/ci/status/_icon_with_name.html.haml b/app/views/ci/status/_icon_with_name.html.haml deleted file mode 100644 index 028e1fe9402..00000000000 --- a/app/views/ci/status/_icon_with_name.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -- detailed_status = subject.detailed_status(current_user) -- details_path = detailed_status.details_path if detailed_status.has_details? -- klass = "ci-status-icon ci-status-icon-#{detailed_status}" - -- if details_path - = link_to details_path, class: klass, data: { toggle: 'tooltip', title: "#{subject.name} - #{detailed_status}" } do - %span{ class: klass }= custom_icon(detailed_status.icon) - .ci-status-text= subject.name -- else - %span{ class: klass }= custom_icon(detailed_status.icon) - .ci-status-text= subject.name diff --git a/app/views/ci/status/_icon_with_name_and_action.html.haml b/app/views/ci/status/_icon_with_name_and_action.html.haml deleted file mode 100644 index 76db3b7f38a..00000000000 --- a/app/views/ci/status/_icon_with_name_and_action.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -= render "ci/status/icon_with_name", subject: subject - -- detailed_status = subject.detailed_status(current_user) -- if detailed_status.has_action? - = link_to detailed_status.action_path, method: detailed_status.action_method, - title: "#{subject.name}: #{detailed_status.action_title}", class: 'ci-action-icon-container' do - %i.ci-action-icon-wrapper - = icon(detailed_status.action_icon, class: detailed_status.action_class) diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml index 6d280468262..ff86d93f354 100644 --- a/app/views/projects/stage/_graph.html.haml +++ b/app/views/projects/stage/_graph.html.haml @@ -13,7 +13,7 @@ %li.build .curve .build-content - = render 'ci/status/icon_with_name_and_action', subject: status + = render 'ci/status/bagde_graph_icon_with_name_and_action', subject: status - else %li.build .curve diff --git a/app/views/projects/stage/_in_stage_group.html.haml b/app/views/projects/stage/_in_stage_group.html.haml index 5c9b6549b37..e4a7613998d 100644 --- a/app/views/projects/stage/_in_stage_group.html.haml +++ b/app/views/projects/stage/_in_stage_group.html.haml @@ -10,4 +10,4 @@ %ul - subject.each do |status| %li.dropdown-build - = render 'ci/status/icon_with_name_and_action', subject: status + = render 'ci/status/bagde_graph_icon_with_name_and_action', subject: status -- cgit v1.2.1 From 5b1a38564efdba1850d7195b01146c6b49ffb29a Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 14 Dec 2016 14:19:18 +0000 Subject: Fix scss error --- app/assets/stylesheets/pages/pipelines.scss | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 23312259f73..26487b2acf9 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -665,14 +665,15 @@ color: $gl-text-color; } - .stage { - max-width: 100px; - width: 100px; - } + .stage { + max-width: 100px; + width: 100px; + } - .ci-status-icon svg { - height: 18px; - width: 18px; + .ci-status-icon svg { + height: 18px; + width: 18px; + } } } } -- cgit v1.2.1 From 268a201cce4da7225841b651336835876be43fc2 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 14 Dec 2016 14:24:25 +0000 Subject: Rename file according to review --- .../_badge_graph_icon_with_name_and_action.html.haml | 18 ------------------ app/views/ci/status/_graph_badge.haml | 20 ++++++++++++++++++++ app/views/projects/stage/_graph.html.haml | 2 +- app/views/projects/stage/_in_stage_group.html.haml | 2 +- 4 files changed, 22 insertions(+), 20 deletions(-) delete mode 100644 app/views/ci/status/_badge_graph_icon_with_name_and_action.html.haml create mode 100644 app/views/ci/status/_graph_badge.haml diff --git a/app/views/ci/status/_badge_graph_icon_with_name_and_action.html.haml b/app/views/ci/status/_badge_graph_icon_with_name_and_action.html.haml deleted file mode 100644 index 12a55735559..00000000000 --- a/app/views/ci/status/_badge_graph_icon_with_name_and_action.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -- detailed_status = subject.detailed_status(current_user) -- details_path = detailed_status.details_path if detailed_status.has_details? -- klass = "ci-status-icon ci-status-icon-#{detailed_status}" - -- if details_path - = link_to details_path, class: klass, - data: { toggle: 'tooltip', title: "#{subject.name} - #{detailed_status}" } do - %span{ class: klass }= custom_icon(detailed_status.icon) - .ci-status-text= subject.name -- else - %span{ class: klass }= custom_icon(detailed_status.icon) - .ci-status-text= subject.name - -- if detailed_status.has_action? - = link_to detailed_status.action_path, method: detailed_status.action_method, - title: "#{subject.name}: #{detailed_status.action_title}", class: 'ci-action-icon-container' do - %i.ci-action-icon-wrapper - = icon(detailed_status.action_icon, class: detailed_status.action_class) diff --git a/app/views/ci/status/_graph_badge.haml b/app/views/ci/status/_graph_badge.haml new file mode 100644 index 00000000000..7c6d36217f0 --- /dev/null +++ b/app/views/ci/status/_graph_badge.haml @@ -0,0 +1,20 @@ +-# Renders the graph node with both the status icon, status name and action icon + +- detailed_status = subject.detailed_status(current_user) +- details_path = detailed_status.details_path if detailed_status.has_details? +- klass = "ci-status-icon ci-status-icon-#{detailed_status}" + +- if details_path + = link_to details_path, class: klass, + data: { toggle: 'tooltip', title: "#{subject.name} - #{detailed_status}" } do + %span{ class: klass }= custom_icon(detailed_status.icon) + .ci-status-text= subject.name +- else + %span{ class: klass }= custom_icon(detailed_status.icon) + .ci-status-text= subject.name + +- if detailed_status.has_action? + = link_to detailed_status.action_path, method: detailed_status.action_method, + title: "#{subject.name}: #{detailed_status.action_title}", class: 'ci-action-icon-container' do + %i.ci-action-icon-wrapper + = icon(detailed_status.action_icon, class: detailed_status.action_class) diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml index ff86d93f354..b70b574e687 100644 --- a/app/views/projects/stage/_graph.html.haml +++ b/app/views/projects/stage/_graph.html.haml @@ -13,7 +13,7 @@ %li.build .curve .build-content - = render 'ci/status/bagde_graph_icon_with_name_and_action', subject: status + = render 'ci/status/graph_badge', subject: status - else %li.build .curve diff --git a/app/views/projects/stage/_in_stage_group.html.haml b/app/views/projects/stage/_in_stage_group.html.haml index e4a7613998d..b03837d1211 100644 --- a/app/views/projects/stage/_in_stage_group.html.haml +++ b/app/views/projects/stage/_in_stage_group.html.haml @@ -10,4 +10,4 @@ %ul - subject.each do |status| %li.dropdown-build - = render 'ci/status/bagde_graph_icon_with_name_and_action', subject: status + = render 'ci/status/graph_badge', subject: status -- cgit v1.2.1 From 0c1f6cc592afbcc426d730f77e5863df6b476ab7 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra <dimitriehoekstra@gmail.com> Date: Wed, 14 Dec 2016 15:44:15 +0100 Subject: added go back anchor on error pages --- public/404.html | 9 +++++++++ public/422.html | 9 +++++++++ public/500.html | 9 +++++++++ public/502.html | 9 +++++++++ public/503.html | 9 +++++++++ 5 files changed, 45 insertions(+) diff --git a/public/404.html b/public/404.html index 11b29d09a82..b3b3a0fa3f3 100644 --- a/public/404.html +++ b/public/404.html @@ -46,6 +46,14 @@ margin: 40px auto; } + a { + line-height: 100px; + font-weight: normal; + color: #4A8BEE; + font-size: 18px; + text-decoration: none; + } + .container { margin: auto 20px; } @@ -63,6 +71,7 @@ <hr /> <p>Make sure the address is correct and that the page hasn't moved.</p> <p>Please contact your GitLab administrator if you think this is a mistake.</p> + <a href="javascript:history.back()">Go back</a> </div> </body> </html> diff --git a/public/422.html b/public/422.html index 9bd7cb4b7c8..119e54ad8bd 100644 --- a/public/422.html +++ b/public/422.html @@ -46,6 +46,14 @@ margin: 40px auto; } + a { + line-height: 100px; + font-weight: normal; + color: #4A8BEE; + font-size: 18px; + text-decoration: none; + } + .container { margin: auto 20px; } @@ -63,6 +71,7 @@ <hr /> <p>Make sure you have access to the thing you tried to change.</p> <p>Please contact your GitLab administrator if you think this is a mistake.</p> + <a href="javascript:history.back()">Go back</a> </div> </body> </html> diff --git a/public/500.html b/public/500.html index f92e8839f8d..226ef3c40ea 100644 --- a/public/500.html +++ b/public/500.html @@ -46,6 +46,14 @@ margin: 40px auto; } + a { + line-height: 100px; + font-weight: normal; + color: #4A8BEE; + font-size: 18px; + text-decoration: none; + } + .container { margin: auto 20px; } @@ -63,6 +71,7 @@ <hr /> <p>Try refreshing the page, or going back and attempting the action again.</p> <p>Please contact your GitLab administrator if this problem persists.</p> + <a href="javascript:history.back()">Go back</a> </div> </body> </html> diff --git a/public/502.html b/public/502.html index c2be4f130a9..f037b81bace 100644 --- a/public/502.html +++ b/public/502.html @@ -46,6 +46,14 @@ margin: 40px auto; } + a { + line-height: 100px; + font-weight: normal; + color: #4A8BEE; + font-size: 18px; + text-decoration: none; + } + .container { margin: auto 20px; } @@ -63,6 +71,7 @@ <hr /> <p>Try refreshing the page, or going back and attempting the action again.</p> <p>Please contact your GitLab administrator if this problem persists.</p> + <a href="javascript:history.back()">Go back</a> </div> </body> </html> diff --git a/public/503.html b/public/503.html index 8850ffce362..f946a087871 100644 --- a/public/503.html +++ b/public/503.html @@ -46,6 +46,14 @@ margin: 40px auto; } + a { + line-height: 100px; + font-weight: normal; + color: #4A8BEE; + font-size: 18px; + text-decoration: none; + } + .container { margin: auto 20px; } @@ -63,6 +71,7 @@ <hr /> <p>Try refreshing the page, or going back and attempting the action again.</p> <p>Please contact your GitLab administrator if this problem persists.</p> + <a href="javascript:history.back()">Go back</a> </div> </body> </html> -- cgit v1.2.1 From f24fed830d2079be61ff7bb8adfa7f4485dfb49a Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra <dimitriehoekstra@gmail.com> Date: Wed, 14 Dec 2016 15:47:39 +0100 Subject: added changelog entry --- .../unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml diff --git a/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml b/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml new file mode 100644 index 00000000000..c6a92547c5c --- /dev/null +++ b/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml @@ -0,0 +1,4 @@ +--- +title: Added go back anchor on error pages. +merge_request: 8087 +author: -- cgit v1.2.1 From 262fc28a7d1015b0e1349d665d5527ccee29592f Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran <alfredo@gitlab.com> Date: Tue, 29 Nov 2016 02:47:02 -0500 Subject: Improve issuable's bulk assignment implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes the case when the user wants to add a label. The user has to use the dropdown’s filter input to look for a label and click it in order to see the bug. Step to reproduce - Select at least two issues, one label should be present in all issues, other label should be present in at least one. - On the label dropdown: Deselect label that is present in all issues, look for another issue using the filter input and click it. - Click on `Update issues` Before: Unmarked label were kept on selected issues. Now: Unmarked label is removed from selected issues --- app/assets/javascripts/dispatcher.js.es6 | 4 +- app/assets/javascripts/gl_dropdown.js | 26 ++-- app/assets/javascripts/issuable.js.es6 | 4 +- .../javascripts/issues_bulk_assignment.js.es6 | 92 ++++++++------ app/assets/javascripts/labels_select.js | 138 ++++++++++++--------- .../merge_requests/_merge_request.html.haml | 2 +- .../features/issues/bulk_assignment_labels_spec.rb | 42 ++++++- 7 files changed, 185 insertions(+), 123 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 413117c2226..b2ad3d763f6 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -36,7 +36,9 @@ case 'projects:merge_requests:index': case 'projects:issues:index': Issuable.init(); - new gl.IssuableBulkActions(); + new gl.IssuableBulkActions({ + page + }); shortcut_handler = new ShortcutsNavigation(); break; case 'projects:issues:show': diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 9a91018a8e4..c35e8e93d72 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -20,7 +20,6 @@ this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true; $inputContainer = this.input.parent(); $clearButton = $inputContainer.find('.js-dropdown-input-clear'); - this.indeterminateIds = []; $clearButton.on('click', (function(_this) { // Clear click return function(e) { @@ -345,12 +344,12 @@ $el = $(this); selected = self.rowClicked($el); if (self.options.clicked) { - self.options.clicked(selected, $el, e); + self.options.clicked(selected[0], $el, e, selected[1]); } // Update label right after all modifications in dropdown has been done if (self.options.toggleLabel) { - self.updateLabel(selected, $el, self); + self.updateLabel(selected[0], $el, self); } $el.trigger('blur'); @@ -441,12 +440,6 @@ this.resetRows(); this.addArrowKeyEvent(); - if (this.options.setIndeterminateIds) { - this.options.setIndeterminateIds.call(this); - } - if (this.options.setActiveIds) { - this.options.setActiveIds.call(this); - } // Makes indeterminate items effective if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { this.parseData(this.fullData); @@ -480,11 +473,6 @@ if (this.options.filterable) { $input.blur().val(""); } - // Triggering 'keyup' will re-render the dropdown which is not always required - // specially if we want to keep the state of the dropdown needed for bulk-assignment - if (!this.options.persistWhenHide) { - $input.trigger("input"); - } if (this.dropdown.find(".dropdown-toggle-page").length) { $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS); } @@ -617,7 +605,8 @@ }; GitLabDropdown.prototype.rowClicked = function(el) { - var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value; + var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, markedIds, unmarkedIds, i, isMarking; + fieldName = this.options.fieldName; isInput = $(this.el).is('input'); if (this.renderedData) { @@ -638,7 +627,7 @@ el.addClass(ACTIVE_CLASS); } - return selectedObject; + return [selectedObject]; } field = []; @@ -656,6 +645,7 @@ } if (el.hasClass(ACTIVE_CLASS)) { + isMarking = false; el.removeClass(ACTIVE_CLASS); if (field && field.length) { if (isInput) { @@ -665,6 +655,7 @@ } } } else if (el.hasClass(INDETERMINATE_CLASS)) { + isMarking = true; el.addClass(ACTIVE_CLASS); el.removeClass(INDETERMINATE_CLASS); if (field && field.length && value == null) { @@ -674,6 +665,7 @@ this.addInput(fieldName, value, selectedObject); } } else { + isMarking = true; if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) { this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS); if (!isInput) { @@ -694,7 +686,7 @@ } } - return selectedObject; + return [selectedObject, isMarking]; }; GitLabDropdown.prototype.focusTextInput = function() { diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 index 46503c290ae..5f6c1bbdc90 100644 --- a/app/assets/javascripts/issuable.js.es6 +++ b/app/assets/javascripts/issuable.js.es6 @@ -141,6 +141,9 @@ const $issuesOtherFilters = $('.issues-other-filters'); const $issuesBulkUpdate = $('.issues_bulk_update'); + this.issuableBulkActions.willUpdateLabels = false; + this.issuableBulkActions.setOriginalDropdownData(); + if ($checkedIssues.length > 0) { let ids = $.map($checkedIssues, function(value) { return $(value).data('id'); @@ -152,7 +155,6 @@ $updateIssuesIds.val([]); $issuesBulkUpdate.hide(); $issuesOtherFilters.show(); - this.issuableBulkActions.willUpdateLabels = false; } return true; }, diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6 index 9697fb33566..8c0eaac8924 100644 --- a/app/assets/javascripts/issues_bulk_assignment.js.es6 +++ b/app/assets/javascripts/issues_bulk_assignment.js.es6 @@ -2,9 +2,10 @@ ((global) => { class IssuableBulkActions { - constructor({ container, form, issues } = {}) { - this.container = container || $('.content'), + constructor({ container, form, issues, page } = {}) { + this.prefixId = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_'; this.form = form || this.getElement('.bulk-update'); + this.$labelDropdown = this.form.find('.js-label-select'); this.issues = issues || this.getElement('.issues-list .issue'); this.form.data('bulkActions', this); this.willUpdateLabels = false; @@ -13,10 +14,6 @@ Issuable.initChecks(); } - getElement(selector) { - return this.container.find(selector); - } - bindEvents() { return this.form.off('submit').on('submit', this.onFormSubmit.bind(this)); } @@ -70,10 +67,7 @@ getUnmarkedIndeterminedLabels() { const result = []; - const labelsToKeep = []; - - this.getElement('.labels-filter .is-indeterminate') - .each((i, el) => labelsToKeep.push($(el).data('labelId'))); + const labelsToKeep = this.$labelDropdown.data('indeterminate'); this.getLabelsFromSelection().forEach((id) => { if (labelsToKeep.indexOf(id) === -1) { @@ -103,45 +97,63 @@ } }; if (this.willUpdateLabels) { - this.getLabelsToApply().map(function(id) { - return formData.update.add_label_ids.push(id); - }); - this.getLabelsToRemove().map(function(id) { - return formData.update.remove_label_ids.push(id); - }); + formData.update.add_label_ids = this.$labelDropdown.data('marked'); + formData.update.remove_label_ids = this.$labelDropdown.data('unmarked'); } return formData; } - getLabelsToApply() { - const labelIds = []; - const $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]'); - $labels.each(function(k, label) { - if (label) { - return labelIds.push(parseInt($(label).val())); - } - }); - return labelIds; + setOriginalDropdownData() { + $('.bulk-update .js-label-select').data('common', this.getOriginalCommonIds()); + $('.bulk-update .js-label-select').data('marked', this.getOriginalMarkedIds()); + $('.bulk-update .js-label-select').data('indeterminate', this.getOriginalIndeterminateIds()); } + // From issuable's initial bulk selection + getOriginalCommonIds() { + let labelIds = []; - /** - * Returns Label IDs that will be removed from issue selection - * @return {Array} Array of labels IDs - */ + this.getElement('.selected_issue:checked').each((i, el) => { + labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels')); + }); + return _.intersection.apply(this, labelIds); + } - getLabelsToRemove() { - const result = []; - const indeterminatedLabels = this.getUnmarkedIndeterminedLabels(); - const labelsToApply = this.getLabelsToApply(); - indeterminatedLabels.map(function(id) { - // We need to exclude label IDs that will be applied - // By not doing this will cause issues from selection to not add labels at all - if (labelsToApply.indexOf(id) === -1) { - return result.push(id); - } + // From issuable's initial bulk selection + getOriginalMarkedIds() { + var labelIds = []; + this.getElement('.selected_issue:checked').each((i, el) => { + labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels')); }); - return result; + return _.intersection.apply(_, labelIds); + } + + // From issuable's initial bulk selection + getOriginalIndeterminateIds() { + let uniqueIds = []; + let labelIds = []; + let issuableLabels = []; + + // Collect unique label IDs for all checked issues + this.getElement('.selected_issue:checked').each((i, el) => { + issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'); + issuableLabels.forEach((labelId) => { + // Store unique IDs + if (uniqueIds.indexOf(labelId) === -1) { + uniqueIds.push(labelId); + } + }); + // Store array of IDs per issuable + labelIds.push(issuableLabels); + }); + // Add uniqueIds to add it as argument for _.intersection + labelIds.unshift(uniqueIds); + // Return IDs that are present but not in all selected issueables + return _.difference(uniqueIds, _.intersection.apply(this, labelIds)); + } + + getElement(selector) { + return $('.content').find(selector); } } diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index f334f35594d..1d79807785f 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -5,8 +5,9 @@ var _this; _this = this; $('.js-label-select').each(function(i, dropdown) { - var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove; + var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container; $dropdown = $(dropdown); + $dropdownContainer = $dropdown.closest('.labels-filter') $toggleText = $dropdown.find('.dropdown-toggle-text'); namespacePath = $dropdown.data('namespace-path'); projectPath = $dropdown.data('project-path'); @@ -122,7 +123,7 @@ }); }); }; - return $dropdown.glDropdown({ + $dropdown.glDropdown({ showMenuAbove: showMenuAbove, data: function(term, callback) { return $.ajax({ @@ -169,33 +170,35 @@ }); }, renderRow: function(label, instance) { - var $a, $li, active, color, colorEl, indeterminate, removesAll, selectedClass, spacing; + var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked; $li = $('<li>'); $a = $('<a href="#">'); selectedClass = []; removesAll = label.id <= 0 || (label.id == null); if ($dropdown.hasClass('js-filter-bulk-update')) { - indeterminate = instance.indeterminateIds; - active = instance.activeIds; + indeterminate = $dropdown.data('indeterminate') || []; + marked = $dropdown.data('marked') || []; + if (indeterminate.indexOf(label.id) !== -1) { selectedClass.push('is-indeterminate'); } - if (active.indexOf(label.id) !== -1) { + + if (marked.indexOf(label.id) !== -1) { // Remove is-indeterminate class if the item will be marked as active i = selectedClass.indexOf('is-indeterminate'); if (i !== -1) { selectedClass.splice(i, 1); } selectedClass.push('is-active'); - // Add input manually - instance.addInput(this.fieldName, label.id); } - } - if (this.id(label) && $form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + this.id(label).toString().replace(/'/g, '\\\'') + "']").length) { - selectedClass.push('is-active'); - } - if ($dropdown.hasClass('js-multiselect') && removesAll) { - selectedClass.push('dropdown-clear-active'); + } else { + if (this.id(label) && $form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + this.id(label).toString().replace(/'/g, '\\\'') + "']").length) { + selectedClass.push('is-active'); + } + + if ($dropdown.hasClass('js-multiselect') && removesAll) { + selectedClass.push('dropdown-clear-active'); + } } if (label.duplicate) { spacing = 100 / label.color.length; @@ -231,7 +234,6 @@ // Return generated html return $li.html($a).prop('outerHTML'); }, - persistWhenHide: $dropdown.data('persistWhenHide'), search: { fields: ['title'] }, @@ -310,18 +312,15 @@ } } } - if ($dropdown.hasClass('js-filter-bulk-update')) { - // If we are persisting state we need the classes - if (!this.options.persistWhenHide) { - return $dropdown.parent().find('.is-active, .is-indeterminate').removeClass(); - } - } }, multiSelect: $dropdown.hasClass('js-multiselect'), vue: $dropdown.hasClass('js-issue-board-sidebar'), - clicked: function(label, $el, e) { + clicked: function(label, $el, e, isMarking) { var isIssueIndex, isMRIndex, page; - _this.enableBulkLabelDropdown(); + + page = $('body').data('page'); + isIssueIndex = page === 'projects:issues:index'; + isMRIndex = page === 'projects:merge_requests:index'; if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) { $dropdown.parent() @@ -330,12 +329,11 @@ } if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { + _this.enableBulkLabelDropdown(); + _this.setDropdownData($dropdown, isMarking, this.id(label)); return; } - page = $('body').data('page'); - isIssueIndex = page === 'projects:issues:index'; - isMRIndex = page === 'projects:merge_requests:index'; if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { if (label.isAny) { gl.issueBoards.BoardsStore.state.filters['label_name'] = []; @@ -397,17 +395,10 @@ } } }, - setIndeterminateIds: function() { - if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { - return this.indeterminateIds = _this.getIndeterminateIds(); - } - }, - setActiveIds: function() { - if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { - return this.activeIds = _this.getActiveIds(); - } - } }); + + // Set dropdown data + _this.setOriginalDropdownData($dropdownContainer, $dropdown); }); this.bindEvents(); } @@ -420,34 +411,9 @@ if ($('.selected_issue:checked').length) { return; } - // Remove inputs - $('.issues_bulk_update .labels-filter input[type="hidden"]').remove(); - // Also restore button text return $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label'); }; - LabelsSelect.prototype.getIndeterminateIds = function() { - var label_ids; - label_ids = []; - $('.selected_issue:checked').each(function(i, el) { - var issue_id; - issue_id = $(el).data('id'); - return label_ids.push($("#issue_" + issue_id).data('labels')); - }); - return _.flatten(label_ids); - }; - - LabelsSelect.prototype.getActiveIds = function() { - var label_ids; - label_ids = []; - $('.selected_issue:checked').each(function(i, el) { - var issue_id; - issue_id = $(el).data('id'); - return label_ids.push($("#issue_" + issue_id).data('labels')); - }); - return _.intersection.apply(_, label_ids); - }; - LabelsSelect.prototype.enableBulkLabelDropdown = function() { var issuableBulkActions; if ($('.selected_issue:checked').length) { @@ -456,8 +422,56 @@ } }; - return LabelsSelect; + LabelsSelect.prototype.setDropdownData = function($dropdown, isMarking, value) { + var issuableBulkActions = $('.bulk-update').data('bulkActions'); + markedIds = $dropdown.data('marked') || []; + unmarkedIds = $dropdown.data('unmarked') || []; + indeterminateIds = $dropdown.data('indeterminate') || []; + + if (isMarking) { + markedIds.push(value); + + i = indeterminateIds.indexOf(value); + if (i > -1) { + indeterminateIds.splice(i, 1); + } + + i = unmarkedIds.indexOf(value); + if (i > -1) { + unmarkedIds.splice(i, 1); + } + } else { + // If marked item (not common) is unmarked + i = markedIds.indexOf(value); + if (i > -1) { + markedIds.splice(i, 1); + } + + // If an indeterminate item is being unmarked + if (issuableBulkActions.getOriginalIndeterminateIds().indexOf(value) > -1) { + unmarkedIds.push(value); + } + + // If a marked item is being unmarked + // (a marked item could also be a label that is present in all selection) + if (issuableBulkActions.getOriginalCommonIds().indexOf(value) > -1) { + unmarkedIds.push(value); + } + } + + $dropdown.data('marked', markedIds); + $dropdown.data('unmarked', unmarkedIds); + $dropdown.data('indeterminate', indeterminateIds); + }; + + LabelsSelect.prototype.setOriginalDropdownData = function($container, $dropdown) { + var labels = []; + $container.find('[name="label_name[]"]').map(function() { return labels.push(this.value); }); + $dropdown.data('marked', labels); + }; + + return LabelsSelect; })(); }).call(this); diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index fa189ae62d8..53ba124b65e 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -1,4 +1,4 @@ -%li{ class: mr_css_classes(merge_request) } +%li{ id: dom_id(merge_request), class: mr_css_classes(merge_request), data: { labels: merge_request.label_ids, id: merge_request.id } } - if @bulk_edit .issue-check = check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue" diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb index bc2c087c9b9..01844f9140a 100644 --- a/spec/features/issues/bulk_assignment_labels_spec.rb +++ b/spec/features/issues/bulk_assignment_labels_spec.rb @@ -9,6 +9,7 @@ feature 'Issues > Labels bulk assignment', feature: true do let!(:issue2) { create(:issue, project: project, title: "Issue 2") } let!(:bug) { create(:label, project: project, title: 'bug') } let!(:feature) { create(:label, project: project, title: 'feature') } + let!(:wontfix) { create(:label, project: project, title: 'wontfix') } context 'as an allowed user', js: true do before do @@ -291,6 +292,45 @@ feature 'Issues > Labels bulk assignment', feature: true do expect(find("#issue_#{issue1.id}")).not_to have_content 'feature' end end + + # Special case https://gitlab.com/gitlab-org/gitlab-ce/issues/24877 + context 'unmarking common label', focus: true do + before do + issue1.labels << bug + issue1.labels << feature + issue2.labels << bug + + visit namespace_project_issues_path(project.namespace, project) + end + + it 'applies label from filtered results' do + check 'check_all_issues' + + page.within('.issues_bulk_update') do + click_button 'Labels' + wait_for_ajax + + expect(find('.dropdown-menu-labels li', text: 'bug')).to have_css('.is-active') + expect(find('.dropdown-menu-labels li', text: 'feature')).to have_css('.is-indeterminate') + + click_link 'bug' + find('.dropdown-input-field', visible: true).set('wontfix'); + click_link 'wontfix' + end + + update_issues + + page.within '.issues-holder' do + expect(find("#issue_#{issue1.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue1.id}")).to have_content 'feature' + expect(find("#issue_#{issue1.id}")).to have_content 'wontfix' + + expect(find("#issue_#{issue2.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue2.id}")).not_to have_content 'feature' + expect(find("#issue_#{issue2.id}")).to have_content 'wontfix' + end + end + end end context 'as a guest' do @@ -320,7 +360,7 @@ feature 'Issues > Labels bulk assignment', feature: true do def open_labels_dropdown(items = [], unmark = false) page.within('.issues_bulk_update') do - click_button 'Label' + click_button 'Labels' wait_for_ajax items.map do |item| click_link item -- cgit v1.2.1 From 1cea63a2c098e4fd2b1248e954c64ba1ed2f1afc Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran <alfredo@gitlab.com> Date: Tue, 6 Dec 2016 18:32:42 -0500 Subject: Add changelog file --- .../24877-bulk-edit-only-keeps-common-labels-when-searching.yml | 4 ++++ spec/features/issues/bulk_assignment_labels_spec.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml diff --git a/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml b/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml new file mode 100644 index 00000000000..cc7c2604824 --- /dev/null +++ b/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml @@ -0,0 +1,4 @@ +--- +title: Improve bulk assignment for issuables +merge_request: +author: diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb index 01844f9140a..175ae5bf93f 100644 --- a/spec/features/issues/bulk_assignment_labels_spec.rb +++ b/spec/features/issues/bulk_assignment_labels_spec.rb @@ -294,7 +294,7 @@ feature 'Issues > Labels bulk assignment', feature: true do end # Special case https://gitlab.com/gitlab-org/gitlab-ce/issues/24877 - context 'unmarking common label', focus: true do + context 'unmarking common label' do before do issue1.labels << bug issue1.labels << feature -- cgit v1.2.1 From 518dc9900c5e659f81378cec8b6f6a27702cfa38 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran <alfredo@gitlab.com> Date: Tue, 6 Dec 2016 23:32:21 -0500 Subject: Remove unwanted semicolon Repeate after me: This is not JS --- spec/features/issues/bulk_assignment_labels_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb index 175ae5bf93f..832757b24d4 100644 --- a/spec/features/issues/bulk_assignment_labels_spec.rb +++ b/spec/features/issues/bulk_assignment_labels_spec.rb @@ -314,7 +314,7 @@ feature 'Issues > Labels bulk assignment', feature: true do expect(find('.dropdown-menu-labels li', text: 'feature')).to have_css('.is-indeterminate') click_link 'bug' - find('.dropdown-input-field', visible: true).set('wontfix'); + find('.dropdown-input-field', visible: true).set('wontfix') click_link 'wontfix' end -- cgit v1.2.1 From 51b2ffaf7ecbfbc7604a38b66576af008aa8599f Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran <alfredo@gitlab.com> Date: Tue, 13 Dec 2016 16:38:21 -0500 Subject: Address feedback --- app/assets/javascripts/dispatcher.js.es6 | 6 +----- app/assets/javascripts/gl_dropdown.js | 2 +- app/assets/javascripts/issues_bulk_assignment.js.es6 | 14 ++++++++------ app/assets/javascripts/labels_select.js | 18 +++++++++++++----- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index b2ad3d763f6..359c869cb9a 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -37,7 +37,7 @@ case 'projects:issues:index': Issuable.init(); new gl.IssuableBulkActions({ - page + prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_' }); shortcut_handler = new ShortcutsNavigation(); break; @@ -108,10 +108,6 @@ new ZenMode(); new MergedButtons(); break; - case 'projects:merge_requests:index': - shortcut_handler = new ShortcutsNavigation(); - Issuable.init(); - break; case 'dashboard:activity': new gl.Activities(); break; diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index c35e8e93d72..ef5bfb709c0 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -605,7 +605,7 @@ }; GitLabDropdown.prototype.rowClicked = function(el) { - var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, markedIds, unmarkedIds, i, isMarking; + var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking; fieldName = this.options.fieldName; isInput = $(this.el).is('input'); diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6 index 8c0eaac8924..b95c0ffb5f4 100644 --- a/app/assets/javascripts/issues_bulk_assignment.js.es6 +++ b/app/assets/javascripts/issues_bulk_assignment.js.es6 @@ -2,8 +2,8 @@ ((global) => { class IssuableBulkActions { - constructor({ container, form, issues, page } = {}) { - this.prefixId = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_'; + constructor({ container, form, issues, prefixId } = {}) { + this.prefixId = prefixId || 'issue_'; this.form = form || this.getElement('.bulk-update'); this.$labelDropdown = this.form.find('.js-label-select'); this.issues = issues || this.getElement('.issues-list .issue'); @@ -104,9 +104,10 @@ } setOriginalDropdownData() { - $('.bulk-update .js-label-select').data('common', this.getOriginalCommonIds()); - $('.bulk-update .js-label-select').data('marked', this.getOriginalMarkedIds()); - $('.bulk-update .js-label-select').data('indeterminate', this.getOriginalIndeterminateIds()); + let $labelSelect = $('.bulk-update .js-label-select'); + $labelSelect.data('common', this.getOriginalCommonIds()); + $labelSelect.data('marked', this.getOriginalMarkedIds()); + $labelSelect.data('indeterminate', this.getOriginalIndeterminateIds()); } // From issuable's initial bulk selection @@ -153,7 +154,8 @@ } getElement(selector) { - return $('.content').find(selector); + this.scopeEl = this.scopeEl || $('.content'); + return this.scopeEl.find(selector); } } diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 1d79807785f..2022a2f286f 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -7,7 +7,7 @@ $('.js-label-select').each(function(i, dropdown) { var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container; $dropdown = $(dropdown); - $dropdownContainer = $dropdown.closest('.labels-filter') + $dropdownContainer = $dropdown.closest('.labels-filter'); $toggleText = $dropdown.find('.dropdown-toggle-text'); namespacePath = $dropdown.data('namespace-path'); projectPath = $dropdown.data('project-path'); @@ -170,7 +170,7 @@ }); }, renderRow: function(label, instance) { - var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked; + var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue; $li = $('<li>'); $a = $('<a href="#">'); selectedClass = []; @@ -192,8 +192,13 @@ selectedClass.push('is-active'); } } else { - if (this.id(label) && $form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + this.id(label).toString().replace(/'/g, '\\\'') + "']").length) { - selectedClass.push('is-active'); + if (this.id(label)) { + dropdownName = $dropdown.data('fieldName'); + dropdownValue = this.id(label).toString().replace(/'/g, '\\\''); + + if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) { + selectedClass.push('is-active'); + } } if ($dropdown.hasClass('js-multiselect') && removesAll) { @@ -423,6 +428,7 @@ }; LabelsSelect.prototype.setDropdownData = function($dropdown, isMarking, value) { + var i, markedIds, unmarkedIds, indeterminateIds; var issuableBulkActions = $('.bulk-update').data('bulkActions'); markedIds = $dropdown.data('marked') || []; @@ -467,7 +473,9 @@ LabelsSelect.prototype.setOriginalDropdownData = function($container, $dropdown) { var labels = []; - $container.find('[name="label_name[]"]').map(function() { return labels.push(this.value); }); + $container.find('[name="label_name[]"]').map(function() { + return labels.push(this.value); + }); $dropdown.data('marked', labels); }; -- cgit v1.2.1 From 281237d7731cfa6669dd650fdfabfe612082d47f Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 14 Dec 2016 15:04:26 +0000 Subject: Changes after review Remove empty line --- app/models/ability.rb | 6 ------ app/views/ci/status/_graph_badge.haml | 3 +-- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index ce461caf686..fa8f8bc3a5f 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -1,10 +1,4 @@ class Ability - module Allowable - def can?(user, action, subject) - Ability.allowed?(user, action, subject) - end - end - class << self # Given a list of users and a project this method returns the users that can # read the given project. diff --git a/app/views/ci/status/_graph_badge.haml b/app/views/ci/status/_graph_badge.haml index 7c6d36217f0..839b4334713 100644 --- a/app/views/ci/status/_graph_badge.haml +++ b/app/views/ci/status/_graph_badge.haml @@ -5,8 +5,7 @@ - klass = "ci-status-icon ci-status-icon-#{detailed_status}" - if details_path - = link_to details_path, class: klass, - data: { toggle: 'tooltip', title: "#{subject.name} - #{detailed_status}" } do + = link_to details_path, data: { toggle: 'tooltip', title: "#{subject.name} - #{detailed_status}" } do %span{ class: klass }= custom_icon(detailed_status.icon) .ci-status-text= subject.name - else -- cgit v1.2.1 From e138646aad4a836459999a399cdd9eca1e09847d Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 14 Dec 2016 15:25:38 +0000 Subject: Fix broken tests --- spec/features/projects/pipelines/pipeline_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 7358931b9f0..5094fcf33e8 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -40,7 +40,7 @@ describe "Pipelines", feature: true, js: true do context 'pipeline graph' do it 'shows a running icon and a cancel action for the running build' do - page.within('.stage-column:nth-child(2) .build:first-child') do + page.within('.stage-column:nth-child(3) .build:first-child') do expect(page).to have_selector('.ci-status-icon-running') expect(page).to have_content('deploy') expect(page).to have_selector('.ci-action-icon-container .fa-ban') @@ -48,7 +48,7 @@ describe "Pipelines", feature: true, js: true do end it 'shows the success icon and a retry action for the successfull build' do - page.within('.stage-column:nth-child(3)') do + page.within('.stage-column:nth-child(2) .build:first-child') do expect(page).to have_selector('.ci-status-icon-success') expect(page).to have_content('build') expect(page).to have_selector('.ci-action-icon-container .fa-refresh') @@ -64,7 +64,7 @@ describe "Pipelines", feature: true, js: true do end it 'shows the skipped icon and a play action for the manual build' do - page.within('.stage-column:nth-child(2) .build:nth-child(2)') do + page.within('.stage-column:nth-child(3) .build:nth-child(2)') do expect(page).to have_selector('.ci-status-icon-skipped') expect(page).to have_content('manual') expect(page).to have_selector('.ci-action-icon-container .ci-play-icon') -- cgit v1.2.1 From 8a6b4c1f7aadd948b5ab5c3525eca67e298c8df4 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran <alfredo@gitlab.com> Date: Tue, 13 Dec 2016 18:01:40 -0500 Subject: Remove white space between nav items --- .../javascripts/environments/components/environment.js.es6 | 3 +-- app/views/admin/logs/show.html.haml | 2 +- app/views/dashboard/_activity_head.html.haml | 4 ++-- app/views/dashboard/todos/index.html.haml | 4 ++-- app/views/projects/pipelines/index.html.haml | 8 ++++---- app/views/shared/_milestones_filter.html.haml | 6 +++--- app/views/shared/builds/_tabs.html.haml | 8 ++++---- app/views/shared/issuable/_nav.html.haml | 10 +++++----- 8 files changed, 22 insertions(+), 23 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index 1db29dd47fb..88c3d257cea 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -164,8 +164,7 @@ {{state.availableCounter}} </span> </a> - </li> - <li v-bind:class="{ 'active' : scope === 'stopped' }"> + </li><li v-bind:class="{ 'active' : scope === 'stopped' }"> <a :href="projectStoppedEnvironmentsPath"> Stopped <span class="badge js-stopped-environments-count"> diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml index 824edd171f3..0a954c20fcd 100644 --- a/app/views/admin/logs/show.html.haml +++ b/app/views/admin/logs/show.html.haml @@ -8,7 +8,7 @@ %div{ class: container_class } %ul.nav-links.log-tabs - loggers.each do |klass| - %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') } + %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }> = link_to klass::file_name, "##{klass::file_name_noext}", 'data-toggle' => 'tab' .row-content-block diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml index b78e70ebc1e..02b94beee92 100644 --- a/app/views/dashboard/_activity_head.html.haml +++ b/app/views/dashboard/_activity_head.html.haml @@ -1,7 +1,7 @@ %ul.nav-links - %li{ class: ("active" unless params[:filter]) } + %li{ class: ("active" unless params[:filter]) }> = link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do Your Projects - %li{ class: ("active" if params[:filter] == 'starred') } + %li{ class: ("active" if params[:filter] == 'starred') }> = link_to activity_dashboard_path(filter: 'starred'), data: {placement: 'right'} do Starred Projects diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 62f52086be4..ea95e91eada 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -5,14 +5,14 @@ .top-area %ul.nav-links - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending') - %li{class: "todos-pending #{todo_pending_active}"} + %li{class: "todos-pending #{todo_pending_active}"}> = link_to todos_filter_path(state: 'pending') do %span To do %span.badge = number_with_delimiter(todos_pending_count) - todo_done_active = ('active' if params[:state] == 'done') - %li{class: "todos-done #{todo_done_active}"} + %li{class: "todos-done #{todo_done_active}"}> = link_to todos_filter_path(state: 'done') do %span Done diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index e1e787dbde4..030cd8ef78f 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -5,23 +5,23 @@ %div{ class: container_class } .top-area %ul.nav-links - %li{class: ('active' if @scope.nil?)} + %li{class: ('active' if @scope.nil?)}> = link_to project_pipelines_path(@project) do All %span.badge.js-totalbuilds-count = number_with_delimiter(@pipelines_count) - %li{class: ('active' if @scope == 'running')} + %li{class: ('active' if @scope == 'running')}> = link_to project_pipelines_path(@project, scope: :running) do Running %span.badge.js-running-count = number_with_delimiter(@running_or_pending_count) - %li{class: ('active' if @scope == 'branches')} + %li{class: ('active' if @scope == 'branches')}> = link_to project_pipelines_path(@project, scope: :branches) do Branches - %li{class: ('active' if @scope == 'tags')} + %li{class: ('active' if @scope == 'tags')}> = link_to project_pipelines_path(@project, scope: :tags) do Tags diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml index 73d288e2236..186ed4a7c8b 100644 --- a/app/views/shared/_milestones_filter.html.haml +++ b/app/views/shared/_milestones_filter.html.haml @@ -2,17 +2,17 @@ - counts = milestone_counts(@project.milestones) %ul.nav-links - %li{class: milestone_class_for_state(params[:state], 'opened', true)} + %li{class: milestone_class_for_state(params[:state], 'opened', true)}> = link_to milestones_filter_path(state: 'opened') do Open - if @project %span.badge #{counts[:opened]} - %li{class: milestone_class_for_state(params[:state], 'closed')} + %li{class: milestone_class_for_state(params[:state], 'closed')}> = link_to milestones_filter_path(state: 'closed') do Closed - if @project %span.badge #{counts[:closed]} - %li{class: milestone_class_for_state(params[:state], 'all')} + %li{class: milestone_class_for_state(params[:state], 'all')}> = link_to milestones_filter_path(state: 'all') do All - if @project diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml index 60353aee7f1..b6047ece592 100644 --- a/app/views/shared/builds/_tabs.html.haml +++ b/app/views/shared/builds/_tabs.html.haml @@ -1,23 +1,23 @@ %ul.nav-links - %li{ class: ('active' if scope.nil?) } + %li{ class: ('active' if scope.nil?) }> = link_to build_path_proc.call(nil) do All %span.badge.js-totalbuilds-count = number_with_delimiter(all_builds.count(:id)) - %li{ class: ('active' if scope == 'pending') } + %li{ class: ('active' if scope == 'pending') }> = link_to build_path_proc.call('pending') do Pending %span.badge = number_with_delimiter(all_builds.pending.count(:id)) - %li{ class: ('active' if scope == 'running') } + %li{ class: ('active' if scope == 'running') }> = link_to build_path_proc.call('running') do Running %span.badge = number_with_delimiter(all_builds.running.count(:id)) - %li{ class: ('active' if scope == 'finished') } + %li{ class: ('active' if scope == 'finished') }> = link_to build_path_proc.call('finished') do Finished %span.badge diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index 0af92b59584..d938edf4dbd 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -3,23 +3,23 @@ - issuables = @issues || @merge_requests %ul.nav-links.issues-state-filters - %li{class: ("active" if params[:state] == 'opened')} + %li{class: ("active" if params[:state] == 'opened')}> = link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened." do #{issuables_state_counter_text(type, :opened)} - if type == :merge_requests - %li{class: ("active" if params[:state] == 'merged')} + %li{class: ("active" if params[:state] == 'merged')}> = link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.' do #{issuables_state_counter_text(type, :merged)} - %li{class: ("active" if params[:state] == 'closed')} + %li{class: ("active" if params[:state] == 'closed')}> = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.' do #{issuables_state_counter_text(type, :closed)} - else - %li{class: ("active" if params[:state] == 'closed')} + %li{class: ("active" if params[:state] == 'closed')}> = link_to page_filter_path(state: 'closed', label: true), id: 'state-all', title: 'Filter by issues that are currently closed.' do #{issuables_state_counter_text(type, :closed)} - %li{class: ("active" if params[:state] == 'all')} + %li{class: ("active" if params[:state] == 'all')}> = link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}." do #{issuables_state_counter_text(type, :all)} -- cgit v1.2.1 From 7cced60069c248156decf6ceabc4d1f447e47ff7 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Wed, 14 Dec 2016 21:00:06 +0800 Subject: Introduce latest_status and add a few tests Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7333#note_20003268 --- app/models/ci/pipeline.rb | 4 +++ app/models/commit.rb | 4 +-- app/services/ci/image_for_build_service.rb | 2 +- spec/models/ci/pipeline_spec.rb | 57 ++++++++++++++++++++++++++++++ spec/models/commit_spec.rb | 4 +-- 5 files changed, 66 insertions(+), 5 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index aa0367227a8..9edfc75eac7 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -98,6 +98,10 @@ module Ci end.where(id: max_id.group(:ref, :sha)) end + def self.latest_status(ref = nil) + latest(ref).status + end + def self.latest_successful_for(ref) success.latest(ref).first end diff --git a/app/models/commit.rb b/app/models/commit.rb index 91c7970fca6..69cfc47f5bf 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -230,7 +230,7 @@ class Commit return @statuses[ref] if @statuses.key?(ref) - @statuses[ref] = pipelines.latest(ref).status + @statuses[ref] = pipelines.latest_status(ref) end def revert_branch_name @@ -266,7 +266,7 @@ class Commit @merged_merge_request_hash ||= Hash.new do |hash, user| hash[user] = merged_merge_request_no_cache(user) end - + @merged_merge_request_hash[current_user] end diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb index 1eeb0e2363a..240ddabec36 100644 --- a/app/services/ci/image_for_build_service.rb +++ b/app/services/ci/image_for_build_service.rb @@ -5,7 +5,7 @@ module Ci sha = opts[:sha] || ref_sha(project, ref) pipelines = project.pipelines.where(sha: sha) - image_name = image_for_status(pipelines.latest(ref).status) + image_name = image_for_status(pipelines.latest_status(ref)) image_path = Rails.root.join('public/ci', image_name) OpenStruct.new(path: image_path, name: image_name) diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index e78ae14b737..21df5df1b76 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -381,6 +381,63 @@ describe Ci::Pipeline, models: true do end end + shared_context 'with some empty pipelines' do + before do + create_pipeline(:canceled, 'ref', 'A') + create_pipeline(:success, 'ref', 'A') + create_pipeline(:failed, 'ref', 'B') + create_pipeline(:skipped, 'feature', 'C') + end + + def create_pipeline(status, ref, sha) + create(:ci_empty_pipeline, status: status, ref: ref, sha: sha) + end + end + + describe '.latest' do + include_context 'with some empty pipelines' + + context 'when no ref is specified' do + let(:pipelines) { Ci::Pipeline.latest.all } + + it 'returns the latest pipeline for the same ref and different sha' do + expect(pipelines.map(&:sha)).to contain_exactly('A', 'B', 'C') + expect(pipelines.map(&:status)). + to contain_exactly('success', 'failed', 'skipped') + end + end + + context 'when ref is specified' do + let(:pipelines) { Ci::Pipeline.latest('ref').all } + + it 'returns the latest pipeline for ref and different sha' do + expect(pipelines.map(&:sha)).to contain_exactly('A', 'B') + expect(pipelines.map(&:status)). + to contain_exactly('success', 'failed') + end + end + end + + describe '.latest_status' do + include_context 'with some empty pipelines' + + context 'when no ref is specified' do + let(:latest_status) { Ci::Pipeline.latest_status } + + it 'returns the latest status for the same ref and different sha' do + expect(latest_status).to eq(Ci::Pipeline.latest.status) + end + end + + context 'when ref is specified' do + let(:latest_status) { Ci::Pipeline.latest_status('ref') } + + it 'returns the latest status for ref and different sha' do + expect(latest_status).to eq(Ci::Pipeline.latest_status('ref')) + end + end + end + describe '#status' do let!(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') } diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index b81fab0372a..a2a8392699e 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -224,7 +224,7 @@ eos end it 'gives compound status from latest pipelines' do - expect(commit.status).to eq(Ci::Pipeline.latest.status) + expect(commit.status).to eq(Ci::Pipeline.latest_status) end end @@ -251,7 +251,7 @@ eos end it 'gives compound status from latest pipelines if ref is nil' do - expect(commit.status(nil)).to eq(Ci::Pipeline.latest.status) + expect(commit.status(nil)).to eq(Ci::Pipeline.latest_status) end end end -- cgit v1.2.1 From 7f9d1ad378e9d00dce8882231e8476faf7067f61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20Vargas=20L=C3=B3pez?= <jvargas@gitlab.com> Date: Wed, 14 Dec 2016 09:44:04 -0600 Subject: Fixed the use of display:flex to display:block --- app/assets/stylesheets/pages/editor.scss | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 935f157b33d..fea9a6b5009 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -132,26 +132,25 @@ } .new-file-name { - margin-bottom: 0; max-width: none; width: 100%; } .file-buttons { - display: flex; - flex-direction: column; + display: block; width: 100%; - margin-bottom: 7px; .soft-wrap-toggle { - margin: 7px 0 0; + width: 100%; + margin: 7px 0; } .encoding-selector, .license-selector, .gitignore-selector, .gitlab-ci-yml-selector { - margin: 7px 0 0; + display: block; + margin: 7px 0; button { width: 100%; -- cgit v1.2.1 From 53fb5a7f24149740e5c579a9496164c29b9d919b Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 14 Dec 2016 10:08:12 -0600 Subject: add node_modules to our eslintignore settings --- .eslintignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintignore b/.eslintignore index 93de4b10dfe..b4bfa5a1f7a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,6 @@ /coverage/ /coverage-javascript/ +/node_modules/ /public/ /tmp/ /vendor/ -- cgit v1.2.1 From f796840fd33d7df2c1de8f965596b5486b1fbf24 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran <alfredo@gitlab.com> Date: Wed, 14 Dec 2016 11:40:45 -0500 Subject: Add Object.assign polyfill --- app/assets/javascripts/extensions/object.js.es6 | 27 +++++++++++++++++++++++++ spec/javascripts/extensions/object_spec.js.es6 | 25 +++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 app/assets/javascripts/extensions/object.js.es6 create mode 100644 spec/javascripts/extensions/object_spec.js.es6 diff --git a/app/assets/javascripts/extensions/object.js.es6 b/app/assets/javascripts/extensions/object.js.es6 new file mode 100644 index 00000000000..f8cdedd507b --- /dev/null +++ b/app/assets/javascripts/extensions/object.js.es6 @@ -0,0 +1,27 @@ +/* eslint-disable */ + +// Taken from https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill +if (typeof Object.assign != 'function') { + Object.assign = function (target, varArgs) { // .length of function is 2 + 'use strict'; + if (target == null) { // TypeError if undefined or null + throw new TypeError('Cannot convert undefined or null to object'); + } + + var to = Object(target); + + for (var index = 1; index < arguments.length; index++) { + var nextSource = arguments[index]; + + if (nextSource != null) { // Skip over if undefined or null + for (var nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; + }; +} diff --git a/spec/javascripts/extensions/object_spec.js.es6 b/spec/javascripts/extensions/object_spec.js.es6 new file mode 100644 index 00000000000..3b71c255b30 --- /dev/null +++ b/spec/javascripts/extensions/object_spec.js.es6 @@ -0,0 +1,25 @@ +/*= require extensions/object */ + +describe('Object extensions', () => { + describe('assign', () => { + it('merges source object into target object', () => { + const targetObj = {}; + const sourceObj = { + foo: 'bar', + }; + Object.assign(targetObj, sourceObj); + expect(targetObj.foo).toBe('bar'); + }); + + it('merges object with the same properties', () => { + const targetObj = { + foo: 'bar', + }; + const sourceObj = { + foo: 'baz', + }; + Object.assign(targetObj, sourceObj); + expect(targetObj.foo).toBe('baz'); + }); + }); +}); -- cgit v1.2.1 From 2d170a20dc4cd3423ac7994c797cae8fbed263ba Mon Sep 17 00:00:00 2001 From: Munken <mm.munk@gmail.com> Date: Fri, 9 Dec 2016 00:15:08 +0000 Subject: Render math in Asciidoc and Markdown with KaTeX using code blocks --- app/assets/javascripts/blob_edit/edit_blob.js | 2 +- app/assets/javascripts/notes.js | 6 +- app/assets/javascripts/preview_markdown.js | 2 +- app/assets/javascripts/render_gfm.js | 16 +++ app/assets/javascripts/render_math.js | 55 ++++++++++ app/assets/javascripts/syntax_highlight.js | 28 +++++ app/assets/javascripts/syntax_highlight.js.erb | 79 -------------- changelogs/unreleased/8003-katex-math.yml | 4 + config/application.rb | 2 + config/initializers/math_lexer.rb | 2 + doc/user/markdown.md | 37 +++++++ lib/banzai/filter/inline_math_filter.rb | 27 ----- lib/banzai/filter/math_filter.rb | 51 +++++++++ lib/banzai/filter/syntax_highlight_filter.rb | 3 - lib/banzai/pipeline/gfm_pipeline.rb | 2 +- lib/gitlab/asciidoc.rb | 31 +++++- lib/gitlab/gon_helper.rb | 2 + lib/rouge/lexers/math.rb | 6 +- spec/lib/banzai/filter/inline_math_filter_spec.rb | 45 -------- spec/lib/banzai/filter/math_filter_spec.rb | 120 ++++++++++++++++++++++ spec/lib/gitlab/asciidoc_spec.rb | 4 +- 21 files changed, 358 insertions(+), 166 deletions(-) create mode 100644 app/assets/javascripts/render_gfm.js create mode 100644 app/assets/javascripts/render_math.js create mode 100644 app/assets/javascripts/syntax_highlight.js delete mode 100644 app/assets/javascripts/syntax_highlight.js.erb create mode 100644 changelogs/unreleased/8003-katex-math.yml create mode 100644 config/initializers/math_lexer.rb delete mode 100644 lib/banzai/filter/inline_math_filter.rb create mode 100644 lib/banzai/filter/math_filter.rb delete mode 100644 spec/lib/banzai/filter/inline_math_filter_spec.rb create mode 100644 spec/lib/banzai/filter/math_filter_spec.rb diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index 0c74aaaa852..51e2094d26a 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -57,7 +57,7 @@ content: this.editor.getValue() }, function(response) { currentPane.empty().append(response); - return currentPane.syntaxHighlight(); + return currentPane.renderGFM(); }); } else { this.$toggleButton.show(); diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 0ca0e255595..327dbd2878c 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -305,7 +305,7 @@ } row = form.closest("tr"); note_html = $(note.html); - note_html.syntaxHighlight(); + note_html.renderGFM(); // is this the first note of discussion? discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']"); if ((note.original_discussion_id != null) && discussionContainer.length === 0) { @@ -322,7 +322,7 @@ discussionContainer.append(note_html); // Init discussion on 'Discussion' page if it is merge request page if ($('body').attr('data-page').indexOf('projects:merge_request') === 0) { - $('ul.main-notes-list').append(note.discussion_html).syntaxHighlight(); + $('ul.main-notes-list').append(note.discussion_html).renderGFM(); } } else { // append new note to all matching discussions @@ -463,7 +463,7 @@ // Convert returned HTML to a jQuery object so we can modify it further $html = $(note.html); gl.utils.localTimeAgo($('.js-timeago', $html)); - $html.syntaxHighlight(); + $html.renderGFM(); $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-row-' + note.id); diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js index 3723aa24942..b2574e8c0cd 100644 --- a/app/assets/javascripts/preview_markdown.js +++ b/app/assets/javascripts/preview_markdown.js @@ -27,7 +27,7 @@ return this.renderMarkdown(mdText, (function(_this) { return function(response) { preview.html(response.body); - preview.syntaxHighlight(); + preview.renderGFM(); return _this.renderReferencedUsers(response.references.users, form); }; })(this)); diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js new file mode 100644 index 00000000000..bbb2f186655 --- /dev/null +++ b/app/assets/javascripts/render_gfm.js @@ -0,0 +1,16 @@ +/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */ +// Render Gitlab flavoured Markdown +// +// Delegates to syntax highlight and render math +// +(function() { + $.fn.renderGFM = function() { + this.find('.js-syntax-highlight').syntaxHighlight(); + this.find('.js-render-math').renderMath(); + }; + + $(document).on('ready page:load', function() { + return $('body').renderGFM(); + }); + +}).call(this); diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js new file mode 100644 index 00000000000..a8a56430f88 --- /dev/null +++ b/app/assets/javascripts/render_math.js @@ -0,0 +1,55 @@ +/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */ +// Renders math using KaTeX in any element with the +// `js-render-math` class +// +// ### Example Markup +// +// <code class="js-render-math"></div> +// +(function() { + // Only load once + var katexLoaded = false; + + // Loop over all math elements and render math + var renderWithKaTeX = function (elements) { + elements.each(function () { + var mathNode = $('<span></span>'); + var $this = $(this); + + var display = $this.attr('data-math-style') === 'display'; + try { + katex.render($this.text(), mathNode.get(0), { displayMode: display }); + mathNode.insertAfter($this); + $this.remove(); + } catch (err) { + // What can we do?? + console.log(err.message); + } + }); + }; + + $.fn.renderMath = function() { + var $this = this; + if ($this.length === 0) return; + + if (katexLoaded) renderWithKaTeX($this); + else { + // Request CSS file so it is in the cache + $.get(gon.katex_css_url, function() { + var css = $('<link>', + { rel: 'stylesheet', + type: 'text/css', + href: gon.katex_css_url, + }); + css.appendTo('head'); + + // Load KaTeX js + $.getScript(gon.katex_js_url, function() { + katexLoaded = true; + renderWithKaTeX($this); // Run KaTeX + }); + }); + } + }; + +}).call(this); diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js new file mode 100644 index 00000000000..f4e9d5af74f --- /dev/null +++ b/app/assets/javascripts/syntax_highlight.js @@ -0,0 +1,28 @@ +/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */ +// Syntax Highlighter +// +// Applies a syntax highlighting color scheme CSS class to any element with the +// `js-syntax-highlight` class +// +// ### Example Markup +// +// <div class="js-syntax-highlight"></div> +// +(function() { + + $.fn.syntaxHighlight = function() { + var $children; + + if ($(this).hasClass('js-syntax-highlight')) { + // Given the element itself, apply highlighting + return $(this).addClass(gon.user_color_scheme); + } else { + // Given a parent element, recurse to any of its applicable children + $children = $(this).find('.js-syntax-highlight'); + if ($children.length) { + return $children.syntaxHighlight(); + } + } + }; + +}).call(this); diff --git a/app/assets/javascripts/syntax_highlight.js.erb b/app/assets/javascripts/syntax_highlight.js.erb deleted file mode 100644 index ea79e82d887..00000000000 --- a/app/assets/javascripts/syntax_highlight.js.erb +++ /dev/null @@ -1,79 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */ -// Syntax Highlighter -// -// Applies a syntax highlighting color scheme CSS class to any element with the -// `js-syntax-highlight` class -// -// ### Example Markup -// -// <div class="js-syntax-highlight"></div> -// -(function() { - // CSS and JS for KaTeX - CSS_PATH = "<%= asset_path('katex.css') %>"; - JS_PATH = "<%= asset_path('katex.js') %>"; - - // Only load once - var katexLoaded = false; - - // Loop over all math elements and render math - var renderWithKaTeX = function (elements) { - elements.each(function () { - if (!!$(this).attr('rendered')) return; - - $(this).attr('rendered', true); - $(this).hide(); - var mathNode = $( "<math>Test</math>" ); - mathNode.insertAfter($(this)); - - var display = $(this).hasClass('highlight'); - katex.render($(this).text(), mathNode.get(0), { displayMode: display }) - }) - }; - var handleMath = function () { - var mathElements = $('.code.math'); - - if (mathElements.length == 0) return; - - if (katexLoaded) renderWithKaTeX(mathElements); - else { - // Request CSS file so it is in the cache - $.get(CSS_PATH, function(){ - var css = $('<link>', - {rel:'stylesheet', - type:'text/css', - href: CSS_PATH - }); - css.appendTo('head'); - - // Load KaTeX js - $.getScript(JS_PATH, function() { - katexLoaded = true; - renderWithKaTeX(mathElements); // Run KaTeX - }) - }); - } - }; - - $.fn.syntaxHighlight = function() { - var $children; - - handleMath(); - - if ($(this).hasClass('js-syntax-highlight')) { - // Given the element itself, apply highlighting - return $(this).addClass(gon.user_color_scheme); - } else { - // Given a parent element, recurse to any of its applicable children - $children = $(this).find('.js-syntax-highlight'); - if ($children.length) { - return $children.syntaxHighlight(); - } - } - }; - - $(document).on('ready page:load', function() { - return $('.js-syntax-highlight').syntaxHighlight(); - }); - -}).call(this); diff --git a/changelogs/unreleased/8003-katex-math.yml b/changelogs/unreleased/8003-katex-math.yml new file mode 100644 index 00000000000..a40dcde1393 --- /dev/null +++ b/changelogs/unreleased/8003-katex-math.yml @@ -0,0 +1,4 @@ +--- +title: Added support for math rendering, using KaTeX, in Markdown and asciidoc +merge_request: 8003 +author: Munken diff --git a/config/application.rb b/config/application.rb index fb84870dfbd..37e15f862c7 100644 --- a/config/application.rb +++ b/config/application.rb @@ -84,6 +84,8 @@ module Gitlab config.assets.precompile << "print.css" config.assets.precompile << "notify.css" config.assets.precompile << "mailers/*.css" + config.assets.precompile << "katex.css" + config.assets.precompile << "katex.js" config.assets.precompile << "graphs/graphs_bundle.js" config.assets.precompile << "users/users_bundle.js" config.assets.precompile << "network/network_bundle.js" diff --git a/config/initializers/math_lexer.rb b/config/initializers/math_lexer.rb new file mode 100644 index 00000000000..8a3388a267e --- /dev/null +++ b/config/initializers/math_lexer.rb @@ -0,0 +1,2 @@ +# Touch the lexers so it is registered with Rouge +Rouge::Lexers::Math diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 4d24eb21976..f6484688721 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -319,6 +319,40 @@ Here's a sample video: ![Sample Video](img/markdown_video.mp4) +### Math + +> If this is not rendered correctly, see +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#math + +It is possible to have math written with the LaTeX syntax rendered using [KaTeX][katex]. + +Math written inside ```$``$``` will be rendered inline with the text. + +Math written inside triple back quotes, with the language declared as `math`, will be rendered on a separate line. + +Example: + + This math is inline $`a^2+b^2=c^2`$. + + This is on a separate line + ```math + a^2+b^2=c^2 + ``` + +Becomes: + +This math is inline $`a^2+b^2=c^2`$. + +This is on a separate line +```math +a^2+b^2=c^2 +``` + +_Be advised that KaTeX only supports a [subset][katex-subset] of LaTeX._ + +>**Note:** +This also works for the asciidoctor `:stem: latexmath`. For details see the [asciidoctor user manual][asciidoctor-manual]. + ## Standard Markdown ### Headers @@ -764,3 +798,6 @@ A link starting with a `/` is relative to the wiki root. [markdown.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md [rouge]: http://rouge.jneen.net/ "Rouge website" [redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website" +[katex]: https://github.com/Khan/KaTeX "KaTeX website" +[katex-subset]: https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX "Macros supported by KaTeX" +[asciidoctor-manual]: http://asciidoctor.org/docs/user-manual/#activating-stem-support "Asciidoctor user manual" \ No newline at end of file diff --git a/lib/banzai/filter/inline_math_filter.rb b/lib/banzai/filter/inline_math_filter.rb deleted file mode 100644 index 1bbe602237a..00000000000 --- a/lib/banzai/filter/inline_math_filter.rb +++ /dev/null @@ -1,27 +0,0 @@ -require 'uri' - -module Banzai - module Filter - # HTML filter that adds class="code math" and removes the dolar sign in $`2+2`$. - # - class InlineMathFilter < HTML::Pipeline::Filter - def call - doc.xpath("descendant-or-self::text()[substring(., string-length(.)) = '$']"\ - "/following-sibling::*[name() = 'code']"\ - "/following-sibling::text()[starts-with(.,'$')]").each do |el| - closing = el - code = el.previous - code[:class] = 'code math' - opening = code.previous - - closing.content = closing.content[1..-1] - opening.content = opening.content[0..-2] - - closing - end - - doc - end - end - end -end diff --git a/lib/banzai/filter/math_filter.rb b/lib/banzai/filter/math_filter.rb new file mode 100644 index 00000000000..cb037f89337 --- /dev/null +++ b/lib/banzai/filter/math_filter.rb @@ -0,0 +1,51 @@ +require 'uri' + +module Banzai + module Filter + # HTML filter that adds class="code math" and removes the dollar sign in $`2+2`$. + # + class MathFilter < HTML::Pipeline::Filter + # This picks out <code>...</code>. + INLINE_MATH = 'descendant-or-self::code'.freeze + + # Pick out a code block which is declared math + DISPLAY_MATH = "descendant-or-self::pre[contains(@class, 'math') and contains(@class, 'code')]".freeze + + # Attribute indicating inline or display math. + STYLE_ATTRIBUTE = 'data-math-style'.freeze + + # Class used for tagging elements that should be rendered + TAG_CLASS = 'js-render-math'.freeze + + INLINE_CLASSES = "code math #{TAG_CLASS}".freeze + + DOLLAR_SIGN = '$'.freeze + + def call + doc.xpath(INLINE_MATH).each do |code| + closing = code.next + opening = code.previous + + # We need a sibling before and after. + # They should end and start with $ respectively. + if closing && opening && + closing.content.first == DOLLAR_SIGN && + opening.content.last == DOLLAR_SIGN + + code[:class] = INLINE_CLASSES + code[STYLE_ATTRIBUTE] = 'inline' + closing.content = closing.content[1..-1] + opening.content = opening.content[0..-2] + end + end + + doc.xpath(DISPLAY_MATH).each do |el| + el[STYLE_ATTRIBUTE] = 'display' + el[:class] += " #{TAG_CLASS}" + end + + doc + end + end + end +end diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb index e7f6b715ba8..026b81ac175 100644 --- a/lib/banzai/filter/syntax_highlight_filter.rb +++ b/lib/banzai/filter/syntax_highlight_filter.rb @@ -48,9 +48,6 @@ module Banzai end def lexer_for(language) - if language == 'math' - return Rouge::Lexers::Math.new - end (Rouge::Lexer.find(language) || Rouge::Lexers::PlainText).new end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 2c81cbe56b3..5a1f873496c 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -6,7 +6,7 @@ module Banzai Filter::SyntaxHighlightFilter, Filter::SanitizationFilter, - Filter::InlineMathFilter, + Filter::MathFilter, Filter::UploadLinkFilter, Filter::VideoLinkFilter, Filter::ImageLinkFilter, diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb index 9667df4ffb8..f77f412da56 100644 --- a/lib/gitlab/asciidoc.rb +++ b/lib/gitlab/asciidoc.rb @@ -1,4 +1,5 @@ require 'asciidoctor' +require 'asciidoctor/converter/html5' module Gitlab # Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters @@ -23,7 +24,7 @@ module Gitlab def self.render(input, context, asciidoc_opts = {}) asciidoc_opts.reverse_merge!( safe: :secure, - backend: :html5, + backend: :gitlab_html5, attributes: [] ) asciidoc_opts[:attributes].unshift(*DEFAULT_ADOC_ATTRS) @@ -36,3 +37,31 @@ module Gitlab end end end + +module Gitlab + module Asciidoc + class Html5Converter < Asciidoctor::Converter::Html5Converter + extend Asciidoctor::Converter::Config + + register_for 'gitlab_html5' + + def stem(node) + return super unless node.style.to_sym == :latexmath + + %(<pre#{id_attribute(node)} class="code math js-render-math #{node.role}" data-math-style="display"><code>#{node.content}</code></pre>) + end + + def inline_quoted(node) + return super unless node.type.to_sym == :latexmath + + %(<code#{id_attribute(node)} class="code math js-render-math #{node.role}" data-math-style="inline">#{node.text}</code>) + end + + private + + def id_attribute(node) + node.id ? %( id="#{node.id}") : nil + end + end + end +end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 2c21804fe7a..4d4e04e9e35 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -8,6 +8,8 @@ module Gitlab gon.shortcuts_path = help_page_path('shortcuts') gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class gon.award_menu_url = emojis_path + gon.katex_css_url = ActionController::Base.helpers.asset_path('katex.css') + gon.katex_js_url = ActionController::Base.helpers.asset_path('katex.js') if current_user gon.current_user_id = current_user.id diff --git a/lib/rouge/lexers/math.rb b/lib/rouge/lexers/math.rb index ae980da8283..80784adfd76 100644 --- a/lib/rouge/lexers/math.rb +++ b/lib/rouge/lexers/math.rb @@ -1,13 +1,13 @@ module Rouge module Lexers class Math < Lexer - title "Plain Text" + title "A passthrough lexer used for LaTeX input" desc "A boring lexer that doesn't highlight anything" tag 'math' mimetypes 'text/plain' - default_options :token => 'Text' + default_options token: 'Text' def token @token ||= Token[option :token] @@ -18,4 +18,4 @@ module Rouge end end end -end \ No newline at end of file +end diff --git a/spec/lib/banzai/filter/inline_math_filter_spec.rb b/spec/lib/banzai/filter/inline_math_filter_spec.rb deleted file mode 100644 index 01d791f9ca1..00000000000 --- a/spec/lib/banzai/filter/inline_math_filter_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -require 'spec_helper' - -describe Banzai::Filter::InlineMathFilter, lib: true do - include FilterSpecHelper - - it 'leaves regular inline code unchanged' do - input = "<code>2+2</code>" - doc = filter(input) - expect(doc.to_s).to eq input - end - - it 'removes surrounding dollar signs and adds class' do - doc = filter("$<code>2+2</code>$") - expect(doc.to_s).to eq '<code class="code math">2+2</code>' - end - - it 'only removes surrounding dollar signs' do - doc = filter("test $<code>2+2</code>$ test") - expect(doc.to_s).to eq 'test <code class="code math">2+2</code> test' - end - - it 'only removes surrounding single dollar sign' do - doc = filter("test $$<code>2+2</code>$$ test") - expect(doc.to_s).to eq 'test $<code class="code math">2+2</code>$ test' - end - - it 'ignores cases with missing dolar sign at the end' do - input = "test $<code>2+2</code> test" - doc = filter(input) - expect(doc.to_s).to eq input - end - - it 'ignores cases with missing dolar sign at the beginning' do - input = "test <code>2+2</code>$ test" - doc = filter(input) - expect(doc.to_s).to eq input - end - - it 'ignores dollar signs if it is not adjacent' do - input = '<p>We check strictly $<code>2+2</code> and <code>2+2</code>$ </p>' - doc = filter(input) - expect(doc.to_s).to eq input - end - -end diff --git a/spec/lib/banzai/filter/math_filter_spec.rb b/spec/lib/banzai/filter/math_filter_spec.rb new file mode 100644 index 00000000000..3fe2c7f5d5d --- /dev/null +++ b/spec/lib/banzai/filter/math_filter_spec.rb @@ -0,0 +1,120 @@ +require 'spec_helper' + +describe Banzai::Filter::MathFilter, lib: true do + include FilterSpecHelper + + it 'leaves regular inline code unchanged' do + input = "<code>2+2</code>" + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'removes surrounding dollar signs and adds class code, math and js-render-math' do + doc = filter("$<code>2+2</code>$") + + expect(doc.to_s).to eq '<code class="code math js-render-math" data-math-style="inline">2+2</code>' + end + + it 'only removes surrounding dollar signs' do + doc = filter("test $<code>2+2</code>$ test") + before = doc.xpath('descendant-or-self::text()[1]').first + after = doc.xpath('descendant-or-self::text()[3]').first + + expect(before.to_s).to eq 'test ' + expect(after.to_s).to eq ' test' + end + + it 'only removes surrounding single dollar sign' do + doc = filter("test $$<code>2+2</code>$$ test") + before = doc.xpath('descendant-or-self::text()[1]').first + after = doc.xpath('descendant-or-self::text()[3]').first + + expect(before.to_s).to eq 'test $' + expect(after.to_s).to eq '$ test' + end + + it 'adds data-math-style inline attribute to inline math' do + doc = filter('$<code>2+2</code>$') + code = doc.xpath('descendant-or-self::code').first + + expect(code['data-math-style']).to eq 'inline' + end + + it 'adds class code and math to inline math' do + doc = filter('$<code>2+2</code>$') + code = doc.xpath('descendant-or-self::code').first + + expect(code[:class]).to include("code") + expect(code[:class]).to include("math") + end + + it 'adds js-render-math class to inline math' do + doc = filter('$<code>2+2</code>$') + code = doc.xpath('descendant-or-self::code').first + + expect(code[:class]).to include("js-render-math") + end + + # Cases with faulty syntax. Should be a no-op + + it 'ignores cases with missing dolar sign at the end' do + input = "test $<code>2+2</code> test" + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'ignores cases with missing dolar sign at the beginning' do + input = "test <code>2+2</code>$ test" + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'ignores dollar signs if it is not adjacent' do + input = '<p>We check strictly $<code>2+2</code> and <code>2+2</code>$ </p>' + doc = filter(input) + + expect(doc.to_s).to eq input + end + + # Display math + + it 'adds data-math-style display attribute to display math' do + doc = filter('<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>') + pre = doc.xpath('descendant-or-self::pre').first + + expect(pre['data-math-style']).to eq 'display' + end + + it 'adds js-render-math class to display math' do + doc = filter('<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>') + pre = doc.xpath('descendant-or-self::pre').first + + expect(pre[:class]).to include("js-render-math") + end + + it 'ignores code blocks that are not math' do + input = '<pre class="code highlight js-syntax-highlight plaintext" v-pre="true"><code>2+2</code></pre>' + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'requires the pre to contain both code and math' do + input = '<pre class="highlight js-syntax-highlight plaintext math" v-pre="true"><code>2+2</code></pre>' + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'dollar signs around to display math' do + doc = filter('$<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>$') + before = doc.xpath('descendant-or-self::text()[1]').first + after = doc.xpath('descendant-or-self::text()[3]').first + + expect(before.to_s).to eq '$' + expect(after.to_s).to eq '$' + end +end diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index 4aba783dc33..f3843ca64ff 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -11,7 +11,7 @@ module Gitlab it "converts the input using Asciidoctor and default options" do expected_asciidoc_opts = { safe: :secure, - backend: :html5, + backend: :gitlab_html5, attributes: described_class::DEFAULT_ADOC_ATTRS } @@ -27,7 +27,7 @@ module Gitlab it "merges the options with default ones" do expected_asciidoc_opts = { safe: :safe, - backend: :html5, + backend: :gitlab_html5, attributes: described_class::DEFAULT_ADOC_ATTRS + ['foo'] } -- cgit v1.2.1 From a0ba72ee28ff666e651d3a9cdef19a9d35c22616 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra <dimitriehoekstra@gmail.com> Date: Wed, 14 Dec 2016 17:53:39 +0100 Subject: css changes @dimitrieh v1 --- app/assets/stylesheets/framework/dropdowns.scss | 2 +- app/assets/stylesheets/framework/variables.scss | 2 +- app/assets/stylesheets/pages/pipelines.scss | 27 ++++++++++++++++++++++--- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index d5914b900e2..21df80c01f7 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -98,7 +98,7 @@ @extend .dropdown-toggle; padding-right: 20px; position: relative; - width: 160px; + width: 163px; text-overflow: ellipsis; overflow: hidden; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 03fbfa5f1bd..3ed19672ec1 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -578,4 +578,4 @@ Pipeline Graph */ $stage-hover-bg: #eaf3fc; $stage-hover-border: #d1e7fc; -$stage-badge-text: #d4d4d4; +$stage-badge-text: #e5e5e5; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 26487b2acf9..ac18c39dfc4 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -460,7 +460,7 @@ border-radius: 30px; background-color: $white-light; position: relative; - padding: 8px 10px 9px; + padding: 8px 4px 9px 10px; width: 186px; margin-bottom: 10px; @@ -605,7 +605,7 @@ float: right; color: $stage-badge-text; font-weight: 100; - font-size: 13px; + font-size: 15px; margin-top: 1px; margin-right: 2px; } @@ -629,6 +629,7 @@ ul { max-height: 245px; overflow: auto; + margin: 5px 0; li { padding-top: 2px; @@ -665,6 +666,18 @@ color: $gl-text-color; } + .ci-action-icon-container { + i { + width: 25px; + height: 25px; + + &:before{ + top: 1px; + left: 1px; + } + } + } + .stage { max-width: 100px; width: 100px; @@ -681,7 +694,7 @@ // Action Icons .ci-action-icon-container .ci-action-icon-wrapper { float: right; - margin-top: -1px; + margin-top: -4px; i { color: $stage-badge-text; @@ -690,6 +703,14 @@ padding: 5px 6px; font-size: 13px; background: $white-light; + height: 30px; + width: 30px; + + &:before { + position: relative; + top: 3px; + left: 3px; + } &:hover { color: $gl-text-color; -- cgit v1.2.1 From 886a939b02c38b5caa222d47f68033fd0cbaf799 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 14 Dec 2016 16:59:36 +0000 Subject: Username exists check respects the relative root URL Closes #25548 --- app/assets/javascripts/username_validator.js.es6 | 2 +- changelogs/unreleased/username-exists-root.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/username-exists-root.yml diff --git a/app/assets/javascripts/username_validator.js.es6 b/app/assets/javascripts/username_validator.js.es6 index c4dde575c6e..609dec12412 100644 --- a/app/assets/javascripts/username_validator.js.es6 +++ b/app/assets/javascripts/username_validator.js.es6 @@ -77,7 +77,7 @@ this.renderState(); return $.ajax({ type: 'GET', - url: `/users/${username}/exists`, + url: `${gon.relative_url_root}/users/${username}/exists`, dataType: 'json', success: (res) => this.setAvailabilityState(res.exists) }); diff --git a/changelogs/unreleased/username-exists-root.yml b/changelogs/unreleased/username-exists-root.yml new file mode 100644 index 00000000000..1ffb3eb435c --- /dev/null +++ b/changelogs/unreleased/username-exists-root.yml @@ -0,0 +1,4 @@ +--- +title: Username exists check respects relative root path +merge_request: +author: -- cgit v1.2.1 From 98a12e535f612339f1af59c9d479b0cdab69df3a Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 14 Dec 2016 17:18:00 +0000 Subject: Fix extension in file --- app/views/ci/status/_graph_badge.haml | 19 ------------------- app/views/ci/status/_graph_badge.html.haml | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 19 deletions(-) delete mode 100644 app/views/ci/status/_graph_badge.haml create mode 100644 app/views/ci/status/_graph_badge.html.haml diff --git a/app/views/ci/status/_graph_badge.haml b/app/views/ci/status/_graph_badge.haml deleted file mode 100644 index 839b4334713..00000000000 --- a/app/views/ci/status/_graph_badge.haml +++ /dev/null @@ -1,19 +0,0 @@ --# Renders the graph node with both the status icon, status name and action icon - -- detailed_status = subject.detailed_status(current_user) -- details_path = detailed_status.details_path if detailed_status.has_details? -- klass = "ci-status-icon ci-status-icon-#{detailed_status}" - -- if details_path - = link_to details_path, data: { toggle: 'tooltip', title: "#{subject.name} - #{detailed_status}" } do - %span{ class: klass }= custom_icon(detailed_status.icon) - .ci-status-text= subject.name -- else - %span{ class: klass }= custom_icon(detailed_status.icon) - .ci-status-text= subject.name - -- if detailed_status.has_action? - = link_to detailed_status.action_path, method: detailed_status.action_method, - title: "#{subject.name}: #{detailed_status.action_title}", class: 'ci-action-icon-container' do - %i.ci-action-icon-wrapper - = icon(detailed_status.action_icon, class: detailed_status.action_class) diff --git a/app/views/ci/status/_graph_badge.html.haml b/app/views/ci/status/_graph_badge.html.haml new file mode 100644 index 00000000000..839b4334713 --- /dev/null +++ b/app/views/ci/status/_graph_badge.html.haml @@ -0,0 +1,19 @@ +-# Renders the graph node with both the status icon, status name and action icon + +- detailed_status = subject.detailed_status(current_user) +- details_path = detailed_status.details_path if detailed_status.has_details? +- klass = "ci-status-icon ci-status-icon-#{detailed_status}" + +- if details_path + = link_to details_path, data: { toggle: 'tooltip', title: "#{subject.name} - #{detailed_status}" } do + %span{ class: klass }= custom_icon(detailed_status.icon) + .ci-status-text= subject.name +- else + %span{ class: klass }= custom_icon(detailed_status.icon) + .ci-status-text= subject.name + +- if detailed_status.has_action? + = link_to detailed_status.action_path, method: detailed_status.action_method, + title: "#{subject.name}: #{detailed_status.action_title}", class: 'ci-action-icon-container' do + %i.ci-action-icon-wrapper + = icon(detailed_status.action_icon, class: detailed_status.action_class) -- cgit v1.2.1 From acc4b73cb5e3edfed1fa25d461d36b82fe2aacb2 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 14 Dec 2016 17:31:43 +0000 Subject: Remove duplicate color variable --- app/assets/stylesheets/framework/variables.scss | 1 - app/assets/stylesheets/pages/pipelines.scss | 11 +++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 3ed19672ec1..710b971615b 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -578,4 +578,3 @@ Pipeline Graph */ $stage-hover-bg: #eaf3fc; $stage-hover-border: #d1e7fc; -$stage-badge-text: #e5e5e5; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index ac18c39dfc4..0124940408a 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -603,10 +603,9 @@ .dropdown-counter-badge { float: right; - color: $stage-badge-text; + color: $border-color; font-weight: 100; font-size: 15px; - margin-top: 1px; margin-right: 2px; } @@ -687,6 +686,10 @@ height: 18px; width: 18px; } + + .ci-status-text { + max-width: 95px; + } } } } @@ -697,9 +700,9 @@ margin-top: -4px; i { - color: $stage-badge-text; + color: $border-color; border-radius: 100%; - border: 1px solid $stage-badge-text; + border: 1px solid $border-color; padding: 5px 6px; font-size: 13px; background: $white-light; -- cgit v1.2.1 From c756e62b08f0a639f5550d17339b2938c9c9e096 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Wed, 14 Dec 2016 20:19:26 +0200 Subject: BB importer: fix specs --- spec/lib/bitbucket/connection_spec.rb | 4 ++++ spec/lib/bitbucket/representation/comment_spec.rb | 2 +- spec/lib/bitbucket/representation/issue_spec.rb | 6 +++--- spec/lib/bitbucket/representation/pull_request_comment_spec.rb | 3 +-- spec/lib/bitbucket/representation/pull_request_spec.rb | 4 ++-- spec/lib/gitlab/bitbucket_import/importer_spec.rb | 8 ++++---- 6 files changed, 15 insertions(+), 12 deletions(-) diff --git a/spec/lib/bitbucket/connection_spec.rb b/spec/lib/bitbucket/connection_spec.rb index 6be681a8b47..14faeb231a9 100644 --- a/spec/lib/bitbucket/connection_spec.rb +++ b/spec/lib/bitbucket/connection_spec.rb @@ -1,6 +1,10 @@ require 'spec_helper' describe Bitbucket::Connection do + before do + allow_any_instance_of(described_class).to receive(:provider).and_return(double(app_id: '', app_secret: '')) + end + describe '#get' do it 'calls OAuth2::AccessToken::get' do expect_any_instance_of(OAuth2::AccessToken).to receive(:get).and_return(double(parsed: true)) diff --git a/spec/lib/bitbucket/representation/comment_spec.rb b/spec/lib/bitbucket/representation/comment_spec.rb index 5864193cbfc..fec243a9f96 100644 --- a/spec/lib/bitbucket/representation/comment_spec.rb +++ b/spec/lib/bitbucket/representation/comment_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Bitbucket::Representation::Comment do describe '#author' do it { expect(described_class.new('user' => { 'username' => 'Ben' }).author).to eq('Ben') } - it { expect(described_class.new({}).author).to eq('Anonymous') } + it { expect(described_class.new({}).author).to be_nil } end describe '#note' do diff --git a/spec/lib/bitbucket/representation/issue_spec.rb b/spec/lib/bitbucket/representation/issue_spec.rb index 56deae63bbc..e1f3419c77e 100644 --- a/spec/lib/bitbucket/representation/issue_spec.rb +++ b/spec/lib/bitbucket/representation/issue_spec.rb @@ -10,12 +10,12 @@ describe Bitbucket::Representation::Issue do end describe '#author' do - it { expect(described_class.new({ 'reporter' => { 'username' => 'Ben' }}).author).to eq('Ben') } - it { expect(described_class.new({}).author).to eq('Anonymous') } + it { expect(described_class.new({ 'reporter' => { 'username' => 'Ben' } }).author).to eq('Ben') } + it { expect(described_class.new({}).author).to be_nil } end describe '#description' do - it { expect(described_class.new({ 'content' => { 'raw' => 'Text' }}).description).to eq('Text') } + it { expect(described_class.new({ 'content' => { 'raw' => 'Text' } }).description).to eq('Text') } it { expect(described_class.new({}).description).to be_nil } end diff --git a/spec/lib/bitbucket/representation/pull_request_comment_spec.rb b/spec/lib/bitbucket/representation/pull_request_comment_spec.rb index 8377f0540cd..673dcf22ce8 100644 --- a/spec/lib/bitbucket/representation/pull_request_comment_spec.rb +++ b/spec/lib/bitbucket/representation/pull_request_comment_spec.rb @@ -17,9 +17,8 @@ describe Bitbucket::Representation::PullRequestComment do it { expect(described_class.new('inline' => { 'to' => 3 }).new_pos).to eq(3) } end - describe '#parent_id' do - it { expect(described_class.new({ 'parent' => { 'id' => 2 }}).parent_id).to eq(2) } + it { expect(described_class.new({ 'parent' => { 'id' => 2 } }).parent_id).to eq(2) } it { expect(described_class.new({}).parent_id).to be_nil } end diff --git a/spec/lib/bitbucket/representation/pull_request_spec.rb b/spec/lib/bitbucket/representation/pull_request_spec.rb index 661422efddf..30453528be4 100644 --- a/spec/lib/bitbucket/representation/pull_request_spec.rb +++ b/spec/lib/bitbucket/representation/pull_request_spec.rb @@ -6,8 +6,8 @@ describe Bitbucket::Representation::PullRequest do end describe '#author' do - it { expect(described_class.new({ 'author' => { 'username' => 'Ben' }}).author).to eq('Ben') } - it { expect(described_class.new({}).author).to eq('Anonymous') } + it { expect(described_class.new({ 'author' => { 'username' => 'Ben' } }).author).to eq('Ben') } + it { expect(described_class.new({}).author).to be_nil } end describe '#description' do diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index 353312675d6..53f3c73ade4 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -84,10 +84,10 @@ describe Gitlab::BitbucketImport::Importer, lib: true do body: issues_statuses_sample_data.to_json) stub_request(:get, "https://api.bitbucket.org/2.0/repositories/namespace/repo?pagelen=50&sort=created_on"). - with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization'=>'Bearer', 'User-Agent'=>'Faraday v0.9.2'}). - to_return(:status => 200, - :body => "", - :headers => {}) + with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer', 'User-Agent' => 'Faraday v0.9.2' }). + to_return(status: 200, + body: "", + headers: {}) sample_issues_statuses.each_with_index do |issue, index| stub_request( -- cgit v1.2.1 From e998d6fb59e331cf000fa1c7463f79610b87be7c Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Wed, 14 Dec 2016 20:06:24 +0100 Subject: Simplify graph status badge partial and require locals --- app/views/ci/status/_graph_badge.html.haml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/views/ci/status/_graph_badge.html.haml b/app/views/ci/status/_graph_badge.html.haml index 839b4334713..a7e8544e7d4 100644 --- a/app/views/ci/status/_graph_badge.html.haml +++ b/app/views/ci/status/_graph_badge.html.haml @@ -1,19 +1,19 @@ -# Renders the graph node with both the status icon, status name and action icon -- detailed_status = subject.detailed_status(current_user) -- details_path = detailed_status.details_path if detailed_status.has_details? -- klass = "ci-status-icon ci-status-icon-#{detailed_status}" +- subject = local_assigns.fetch(:subject) +- status = subject.detailed_status(current_user) +- klass = "ci-status-icon ci-status-icon-#{status}" -- if details_path - = link_to details_path, data: { toggle: 'tooltip', title: "#{subject.name} - #{detailed_status}" } do - %span{ class: klass }= custom_icon(detailed_status.icon) +- if status.has_details? + = link_to status.details_path, data: { toggle: 'tooltip', title: "#{subject.name} - #{status}" } do + %span{ class: klass }= custom_icon(status.icon) .ci-status-text= subject.name - else - %span{ class: klass }= custom_icon(detailed_status.icon) + %span{ class: klass }= custom_icon(status.icon) .ci-status-text= subject.name -- if detailed_status.has_action? - = link_to detailed_status.action_path, method: detailed_status.action_method, - title: "#{subject.name}: #{detailed_status.action_title}", class: 'ci-action-icon-container' do +- if status.has_action? + = link_to status.action_path, method: status.action_method, + title: "#{subject.name}: #{status.action_title}", class: 'ci-action-icon-container' do %i.ci-action-icon-wrapper - = icon(detailed_status.action_icon, class: detailed_status.action_class) + = icon(status.action_icon, class: status.action_class) -- cgit v1.2.1 From b3bd9abceaaccafe0462220ffe4bf611c784e5c0 Mon Sep 17 00:00:00 2001 From: tauriedavis <taurie@gitlab.com> Date: Thu, 1 Dec 2016 14:45:48 -0800 Subject: 24824 Add focus state to dropdowns --- app/assets/stylesheets/framework/dropdowns.scss | 1 - changelogs/unreleased/24824-dropdown-items-focus.yml | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/24824-dropdown-items-focus.yml diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index d5914b900e2..b1422944926 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -188,7 +188,6 @@ &.is-focused { background-color: $dropdown-link-hover-bg; text-decoration: none; - outline: 0; } &.dropdown-menu-empty-link { diff --git a/changelogs/unreleased/24824-dropdown-items-focus.yml b/changelogs/unreleased/24824-dropdown-items-focus.yml new file mode 100644 index 00000000000..66970c2a9a5 --- /dev/null +++ b/changelogs/unreleased/24824-dropdown-items-focus.yml @@ -0,0 +1,4 @@ +--- +title: Add focus state to dropdown items +merge_request: +author: -- cgit v1.2.1 From b7b83fe0c9368fa6f04dcb6eb8cd247978bba76b Mon Sep 17 00:00:00 2001 From: Nick Thomas <nick@gitlab.com> Date: Thu, 8 Dec 2016 16:36:26 +0000 Subject: Introduce deployment services, starting with a KubernetesService --- Gemfile | 3 + Gemfile.lock | 22 ++++ app/controllers/concerns/service_params.rb | 2 +- app/models/project.rb | 9 ++ app/models/project_services/deployment_service.rb | 11 ++ app/models/project_services/kubernetes_service.rb | 118 +++++++++++++++++++ app/models/service.rb | 1 + changelogs/unreleased/22864-kubernetes-service.yml | 4 + .../img/kubernetes_configuration.png | Bin 0 -> 113827 bytes doc/project_services/kubernetes.md | 38 +++++++ doc/project_services/project_services.md | 1 + lib/api/services.rb | 28 +++++ lib/gitlab/regex.rb | 8 ++ spec/factories/projects.rb | 13 +++ spec/lib/gitlab/import_export/all_models.yml | 1 + .../project_services/kubernetes_service_spec.rb | 126 +++++++++++++++++++++ 16 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 app/models/project_services/deployment_service.rb create mode 100644 app/models/project_services/kubernetes_service.rb create mode 100644 changelogs/unreleased/22864-kubernetes-service.yml create mode 100644 doc/project_services/img/kubernetes_configuration.png create mode 100644 doc/project_services/kubernetes.md create mode 100644 spec/models/project_services/kubernetes_service_spec.rb diff --git a/Gemfile b/Gemfile index 2cc7764e6b8..17e628ceea6 100644 --- a/Gemfile +++ b/Gemfile @@ -178,6 +178,9 @@ gem 'asana', '~> 0.4.0' # FogBugz integration gem 'ruby-fogbugz', '~> 0.2.1' +# Kubernetes integration +gem 'kubeclient', '~> 2.2.0' + # d3 gem 'd3_rails', '~> 3.5.0' diff --git a/Gemfile.lock b/Gemfile.lock index 3de1a7cbf26..7269b528e30 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -161,6 +161,8 @@ GEM diff-lcs (1.2.5) diffy (3.1.0) docile (1.1.5) + domain_name (0.5.20161021) + unf (>= 0.0.5, < 1.0.0) doorkeeper (4.2.0) railties (>= 4.2) dropzonejs-rails (0.7.2) @@ -318,6 +320,15 @@ GEM html2text (0.2.0) nokogiri (~> 1.6) htmlentities (4.3.4) + http (0.9.8) + addressable (~> 2.3) + http-cookie (~> 1.0) + http-form_data (~> 1.0.1) + http_parser.rb (~> 0.6.0) + http-cookie (1.0.3) + domain_name (~> 0.5) + http-form_data (1.0.1) + http_parser.rb (0.6.0) httparty (0.13.7) json (~> 1.8) multi_xml (>= 0.5.2) @@ -352,6 +363,10 @@ GEM knapsack (1.11.0) rake timecop (>= 0.1.0) + kubeclient (2.2.0) + http (= 0.9.8) + recursive-open-struct (= 1.0.0) + rest-client launchy (2.4.3) addressable (~> 2.3) letter_opener (1.4.1) @@ -388,6 +403,7 @@ GEM mysql2 (0.3.20) net-ldap (0.12.1) net-ssh (3.0.1) + netrc (0.11.0) newrelic_rpm (3.16.0.318) nokogiri (1.6.8) mini_portile2 (~> 2.1.0) @@ -543,6 +559,7 @@ GEM json (~> 1.4) recaptcha (3.0.0) json + recursive-open-struct (1.0.0) redcarpet (3.3.3) redis (3.2.2) redis-actionpack (5.0.1) @@ -568,6 +585,10 @@ GEM listen (~> 3.0) responders (2.3.0) railties (>= 4.2.0, < 5.1) + rest-client (2.0.0) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 4.0) + netrc (~> 0.8) rinku (2.0.0) rotp (2.1.2) rouge (2.0.7) @@ -859,6 +880,7 @@ DEPENDENCIES jwt kaminari (~> 0.17.0) knapsack (~> 1.11.0) + kubeclient (~> 2.2.0) letter_opener_web (~> 1.3.0) license_finder (~> 2.1.0) licensee (~> 8.0.0) diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb index c33d7eecb9f..549a8526715 100644 --- a/app/controllers/concerns/service_params.rb +++ b/app/controllers/concerns/service_params.rb @@ -18,7 +18,7 @@ module ServiceParams :add_pusher, :send_from_committer_email, :disable_diffs, :external_wiki_url, :notify, :color, :server_host, :server_port, :default_irc_uri, :enable_ssl_verification, - :jira_issue_transition_id, :url, :project_key] + :jira_issue_transition_id, :url, :project_key, :ca_pem, :namespace] # Parameters to ignore if no value is specified FILTER_BLANK_PARAMS = [:password] diff --git a/app/models/project.rb b/app/models/project.rb index 77d740081c6..2c726cfc5df 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -106,6 +106,7 @@ class Project < ActiveRecord::Base has_one :bugzilla_service, dependent: :destroy has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project has_one :external_wiki_service, dependent: :destroy + has_one :kubernetes_service, dependent: :destroy, inverse_of: :project has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" has_one :forked_from_project, through: :forked_project_link @@ -742,6 +743,14 @@ class Project < ActiveRecord::Base @ci_service ||= ci_services.reorder(nil).find_by(active: true) end + def deployment_services + services.where(category: :deployment) + end + + def deployment_service + @deployment_service ||= deployment_services.reorder(nil).find_by(active: true) + end + def jira_tracker? issues_tracker.to_param == 'jira' end diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb new file mode 100644 index 00000000000..55e98c31251 --- /dev/null +++ b/app/models/project_services/deployment_service.rb @@ -0,0 +1,11 @@ +# Base class for deployment services +# +# These services integrate with a deployment solution like Kubernetes/OpenShift, +# Mesosphere, etc, to provide additional features to environments. +class DeploymentService < Service + default_value_for :category, 'deployment' + + def supported_events + [] + end +end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb new file mode 100644 index 00000000000..80ae1191108 --- /dev/null +++ b/app/models/project_services/kubernetes_service.rb @@ -0,0 +1,118 @@ +class KubernetesService < DeploymentService + # Namespace defaults to the project path, but can be overridden in case that + # is an invalid or inappropriate name + prop_accessor :namespace + + # Access to kubernetes is directly through the API + prop_accessor :api_url + + # Bearer authentication + # TODO: user/password auth, client certificates + prop_accessor :token + + # Provide a custom CA bundle for self-signed deployments + prop_accessor :ca_pem + + with_options presence: true, if: :activated? do + validates :api_url, url: true + validates :token + + validates :namespace, + format: { + with: Gitlab::Regex.kubernetes_namespace_regex, + message: Gitlab::Regex.kubernetes_namespace_regex_message, + }, + length: 1..63 + end + + def initialize_properties + if properties.nil? + self.properties = {} + self.namespace = project.path if project.present? + end + end + + def title + 'Kubernetes' + end + + def description + 'Kubernetes / Openshift integration' + end + + def help + '' + end + + def to_param + 'kubernetes' + end + + def fields + [ + { type: 'text', + name: 'namespace', + title: 'Kubernetes namespace', + placeholder: 'Kubernetes namespace', + }, + { type: 'text', + name: 'api_url', + title: 'API URL', + placeholder: 'Kubernetes API URL, like https://kube.example.com/', + }, + { type: 'text', + name: 'token', + title: 'Service token', + placeholder: 'Service token', + }, + { type: 'textarea', + name: 'ca_pem', + title: 'Custom CA bundle', + placeholder: 'Certificate Authority bundle (PEM format)', + }, + ] + end + + # Check we can connect to the Kubernetes API + def test(*args) + kubeclient = build_kubeclient + kubeclient.discover + + { success: kubeclient.discovered, result: "Checked API discovery endpoint" } + rescue => err + { success: false, result: err } + end + + private + + def build_kubeclient(api_path = '/api', api_version = 'v1') + return nil unless api_url && namespace && token + + url = URI.parse(api_url) + url.path = url.path[0..-2] if url.path[-1] == "/" + url.path += api_path + + ::Kubeclient::Client.new( + url, + api_version, + ssl_options: kubeclient_ssl_options, + auth_options: kubeclient_auth_options, + http_proxy_uri: ENV['http_proxy'] + ) + end + + def kubeclient_ssl_options + opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } + + if ca_pem.present? + opts[:cert_store] = OpenSSL::X509::Store.new + opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem)) + end + + opts + end + + def kubeclient_auth_options + { bearer_token: token } + end +end diff --git a/app/models/service.rb b/app/models/service.rb index 0c36acfc1b7..e49a8fa2904 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -214,6 +214,7 @@ class Service < ActiveRecord::Base hipchat irker jira + kubernetes mattermost_slash_commands pipelines_email pivotaltracker diff --git a/changelogs/unreleased/22864-kubernetes-service.yml b/changelogs/unreleased/22864-kubernetes-service.yml new file mode 100644 index 00000000000..ea1323cbeb0 --- /dev/null +++ b/changelogs/unreleased/22864-kubernetes-service.yml @@ -0,0 +1,4 @@ +--- +title: Introduce deployment services, starting with a KubernetesService +merge_request: 7994 +author: diff --git a/doc/project_services/img/kubernetes_configuration.png b/doc/project_services/img/kubernetes_configuration.png new file mode 100644 index 00000000000..349a2dc8456 Binary files /dev/null and b/doc/project_services/img/kubernetes_configuration.png differ diff --git a/doc/project_services/kubernetes.md b/doc/project_services/kubernetes.md new file mode 100644 index 00000000000..cb577b608b4 --- /dev/null +++ b/doc/project_services/kubernetes.md @@ -0,0 +1,38 @@ +# GitLab Kubernetes / OpenShift integration + +GitLab can be configured to interact with Kubernetes, or other systems using the +Kubernetes API (such as OpenShift). + +Each project can be configured to connect to a different Kubernetes cluster, see +the [configuration](#configuration) section. + +If you have a single cluster that you want to use for all your projects, +you can pre-fill the settings page with a default template. To configure the +template, see the [Services Templates](services-templates.md) document. + +## Configuration + +![Kubernetes configuration settings](img/kubernetes_configuration.png) + +The Kubernetes service takes the following arguments: + +1. Kubernetes namespace +1. API URL +1. Service token +1. Custom CA bundle + +The API URL is the URL that GitLab uses to access the Kubernetes API. Kubernetes +exposes several APIs - we want the "base" URL that is common to all of them, +e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`. + +GitLab authenticates against Kubernetes using service tokens, which are +scoped to a particular `namespace`. If you don't have a service token yet, +you can follow the +[Kubernetes documentation](http://kubernetes.io/docs/user-guide/service-accounts/) +to create one. You can also view or create service tokens in the +[Kubernetes dashboard](http://kubernetes.io/docs/user-guide/ui/) - visit +`Config -> Secrets`. + +Fill in the service token and namespace according to the values you just got. +If the API is using a self-signed TLS certificate, you'll also need to include +the `ca.crt` contents as the `Custom CA bundle`. diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index 890f7525b0e..a7bcd186a8c 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -42,6 +42,7 @@ further configuration instructions and details. Contributions are welcome. | [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway | | [JIRA](jira.md) | JIRA issue tracker | | JetBrains TeamCity CI | A continuous integration and build server | +| [Kubernetes](kubernetes.md) | A containerized deployment service | | [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands | | PivotalTracker | Project Management Software (Source Commits Endpoint) | | Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop | diff --git a/lib/api/services.rb b/lib/api/services.rb index fde2e2746f1..b1e072b4f47 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -351,6 +351,34 @@ module API desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`' } ], + + 'kubernetes' => [ + { + required: true, + name: :namespace, + type: String, + desc: 'The Kubernetes namespace to use' + }, + { + required: true, + name: :api_url, + type: String, + desc: 'The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com' + }, + { + required: true, + name: :token, + type: String, + desc: 'The service token to authenticate against the Kubernetes cluster with' + }, + { + required: false, + name: :ca_pem, + type: String, + desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)' + }, + ], + 'mattermost-slash-commands' => [ { required: true, diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index d9d1e3cccca..7c711d581e8 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -123,5 +123,13 @@ module Gitlab def environment_name_regex_message "can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.' and spaces" end + + def kubernetes_namespace_regex + /\A[a-z0-9]([-a-z0-9]*[a-z0-9])?\z/ + end + + def kubernetes_namespace_regex_message + "can contain only letters, digits or '-', and cannot start or end with '-'" + end end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 1166498ddff..0d072d6a690 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -133,4 +133,17 @@ FactoryGirl.define do ) end end + + factory :kubernetes_project, parent: :empty_project do + after :create do |project| + project.create_kubernetes_service( + active: true, + properties: { + namespace: project.path, + api_url: 'https://kubernetes.example.com/api', + token: 'a' * 40, + } + ) + end + end end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 8e1a28f2723..c4ee838b7c9 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -147,6 +147,7 @@ project: - bugzilla_service - gitlab_issue_tracker_service - external_wiki_service +- kubernetes_service - forked_project_link - forked_from_project - forked_project_links diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb new file mode 100644 index 00000000000..ffb92012b89 --- /dev/null +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -0,0 +1,126 @@ +require 'spec_helper' + +describe KubernetesService, models: true do + let(:project) { create(:empty_project) } + + describe "Associations" do + it { is_expected.to belong_to :project } + end + + describe 'Validations' do + context 'when service is active' do + before { subject.active = true } + it { is_expected.to validate_presence_of(:namespace) } + it { is_expected.to validate_presence_of(:api_url) } + it { is_expected.to validate_presence_of(:token) } + + context 'namespace format' do + before do + subject.project = project + subject.api_url = "http://example.com" + subject.token = "test" + end + + { + 'foo' => true, + '1foo' => true, + 'foo1' => true, + 'foo-bar' => true, + '-foo' => false, + 'foo-' => false, + 'a' * 63 => true, + 'a' * 64 => false, + 'a.b' => false, + 'a*b' => false, + }.each do |namespace, validity| + it "should validate #{namespace} as #{validity ? 'valid' : 'invalid'}" do + subject.namespace = namespace + + expect(subject.valid?).to eq(validity) + end + end + end + end + + context 'when service is inactive' do + before { subject.active = false } + it { is_expected.not_to validate_presence_of(:namespace) } + it { is_expected.not_to validate_presence_of(:api_url) } + it { is_expected.not_to validate_presence_of(:token) } + end + end + + describe '#initialize_properties' do + context 'with a project' do + it 'defaults to the project name' do + expect(described_class.new(project: project).namespace).to eq(project.name) + end + end + + context 'without a project' do + it 'leaves the namespace unset' do + expect(described_class.new.namespace).to be_nil + end + end + end + + describe '#test' do + let(:project) { create(:kubernetes_project) } + let(:service) { project.kubernetes_service } + let(:discovery_url) { service.api_url + '/api/v1' } + + # JSON response body from Kubernetes GET /api/v1 request + let(:discovery_response) { { "kind" => "APIResourceList", "groupVersion" => "v1", "resources" => [] }.to_json } + + context 'with path prefix in api_url' do + let(:discovery_url) { 'https://kubernetes.example.com/prefix/api/v1' } + + before do + service.api_url = 'https://kubernetes.example.com/prefix/' + end + + it 'tests with the prefix' do + WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response) + + expect(service.test[:success]).to be_truthy + expect(WebMock).to have_requested(:get, discovery_url).once + end + end + + context 'with custom CA certificate' do + let(:certificate) { "CA PEM DATA" } + before do + service.update_attributes!(ca_pem: certificate) + end + + it 'is added to the certificate store' do + cert = double("certificate") + + expect(OpenSSL::X509::Certificate).to receive(:new).with(certificate).and_return(cert) + expect_any_instance_of(OpenSSL::X509::Store).to receive(:add_cert).with(cert) + WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response) + + expect(service.test[:success]).to be_truthy + expect(WebMock).to have_requested(:get, discovery_url).once + end + end + + context 'success' do + it 'reads the discovery endpoint' do + WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response) + + expect(service.test[:success]).to be_truthy + expect(WebMock).to have_requested(:get, discovery_url).once + end + end + + context 'failure' do + it 'fails to read the discovery endpoint' do + WebMock.stub_request(:get, discovery_url).to_return(status: 404) + + expect(service.test[:success]).to be_falsy + expect(WebMock).to have_requested(:get, discovery_url).once + end + end + end +end -- cgit v1.2.1 From 7760d8c06f486a64e9f97f7bc72e8d761683c289 Mon Sep 17 00:00:00 2001 From: tauriedavis <taurie@gitlab.com> Date: Wed, 14 Dec 2016 13:39:57 -0800 Subject: 25617 Fix placeholder color of todo filters --- app/views/dashboard/todos/index.html.haml | 6 +++--- changelogs/unreleased/25617-todos-filter-placeholder.yml | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/25617-todos-filter-placeholder.yml diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index ea95e91eada..e13f404fee2 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -32,7 +32,7 @@ - if params[:project_id].present? = hidden_field_tag(:project_id, params[:project_id]) = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit', - placeholder: 'Search projects', data: { data: todo_projects_options } }) + placeholder: 'Search projects', data: { data: todo_projects_options, default_label: 'Project' } }) .filter-item.inline - if params[:author_id].present? = hidden_field_tag(:author_id, params[:author_id]) @@ -42,12 +42,12 @@ - if params[:type].present? = hidden_field_tag(:type, params[:type]) = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit', - data: { data: todo_types_options } }) + data: { data: todo_types_options, default_label: 'Type' } }) .filter-item.inline.actions-filter - if params[:action_id].present? = hidden_field_tag(:action_id, params[:action_id]) = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit', - data: { data: todo_actions_options }}) + data: { data: todo_actions_options, default_label: 'Action' } }) .pull-right .dropdown.inline.prepend-left-10 %button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'} diff --git a/changelogs/unreleased/25617-todos-filter-placeholder.yml b/changelogs/unreleased/25617-todos-filter-placeholder.yml new file mode 100644 index 00000000000..5d0adb04ef3 --- /dev/null +++ b/changelogs/unreleased/25617-todos-filter-placeholder.yml @@ -0,0 +1,4 @@ +--- +title: 25617 Fix placeholder color of todo filters +merge_request: +author: -- cgit v1.2.1 From 47646d85b10fe92d67f801325daa05a2e49a1188 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 14 Dec 2016 15:54:33 -0600 Subject: fix eslint violations in Object.assign polyfill --- app/assets/javascripts/extensions/object.js.es6 | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/extensions/object.js.es6 b/app/assets/javascripts/extensions/object.js.es6 index f8cdedd507b..70a2d765abd 100644 --- a/app/assets/javascripts/extensions/object.js.es6 +++ b/app/assets/javascripts/extensions/object.js.es6 @@ -1,20 +1,19 @@ -/* eslint-disable */ +/* eslint-disable no-restricted-syntax */ -// Taken from https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill -if (typeof Object.assign != 'function') { - Object.assign = function (target, varArgs) { // .length of function is 2 - 'use strict'; +// Adapted from https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill +if (typeof Object.assign !== 'function') { + Object.assign = function assign(target, ...args) { if (target == null) { // TypeError if undefined or null throw new TypeError('Cannot convert undefined or null to object'); } - var to = Object(target); + const to = Object(target); - for (var index = 1; index < arguments.length; index++) { - var nextSource = arguments[index]; + for (let index = 0; index < args.length; index += 1) { + const nextSource = args[index]; if (nextSource != null) { // Skip over if undefined or null - for (var nextKey in nextSource) { + for (const nextKey in nextSource) { // Avoid bugs when hasOwnProperty is shadowed if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { to[nextKey] = nextSource[nextKey]; -- cgit v1.2.1 From bdae2df31065365283720c1e29d9e5e5d495bcf3 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 14 Dec 2016 23:27:26 +0000 Subject: Fix firefox bug --- app/assets/stylesheets/pages/pipelines.scss | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 0124940408a..ee08d9d3ca0 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -463,6 +463,7 @@ padding: 8px 4px 9px 10px; width: 186px; margin-bottom: 10px; + white-space: normal; &:hover { background-color: $stage-hover-bg; @@ -586,7 +587,7 @@ border: none; padding: 0; color: $gl-text-color-light; - flex-grow: 1; + white-space: normal; &:focus { outline: none; @@ -603,6 +604,7 @@ .dropdown-counter-badge { float: right; + clear: right; color: $border-color; font-weight: 100; font-size: 15px; @@ -651,7 +653,10 @@ padding: 0; font-size: 11px; float: right; - margin-top: 5px; + clear: right; + margin-top: 3px; + display: inline-block; + position: relative; i { font-size: 11px; -- cgit v1.2.1 From 63e5e009b3fb7c85a8471d2e15951bd5d04870b5 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 14 Dec 2016 23:33:25 +0000 Subject: Changes after review --- app/assets/stylesheets/pages/pipelines.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index ee08d9d3ca0..e879c2495fb 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -484,6 +484,10 @@ } } + .ci-status-icon { + position: relative; + top: 1px; + } .ci-status-icon svg { height: 20px; width: 20px; @@ -588,6 +592,7 @@ padding: 0; color: $gl-text-color-light; white-space: normal; + overflow: visible; &:focus { outline: none; -- cgit v1.2.1 From 4564492a84b91b820ef0bcc69a84563ebeed9cb2 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 15 Dec 2016 00:04:22 +0000 Subject: Fix dropdown hover --- app/assets/stylesheets/pages/pipelines.scss | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index e879c2495fb..4569b91383f 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -488,6 +488,7 @@ position: relative; top: 1px; } + .ci-status-icon svg { height: 20px; width: 20px; @@ -638,8 +639,11 @@ margin: 5px 0; li { - padding-top: 2px; margin: 0 5px; + padding-left: 0; + padding-bottom: 0; + margin-bottom: 0; + line-height: 1.2; } li:first-child { @@ -658,8 +662,7 @@ padding: 0; font-size: 11px; float: right; - clear: right; - margin-top: 3px; + margin-top: 4px; display: inline-block; position: relative; @@ -699,6 +702,9 @@ .ci-status-text { max-width: 95px; + padding-bottom: 3px; + position: relative; + top: 3px; } } } -- cgit v1.2.1 From fd3be70dcd1bd625a69701a7be17ae4e733d1512 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Tue, 6 Dec 2016 13:27:56 -0600 Subject: Shift emojis and icons styles into framework --- app/assets/stylesheets/framework.scss | 2 + app/assets/stylesheets/framework/emojis.scss | 1809 ++++++++++++++++++++++++++ app/assets/stylesheets/framework/icons.scss | 51 + app/assets/stylesheets/pages/emojis.scss | 1809 -------------------------- app/assets/stylesheets/pages/explore.scss | 8 - app/assets/stylesheets/pages/icons.scss | 51 - app/views/explore/_head.html.haml | 4 +- 7 files changed, 1864 insertions(+), 1870 deletions(-) create mode 100644 app/assets/stylesheets/framework/emojis.scss create mode 100644 app/assets/stylesheets/framework/icons.scss delete mode 100644 app/assets/stylesheets/pages/emojis.scss delete mode 100644 app/assets/stylesheets/pages/explore.scss delete mode 100644 app/assets/stylesheets/pages/icons.scss diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index c82a9a2b9e3..928ef408722 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -44,3 +44,5 @@ @import "framework/awards.scss"; @import "framework/images.scss"; @import "framework/broadcast-messages"; +@import "framework/emojis.scss"; +@import "framework/icons.scss"; diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss new file mode 100644 index 00000000000..f17797b2381 --- /dev/null +++ b/app/assets/stylesheets/framework/emojis.scss @@ -0,0 +1,1809 @@ +.emoji-0023-20E3 { background-position: 0 0px; } +.emoji-002A-20E3 { background-position: -20px 0; } +.emoji-0030-20E3 { background-position: 0 -20px; } +.emoji-0031-20E3 { background-position: -20px -20px; } +.emoji-0032-20E3 { background-position: -40px 0; } +.emoji-0033-20E3 { background-position: -40px -20px; } +.emoji-0034-20E3 { background-position: 0 -40px; } +.emoji-0035-20E3 { background-position: -20px -40px; } +.emoji-0036-20E3 { background-position: -40px -40px; } +.emoji-0037-20E3 { background-position: -60px 0; } +.emoji-0038-20E3 { background-position: -60px -20px; } +.emoji-0039-20E3 { background-position: -60px -40px; } +.emoji-00A9 { background-position: 0 -60px; } +.emoji-00AE { background-position: -20px -60px; } +.emoji-1F004 { background-position: -40px -60px; } +.emoji-1F0CF { background-position: -60px -60px; } +.emoji-1F170 { background-position: -80px 0; } +.emoji-1F171 { background-position: -80px -20px; } +.emoji-1F17E { background-position: -80px -40px; } +.emoji-1F17F { background-position: -80px -60px; } +.emoji-1F18E { background-position: 0 -80px; } +.emoji-1F191 { background-position: -20px -80px; } +.emoji-1F192 { background-position: -40px -80px; } +.emoji-1F193 { background-position: -60px -80px; } +.emoji-1F194 { background-position: -80px -80px; } +.emoji-1F195 { background-position: -100px 0; } +.emoji-1F196 { background-position: -100px -20px; } +.emoji-1F197 { background-position: -100px -40px; } +.emoji-1F198 { background-position: -100px -60px; } +.emoji-1F199 { background-position: -100px -80px; } +.emoji-1F19A { background-position: 0 -100px; } +.emoji-1F1E6-1F1E8 { background-position: -20px -100px; } +.emoji-1F1E6-1F1E9 { background-position: -40px -100px; } +.emoji-1F1E6-1F1EA { background-position: -60px -100px; } +.emoji-1F1E6-1F1EB { background-position: -80px -100px; } +.emoji-1F1E6-1F1EC { background-position: -100px -100px; } +.emoji-1F1E6-1F1EE { background-position: -120px 0; } +.emoji-1F1E6-1F1F1 { background-position: -120px -20px; } +.emoji-1F1E6-1F1F2 { background-position: -120px -40px; } +.emoji-1F1E6-1F1F4 { background-position: -120px -60px; } +.emoji-1F1E6-1F1F6 { background-position: -120px -80px; } +.emoji-1F1E6-1F1F7 { background-position: -120px -100px; } +.emoji-1F1E6-1F1F8 { background-position: 0 -120px; } +.emoji-1F1E6-1F1F9 { background-position: -20px -120px; } +.emoji-1F1E6-1F1FA { background-position: -40px -120px; } +.emoji-1F1E6-1F1FC { background-position: -60px -120px; } +.emoji-1F1E6-1F1FD { background-position: -80px -120px; } +.emoji-1F1E6-1F1FF { background-position: -100px -120px; } +.emoji-1F1E7-1F1E6 { background-position: -120px -120px; } +.emoji-1F1E7-1F1E7 { background-position: -140px 0; } +.emoji-1F1E7-1F1E9 { background-position: -140px -20px; } +.emoji-1F1E7-1F1EA { background-position: -140px -40px; } +.emoji-1F1E7-1F1EB { background-position: -140px -60px; } +.emoji-1F1E7-1F1EC { background-position: -140px -80px; } +.emoji-1F1E7-1F1ED { background-position: -140px -100px; } +.emoji-1F1E7-1F1EE { background-position: -140px -120px; } +.emoji-1F1E7-1F1EF { background-position: 0 -140px; } +.emoji-1F1E7-1F1F1 { background-position: -20px -140px; } +.emoji-1F1E7-1F1F2 { background-position: -40px -140px; } +.emoji-1F1E7-1F1F3 { background-position: -60px -140px; } +.emoji-1F1E7-1F1F4 { background-position: -80px -140px; } +.emoji-1F1E7-1F1F6 { background-position: -100px -140px; } +.emoji-1F1E7-1F1F7 { background-position: -120px -140px; } +.emoji-1F1E7-1F1F8 { background-position: -140px -140px; } +.emoji-1F1E7-1F1F9 { background-position: -160px 0; } +.emoji-1F1E7-1F1FB { background-position: -160px -20px; } +.emoji-1F1E7-1F1FC { background-position: -160px -40px; } +.emoji-1F1E7-1F1FE { background-position: -160px -60px; } +.emoji-1F1E7-1F1FF { background-position: -160px -80px; } +.emoji-1F1E8-1F1E6 { background-position: -160px -100px; } +.emoji-1F1E8-1F1E8 { background-position: -160px -120px; } +.emoji-1F1E8-1F1E9 { background-position: -160px -140px; } +.emoji-1F1E8-1F1EB { background-position: 0 -160px; } +.emoji-1F1E8-1F1EC { background-position: -20px -160px; } +.emoji-1F1E8-1F1ED { background-position: -40px -160px; } +.emoji-1F1E8-1F1EE { background-position: -60px -160px; } +.emoji-1F1E8-1F1F0 { background-position: -80px -160px; } +.emoji-1F1E8-1F1F1 { background-position: -100px -160px; } +.emoji-1F1E8-1F1F2 { background-position: -120px -160px; } +.emoji-1F1E8-1F1F3 { background-position: -140px -160px; } +.emoji-1F1E8-1F1F4 { background-position: -160px -160px; } +.emoji-1F1E8-1F1F5 { background-position: -180px 0; } +.emoji-1F1E8-1F1F7 { background-position: -180px -20px; } +.emoji-1F1E8-1F1FA { background-position: -180px -40px; } +.emoji-1F1E8-1F1FB { background-position: -180px -60px; } +.emoji-1F1E8-1F1FC { background-position: -180px -80px; } +.emoji-1F1E8-1F1FD { background-position: -180px -100px; } +.emoji-1F1E8-1F1FE { background-position: -180px -120px; } +.emoji-1F1E8-1F1FF { background-position: -180px -140px; } +.emoji-1F1E9-1F1EA { background-position: -180px -160px; } +.emoji-1F1E9-1F1EC { background-position: 0 -180px; } +.emoji-1F1E9-1F1EF { background-position: -20px -180px; } +.emoji-1F1E9-1F1F0 { background-position: -40px -180px; } +.emoji-1F1E9-1F1F2 { background-position: -60px -180px; } +.emoji-1F1E9-1F1F4 { background-position: -80px -180px; } +.emoji-1F1E9-1F1FF { background-position: -100px -180px; } +.emoji-1F1EA-1F1E6 { background-position: -120px -180px; } +.emoji-1F1EA-1F1E8 { background-position: -140px -180px; } +.emoji-1F1EA-1F1EA { background-position: -160px -180px; } +.emoji-1F1EA-1F1EC { background-position: -180px -180px; } +.emoji-1F1EA-1F1ED { background-position: -200px 0; } +.emoji-1F1EA-1F1F7 { background-position: -200px -20px; } +.emoji-1F1EA-1F1F8 { background-position: -200px -40px; } +.emoji-1F1EA-1F1F9 { background-position: -200px -60px; } +.emoji-1F1EA-1F1FA { background-position: -200px -80px; } +.emoji-1F1EB-1F1EE { background-position: -200px -100px; } +.emoji-1F1EB-1F1EF { background-position: -200px -120px; } +.emoji-1F1EB-1F1F0 { background-position: -200px -140px; } +.emoji-1F1EB-1F1F2 { background-position: -200px -160px; } +.emoji-1F1EB-1F1F4 { background-position: -200px -180px; } +.emoji-1F1EB-1F1F7 { background-position: 0 -200px; } +.emoji-1F1EC-1F1E6 { background-position: -20px -200px; } +.emoji-1F1EC-1F1E7 { background-position: -40px -200px; } +.emoji-1F1EC-1F1E9 { background-position: -60px -200px; } +.emoji-1F1EC-1F1EA { background-position: -80px -200px; } +.emoji-1F1EC-1F1EB { background-position: -100px -200px; } +.emoji-1F1EC-1F1EC { background-position: -120px -200px; } +.emoji-1F1EC-1F1ED { background-position: -140px -200px; } +.emoji-1F1EC-1F1EE { background-position: -160px -200px; } +.emoji-1F1EC-1F1F1 { background-position: -180px -200px; } +.emoji-1F1EC-1F1F2 { background-position: -200px -200px; } +.emoji-1F1EC-1F1F3 { background-position: -220px 0; } +.emoji-1F1EC-1F1F5 { background-position: -220px -20px; } +.emoji-1F1EC-1F1F6 { background-position: -220px -40px; } +.emoji-1F1EC-1F1F7 { background-position: -220px -60px; } +.emoji-1F1EC-1F1F8 { background-position: -220px -80px; } +.emoji-1F1EC-1F1F9 { background-position: -220px -100px; } +.emoji-1F1EC-1F1FA { background-position: -220px -120px; } +.emoji-1F1EC-1F1FC { background-position: -220px -140px; } +.emoji-1F1EC-1F1FE { background-position: -220px -160px; } +.emoji-1F1ED-1F1F0 { background-position: -220px -180px; } +.emoji-1F1ED-1F1F2 { background-position: -220px -200px; } +.emoji-1F1ED-1F1F3 { background-position: 0 -220px; } +.emoji-1F1ED-1F1F7 { background-position: -20px -220px; } +.emoji-1F1ED-1F1F9 { background-position: -40px -220px; } +.emoji-1F1ED-1F1FA { background-position: -60px -220px; } +.emoji-1F1EE-1F1E8 { background-position: -80px -220px; } +.emoji-1F1EE-1F1E9 { background-position: -100px -220px; } +.emoji-1F1EE-1F1EA { background-position: -120px -220px; } +.emoji-1F1EE-1F1F1 { background-position: -140px -220px; } +.emoji-1F1EE-1F1F2 { background-position: -160px -220px; } +.emoji-1F1EE-1F1F3 { background-position: -180px -220px; } +.emoji-1F1EE-1F1F4 { background-position: -200px -220px; } +.emoji-1F1EE-1F1F6 { background-position: -220px -220px; } +.emoji-1F1EE-1F1F7 { background-position: -240px 0; } +.emoji-1F1EE-1F1F8 { background-position: -240px -20px; } +.emoji-1F1EE-1F1F9 { background-position: -240px -40px; } +.emoji-1F1EF-1F1EA { background-position: -240px -60px; } +.emoji-1F1EF-1F1F2 { background-position: -240px -80px; } +.emoji-1F1EF-1F1F4 { background-position: -240px -100px; } +.emoji-1F1EF-1F1F5 { background-position: -240px -120px; } +.emoji-1F1F0-1F1EA { background-position: -240px -140px; } +.emoji-1F1F0-1F1EC { background-position: -240px -160px; } +.emoji-1F1F0-1F1ED { background-position: -240px -180px; } +.emoji-1F1F0-1F1EE { background-position: -240px -200px; } +.emoji-1F1F0-1F1F2 { background-position: -240px -220px; } +.emoji-1F1F0-1F1F3 { background-position: 0 -240px; } +.emoji-1F1F0-1F1F5 { background-position: -20px -240px; } +.emoji-1F1F0-1F1F7 { background-position: -40px -240px; } +.emoji-1F1F0-1F1FC { background-position: -60px -240px; } +.emoji-1F1F0-1F1FE { background-position: -80px -240px; } +.emoji-1F1F0-1F1FF { background-position: -100px -240px; } +.emoji-1F1F1-1F1E6 { background-position: -120px -240px; } +.emoji-1F1F1-1F1E7 { background-position: -140px -240px; } +.emoji-1F1F1-1F1E8 { background-position: -160px -240px; } +.emoji-1F1F1-1F1EE { background-position: -180px -240px; } +.emoji-1F1F1-1F1F0 { background-position: -200px -240px; } +.emoji-1F1F1-1F1F7 { background-position: -220px -240px; } +.emoji-1F1F1-1F1F8 { background-position: -240px -240px; } +.emoji-1F1F1-1F1F9 { background-position: -260px 0; } +.emoji-1F1F1-1F1FA { background-position: -260px -20px; } +.emoji-1F1F1-1F1FB { background-position: -260px -40px; } +.emoji-1F1F1-1F1FE { background-position: -260px -60px; } +.emoji-1F1F2-1F1E6 { background-position: -260px -80px; } +.emoji-1F1F2-1F1E8 { background-position: -260px -100px; } +.emoji-1F1F2-1F1E9 { background-position: -260px -120px; } +.emoji-1F1F2-1F1EA { background-position: -260px -140px; } +.emoji-1F1F2-1F1EB { background-position: -260px -160px; } +.emoji-1F1F2-1F1EC { background-position: -260px -180px; } +.emoji-1F1F2-1F1ED { background-position: -260px -200px; } +.emoji-1F1F2-1F1F0 { background-position: -260px -220px; } +.emoji-1F1F2-1F1F1 { background-position: -260px -240px; } +.emoji-1F1F2-1F1F2 { background-position: 0 -260px; } +.emoji-1F1F2-1F1F3 { background-position: -20px -260px; } +.emoji-1F1F2-1F1F4 { background-position: -40px -260px; } +.emoji-1F1F2-1F1F5 { background-position: -60px -260px; } +.emoji-1F1F2-1F1F6 { background-position: -80px -260px; } +.emoji-1F1F2-1F1F7 { background-position: -100px -260px; } +.emoji-1F1F2-1F1F8 { background-position: -120px -260px; } +.emoji-1F1F2-1F1F9 { background-position: -140px -260px; } +.emoji-1F1F2-1F1FA { background-position: -160px -260px; } +.emoji-1F1F2-1F1FB { background-position: -180px -260px; } +.emoji-1F1F2-1F1FC { background-position: -200px -260px; } +.emoji-1F1F2-1F1FD { background-position: -220px -260px; } +.emoji-1F1F2-1F1FE { background-position: -240px -260px; } +.emoji-1F1F2-1F1FF { background-position: -260px -260px; } +.emoji-1F1F3-1F1E6 { background-position: -280px 0; } +.emoji-1F1F3-1F1E8 { background-position: -280px -20px; } +.emoji-1F1F3-1F1EA { background-position: -280px -40px; } +.emoji-1F1F3-1F1EB { background-position: -280px -60px; } +.emoji-1F1F3-1F1EC { background-position: -280px -80px; } +.emoji-1F1F3-1F1EE { background-position: -280px -100px; } +.emoji-1F1F3-1F1F1 { background-position: -280px -120px; } +.emoji-1F1F3-1F1F4 { background-position: -280px -140px; } +.emoji-1F1F3-1F1F5 { background-position: -280px -160px; } +.emoji-1F1F3-1F1F7 { background-position: -280px -180px; } +.emoji-1F1F3-1F1FA { background-position: -280px -200px; } +.emoji-1F1F3-1F1FF { background-position: -280px -220px; } +.emoji-1F1F4-1F1F2 { background-position: -280px -240px; } +.emoji-1F1F5-1F1E6 { background-position: -280px -260px; } +.emoji-1F1F5-1F1EA { background-position: 0 -280px; } +.emoji-1F1F5-1F1EB { background-position: -20px -280px; } +.emoji-1F1F5-1F1EC { background-position: -40px -280px; } +.emoji-1F1F5-1F1ED { background-position: -60px -280px; } +.emoji-1F1F5-1F1F0 { background-position: -80px -280px; } +.emoji-1F1F5-1F1F1 { background-position: -100px -280px; } +.emoji-1F1F5-1F1F2 { background-position: -120px -280px; } +.emoji-1F1F5-1F1F3 { background-position: -140px -280px; } +.emoji-1F1F5-1F1F7 { background-position: -160px -280px; } +.emoji-1F1F5-1F1F8 { background-position: -180px -280px; } +.emoji-1F1F5-1F1F9 { background-position: -200px -280px; } +.emoji-1F1F5-1F1FC { background-position: -220px -280px; } +.emoji-1F1F5-1F1FE { background-position: -240px -280px; } +.emoji-1F1F6-1F1E6 { background-position: -260px -280px; } +.emoji-1F1F7-1F1EA { background-position: -280px -280px; } +.emoji-1F1F7-1F1F4 { background-position: -300px 0; } +.emoji-1F1F7-1F1F8 { background-position: -300px -20px; } +.emoji-1F1F7-1F1FA { background-position: -300px -40px; } +.emoji-1F1F7-1F1FC { background-position: -300px -60px; } +.emoji-1F1F8-1F1E6 { background-position: -300px -80px; } +.emoji-1F1F8-1F1E7 { background-position: -300px -100px; } +.emoji-1F1F8-1F1E8 { background-position: -300px -120px; } +.emoji-1F1F8-1F1E9 { background-position: -300px -140px; } +.emoji-1F1F8-1F1EA { background-position: -300px -160px; } +.emoji-1F1F8-1F1EC { background-position: -300px -180px; } +.emoji-1F1F8-1F1ED { background-position: -300px -200px; } +.emoji-1F1F8-1F1EE { background-position: -300px -220px; } +.emoji-1F1F8-1F1EF { background-position: -300px -240px; } +.emoji-1F1F8-1F1F0 { background-position: -300px -260px; } +.emoji-1F1F8-1F1F1 { background-position: -300px -280px; } +.emoji-1F1F8-1F1F2 { background-position: 0 -300px; } +.emoji-1F1F8-1F1F3 { background-position: -20px -300px; } +.emoji-1F1F8-1F1F4 { background-position: -40px -300px; } +.emoji-1F1F8-1F1F7 { background-position: -60px -300px; } +.emoji-1F1F8-1F1F8 { background-position: -80px -300px; } +.emoji-1F1F8-1F1F9 { background-position: -100px -300px; } +.emoji-1F1F8-1F1FB { background-position: -120px -300px; } +.emoji-1F1F8-1F1FD { background-position: -140px -300px; } +.emoji-1F1F8-1F1FE { background-position: -160px -300px; } +.emoji-1F1F8-1F1FF { background-position: -180px -300px; } +.emoji-1F1F9-1F1E6 { background-position: -200px -300px; } +.emoji-1F1F9-1F1E8 { background-position: -220px -300px; } +.emoji-1F1F9-1F1E9 { background-position: -240px -300px; } +.emoji-1F1F9-1F1EB { background-position: -260px -300px; } +.emoji-1F1F9-1F1EC { background-position: -280px -300px; } +.emoji-1F1F9-1F1ED { background-position: -300px -300px; } +.emoji-1F1F9-1F1EF { background-position: -320px 0; } +.emoji-1F1F9-1F1F0 { background-position: -320px -20px; } +.emoji-1F1F9-1F1F1 { background-position: -320px -40px; } +.emoji-1F1F9-1F1F2 { background-position: -320px -60px; } +.emoji-1F1F9-1F1F3 { background-position: -320px -80px; } +.emoji-1F1F9-1F1F4 { background-position: -320px -100px; } +.emoji-1F1F9-1F1F7 { background-position: -320px -120px; } +.emoji-1F1F9-1F1F9 { background-position: -320px -140px; } +.emoji-1F1F9-1F1FB { background-position: -320px -160px; } +.emoji-1F1F9-1F1FC { background-position: -320px -180px; } +.emoji-1F1F9-1F1FF { background-position: -320px -200px; } +.emoji-1F1FA-1F1E6 { background-position: -320px -220px; } +.emoji-1F1FA-1F1EC { background-position: -320px -240px; } +.emoji-1F1FA-1F1F2 { background-position: -320px -260px; } +.emoji-1F1FA-1F1F8 { background-position: -320px -280px; } +.emoji-1F1FA-1F1FE { background-position: -320px -300px; } +.emoji-1F1FA-1F1FF { background-position: 0 -320px; } +.emoji-1F1FB-1F1E6 { background-position: -20px -320px; } +.emoji-1F1FB-1F1E8 { background-position: -40px -320px; } +.emoji-1F1FB-1F1EA { background-position: -60px -320px; } +.emoji-1F1FB-1F1EC { background-position: -80px -320px; } +.emoji-1F1FB-1F1EE { background-position: -100px -320px; } +.emoji-1F1FB-1F1F3 { background-position: -120px -320px; } +.emoji-1F1FB-1F1FA { background-position: -140px -320px; } +.emoji-1F1FC-1F1EB { background-position: -160px -320px; } +.emoji-1F1FC-1F1F8 { background-position: -180px -320px; } +.emoji-1F1FD-1F1F0 { background-position: -200px -320px; } +.emoji-1F1FE-1F1EA { background-position: -220px -320px; } +.emoji-1F1FE-1F1F9 { background-position: -240px -320px; } +.emoji-1F1FF-1F1E6 { background-position: -260px -320px; } +.emoji-1F1FF-1F1F2 { background-position: -280px -320px; } +.emoji-1F1FF-1F1FC { background-position: -300px -320px; } +.emoji-1F201 { background-position: -320px -320px; } +.emoji-1F202 { background-position: -340px 0; } +.emoji-1F21A { background-position: -340px -20px; } +.emoji-1F22F { background-position: -340px -40px; } +.emoji-1F232 { background-position: -340px -60px; } +.emoji-1F233 { background-position: -340px -80px; } +.emoji-1F234 { background-position: -340px -100px; } +.emoji-1F235 { background-position: -340px -120px; } +.emoji-1F236 { background-position: -340px -140px; } +.emoji-1F237 { background-position: -340px -160px; } +.emoji-1F238 { background-position: -340px -180px; } +.emoji-1F239 { background-position: -340px -200px; } +.emoji-1F23A { background-position: -340px -220px; } +.emoji-1F250 { background-position: -340px -240px; } +.emoji-1F251 { background-position: -340px -260px; } +.emoji-1F300 { background-position: -340px -280px; } +.emoji-1F301 { background-position: -340px -300px; } +.emoji-1F302 { background-position: -340px -320px; } +.emoji-1F303 { background-position: 0 -340px; } +.emoji-1F304 { background-position: -20px -340px; } +.emoji-1F305 { background-position: -40px -340px; } +.emoji-1F306 { background-position: -60px -340px; } +.emoji-1F307 { background-position: -80px -340px; } +.emoji-1F308 { background-position: -100px -340px; } +.emoji-1F309 { background-position: -120px -340px; } +.emoji-1F30A { background-position: -140px -340px; } +.emoji-1F30B { background-position: -160px -340px; } +.emoji-1F30C { background-position: -180px -340px; } +.emoji-1F30D { background-position: -200px -340px; } +.emoji-1F30E { background-position: -220px -340px; } +.emoji-1F30F { background-position: -240px -340px; } +.emoji-1F310 { background-position: -260px -340px; } +.emoji-1F311 { background-position: -280px -340px; } +.emoji-1F312 { background-position: -300px -340px; } +.emoji-1F313 { background-position: -320px -340px; } +.emoji-1F314 { background-position: -340px -340px; } +.emoji-1F315 { background-position: -360px 0; } +.emoji-1F316 { background-position: -360px -20px; } +.emoji-1F317 { background-position: -360px -40px; } +.emoji-1F318 { background-position: -360px -60px; } +.emoji-1F319 { background-position: -360px -80px; } +.emoji-1F31A { background-position: -360px -100px; } +.emoji-1F31B { background-position: -360px -120px; } +.emoji-1F31C { background-position: -360px -140px; } +.emoji-1F31D { background-position: -360px -160px; } +.emoji-1F31E { background-position: -360px -180px; } +.emoji-1F31F { background-position: -360px -200px; } +.emoji-1F320 { background-position: -360px -220px; } +.emoji-1F321 { background-position: -360px -240px; } +.emoji-1F324 { background-position: -360px -260px; } +.emoji-1F325 { background-position: -360px -280px; } +.emoji-1F326 { background-position: -360px -300px; } +.emoji-1F327 { background-position: -360px -320px; } +.emoji-1F328 { background-position: -360px -340px; } +.emoji-1F329 { background-position: 0 -360px; } +.emoji-1F32A { background-position: -20px -360px; } +.emoji-1F32B { background-position: -40px -360px; } +.emoji-1F32C { background-position: -60px -360px; } +.emoji-1F32D { background-position: -80px -360px; } +.emoji-1F32E { background-position: -100px -360px; } +.emoji-1F32F { background-position: -120px -360px; } +.emoji-1F330 { background-position: -140px -360px; } +.emoji-1F331 { background-position: -160px -360px; } +.emoji-1F332 { background-position: -180px -360px; } +.emoji-1F333 { background-position: -200px -360px; } +.emoji-1F334 { background-position: -220px -360px; } +.emoji-1F335 { background-position: -240px -360px; } +.emoji-1F336 { background-position: -260px -360px; } +.emoji-1F337 { background-position: -280px -360px; } +.emoji-1F338 { background-position: -300px -360px; } +.emoji-1F339 { background-position: -320px -360px; } +.emoji-1F33A { background-position: -340px -360px; } +.emoji-1F33B { background-position: -360px -360px; } +.emoji-1F33C { background-position: -380px 0; } +.emoji-1F33D { background-position: -380px -20px; } +.emoji-1F33E { background-position: -380px -40px; } +.emoji-1F33F { background-position: -380px -60px; } +.emoji-1F340 { background-position: -380px -80px; } +.emoji-1F341 { background-position: -380px -100px; } +.emoji-1F342 { background-position: -380px -120px; } +.emoji-1F343 { background-position: -380px -140px; } +.emoji-1F344 { background-position: -380px -160px; } +.emoji-1F345 { background-position: -380px -180px; } +.emoji-1F346 { background-position: -380px -200px; } +.emoji-1F347 { background-position: -380px -220px; } +.emoji-1F348 { background-position: -380px -240px; } +.emoji-1F349 { background-position: -380px -260px; } +.emoji-1F34A { background-position: -380px -280px; } +.emoji-1F34B { background-position: -380px -300px; } +.emoji-1F34C { background-position: -380px -320px; } +.emoji-1F34D { background-position: -380px -340px; } +.emoji-1F34E { background-position: -380px -360px; } +.emoji-1F34F { background-position: 0 -380px; } +.emoji-1F350 { background-position: -20px -380px; } +.emoji-1F351 { background-position: -40px -380px; } +.emoji-1F352 { background-position: -60px -380px; } +.emoji-1F353 { background-position: -80px -380px; } +.emoji-1F354 { background-position: -100px -380px; } +.emoji-1F355 { background-position: -120px -380px; } +.emoji-1F356 { background-position: -140px -380px; } +.emoji-1F357 { background-position: -160px -380px; } +.emoji-1F358 { background-position: -180px -380px; } +.emoji-1F359 { background-position: -200px -380px; } +.emoji-1F35A { background-position: -220px -380px; } +.emoji-1F35B { background-position: -240px -380px; } +.emoji-1F35C { background-position: -260px -380px; } +.emoji-1F35D { background-position: -280px -380px; } +.emoji-1F35E { background-position: -300px -380px; } +.emoji-1F35F { background-position: -320px -380px; } +.emoji-1F360 { background-position: -340px -380px; } +.emoji-1F361 { background-position: -360px -380px; } +.emoji-1F362 { background-position: -380px -380px; } +.emoji-1F363 { background-position: -400px 0; } +.emoji-1F364 { background-position: -400px -20px; } +.emoji-1F365 { background-position: -400px -40px; } +.emoji-1F366 { background-position: -400px -60px; } +.emoji-1F367 { background-position: -400px -80px; } +.emoji-1F368 { background-position: -400px -100px; } +.emoji-1F369 { background-position: -400px -120px; } +.emoji-1F36A { background-position: -400px -140px; } +.emoji-1F36B { background-position: -400px -160px; } +.emoji-1F36C { background-position: -400px -180px; } +.emoji-1F36D { background-position: -400px -200px; } +.emoji-1F36E { background-position: -400px -220px; } +.emoji-1F36F { background-position: -400px -240px; } +.emoji-1F370 { background-position: -400px -260px; } +.emoji-1F371 { background-position: -400px -280px; } +.emoji-1F372 { background-position: -400px -300px; } +.emoji-1F373 { background-position: -400px -320px; } +.emoji-1F374 { background-position: -400px -340px; } +.emoji-1F375 { background-position: -400px -360px; } +.emoji-1F376 { background-position: -400px -380px; } +.emoji-1F377 { background-position: 0 -400px; } +.emoji-1F378 { background-position: -20px -400px; } +.emoji-1F379 { background-position: -40px -400px; } +.emoji-1F37A { background-position: -60px -400px; } +.emoji-1F37B { background-position: -80px -400px; } +.emoji-1F37C { background-position: -100px -400px; } +.emoji-1F37D { background-position: -120px -400px; } +.emoji-1F37E { background-position: -140px -400px; } +.emoji-1F37F { background-position: -160px -400px; } +.emoji-1F380 { background-position: -180px -400px; } +.emoji-1F381 { background-position: -200px -400px; } +.emoji-1F382 { background-position: -220px -400px; } +.emoji-1F383 { background-position: -240px -400px; } +.emoji-1F384 { background-position: -260px -400px; } +.emoji-1F385 { background-position: -280px -400px; } +.emoji-1F385-1F3FB { background-position: -300px -400px; } +.emoji-1F385-1F3FC { background-position: -320px -400px; } +.emoji-1F385-1F3FD { background-position: -340px -400px; } +.emoji-1F385-1F3FE { background-position: -360px -400px; } +.emoji-1F385-1F3FF { background-position: -380px -400px; } +.emoji-1F386 { background-position: -400px -400px; } +.emoji-1F387 { background-position: -420px 0; } +.emoji-1F388 { background-position: -420px -20px; } +.emoji-1F389 { background-position: -420px -40px; } +.emoji-1F38A { background-position: -420px -60px; } +.emoji-1F38B { background-position: -420px -80px; } +.emoji-1F38C { background-position: -420px -100px; } +.emoji-1F38D { background-position: -420px -120px; } +.emoji-1F38E { background-position: -420px -140px; } +.emoji-1F38F { background-position: -420px -160px; } +.emoji-1F390 { background-position: -420px -180px; } +.emoji-1F391 { background-position: -420px -200px; } +.emoji-1F392 { background-position: -420px -220px; } +.emoji-1F393 { background-position: -420px -240px; } +.emoji-1F396 { background-position: -420px -260px; } +.emoji-1F397 { background-position: -420px -280px; } +.emoji-1F399 { background-position: -420px -300px; } +.emoji-1F39A { background-position: -420px -320px; } +.emoji-1F39B { background-position: -420px -340px; } +.emoji-1F39E { background-position: -420px -360px; } +.emoji-1F39F { background-position: -420px -380px; } +.emoji-1F3A0 { background-position: -420px -400px; } +.emoji-1F3A1 { background-position: 0 -420px; } +.emoji-1F3A2 { background-position: -20px -420px; } +.emoji-1F3A3 { background-position: -40px -420px; } +.emoji-1F3A4 { background-position: -60px -420px; } +.emoji-1F3A5 { background-position: -80px -420px; } +.emoji-1F3A6 { background-position: -100px -420px; } +.emoji-1F3A7 { background-position: -120px -420px; } +.emoji-1F3A8 { background-position: -140px -420px; } +.emoji-1F3A9 { background-position: -160px -420px; } +.emoji-1F3AA { background-position: -180px -420px; } +.emoji-1F3AB { background-position: -200px -420px; } +.emoji-1F3AC { background-position: -220px -420px; } +.emoji-1F3AD { background-position: -240px -420px; } +.emoji-1F3AE { background-position: -260px -420px; } +.emoji-1F3AF { background-position: -280px -420px; } +.emoji-1F3B0 { background-position: -300px -420px; } +.emoji-1F3B1 { background-position: -320px -420px; } +.emoji-1F3B2 { background-position: -340px -420px; } +.emoji-1F3B3 { background-position: -360px -420px; } +.emoji-1F3B4 { background-position: -380px -420px; } +.emoji-1F3B5 { background-position: -400px -420px; } +.emoji-1F3B6 { background-position: -420px -420px; } +.emoji-1F3B7 { background-position: -440px 0; } +.emoji-1F3B8 { background-position: -440px -20px; } +.emoji-1F3B9 { background-position: -440px -40px; } +.emoji-1F3BA { background-position: -440px -60px; } +.emoji-1F3BB { background-position: -440px -80px; } +.emoji-1F3BC { background-position: -440px -100px; } +.emoji-1F3BD { background-position: -440px -120px; } +.emoji-1F3BE { background-position: -440px -140px; } +.emoji-1F3BF { background-position: -440px -160px; } +.emoji-1F3C0 { background-position: -440px -180px; } +.emoji-1F3C1 { background-position: -440px -200px; } +.emoji-1F3C2 { background-position: -440px -220px; } +.emoji-1F3C3 { background-position: -440px -240px; } +.emoji-1F3C3-1F3FB { background-position: -440px -260px; } +.emoji-1F3C3-1F3FC { background-position: -440px -280px; } +.emoji-1F3C3-1F3FD { background-position: -440px -300px; } +.emoji-1F3C3-1F3FE { background-position: -440px -320px; } +.emoji-1F3C3-1F3FF { background-position: -440px -340px; } +.emoji-1F3C4 { background-position: -440px -360px; } +.emoji-1F3C4-1F3FB { background-position: -440px -380px; } +.emoji-1F3C4-1F3FC { background-position: -440px -400px; } +.emoji-1F3C4-1F3FD { background-position: -440px -420px; } +.emoji-1F3C4-1F3FE { background-position: 0 -440px; } +.emoji-1F3C4-1F3FF { background-position: -20px -440px; } +.emoji-1F3C5 { background-position: -40px -440px; } +.emoji-1F3C6 { background-position: -60px -440px; } +.emoji-1F3C7 { background-position: -80px -440px; } +.emoji-1F3C7-1F3FB { background-position: -100px -440px; } +.emoji-1F3C7-1F3FC { background-position: -120px -440px; } +.emoji-1F3C7-1F3FD { background-position: -140px -440px; } +.emoji-1F3C7-1F3FE { background-position: -160px -440px; } +.emoji-1F3C7-1F3FF { background-position: -180px -440px; } +.emoji-1F3C8 { background-position: -200px -440px; } +.emoji-1F3C9 { background-position: -220px -440px; } +.emoji-1F3CA { background-position: -240px -440px; } +.emoji-1F3CA-1F3FB { background-position: -260px -440px; } +.emoji-1F3CA-1F3FC { background-position: -280px -440px; } +.emoji-1F3CA-1F3FD { background-position: -300px -440px; } +.emoji-1F3CA-1F3FE { background-position: -320px -440px; } +.emoji-1F3CA-1F3FF { background-position: -340px -440px; } +.emoji-1F3CB { background-position: -360px -440px; } +.emoji-1F3CB-1F3FB { background-position: -380px -440px; } +.emoji-1F3CB-1F3FC { background-position: -400px -440px; } +.emoji-1F3CB-1F3FD { background-position: -420px -440px; } +.emoji-1F3CB-1F3FE { background-position: -440px -440px; } +.emoji-1F3CB-1F3FF { background-position: -460px 0; } +.emoji-1F3CC { background-position: -460px -20px; } +.emoji-1F3CD { background-position: -460px -40px; } +.emoji-1F3CE { background-position: -460px -60px; } +.emoji-1F3CF { background-position: -460px -80px; } +.emoji-1F3D0 { background-position: -460px -100px; } +.emoji-1F3D1 { background-position: -460px -120px; } +.emoji-1F3D2 { background-position: -460px -140px; } +.emoji-1F3D3 { background-position: -460px -160px; } +.emoji-1F3D4 { background-position: -460px -180px; } +.emoji-1F3D5 { background-position: -460px -200px; } +.emoji-1F3D6 { background-position: -460px -220px; } +.emoji-1F3D7 { background-position: -460px -240px; } +.emoji-1F3D8 { background-position: -460px -260px; } +.emoji-1F3D9 { background-position: -460px -280px; } +.emoji-1F3DA { background-position: -460px -300px; } +.emoji-1F3DB { background-position: -460px -320px; } +.emoji-1F3DC { background-position: -460px -340px; } +.emoji-1F3DD { background-position: -460px -360px; } +.emoji-1F3DE { background-position: -460px -380px; } +.emoji-1F3DF { background-position: -460px -400px; } +.emoji-1F3E0 { background-position: -460px -420px; } +.emoji-1F3E1 { background-position: -460px -440px; } +.emoji-1F3E2 { background-position: 0 -460px; } +.emoji-1F3E3 { background-position: -20px -460px; } +.emoji-1F3E4 { background-position: -40px -460px; } +.emoji-1F3E5 { background-position: -60px -460px; } +.emoji-1F3E6 { background-position: -80px -460px; } +.emoji-1F3E7 { background-position: -100px -460px; } +.emoji-1F3E8 { background-position: -120px -460px; } +.emoji-1F3E9 { background-position: -140px -460px; } +.emoji-1F3EA { background-position: -160px -460px; } +.emoji-1F3EB { background-position: -180px -460px; } +.emoji-1F3EC { background-position: -200px -460px; } +.emoji-1F3ED { background-position: -220px -460px; } +.emoji-1F3EE { background-position: -240px -460px; } +.emoji-1F3EF { background-position: -260px -460px; } +.emoji-1F3F0 { background-position: -280px -460px; } +.emoji-1F3F3 { background-position: -300px -460px; } +.emoji-1F3F4 { background-position: -320px -460px; } +.emoji-1F3F5 { background-position: -340px -460px; } +.emoji-1F3F7 { background-position: -360px -460px; } +.emoji-1F3F8 { background-position: -380px -460px; } +.emoji-1F3F9 { background-position: -400px -460px; } +.emoji-1F3FA { background-position: -420px -460px; } +.emoji-1F3FB { background-position: -440px -460px; } +.emoji-1F3FC { background-position: -460px -460px; } +.emoji-1F3FD { background-position: -480px 0; } +.emoji-1F3FE { background-position: -480px -20px; } +.emoji-1F3FF { background-position: -480px -40px; } +.emoji-1F400 { background-position: -480px -60px; } +.emoji-1F401 { background-position: -480px -80px; } +.emoji-1F402 { background-position: -480px -100px; } +.emoji-1F403 { background-position: -480px -120px; } +.emoji-1F404 { background-position: -480px -140px; } +.emoji-1F405 { background-position: -480px -160px; } +.emoji-1F406 { background-position: -480px -180px; } +.emoji-1F407 { background-position: -480px -200px; } +.emoji-1F408 { background-position: -480px -220px; } +.emoji-1F409 { background-position: -480px -240px; } +.emoji-1F40A { background-position: -480px -260px; } +.emoji-1F40B { background-position: -480px -280px; } +.emoji-1F40C { background-position: -480px -300px; } +.emoji-1F40D { background-position: -480px -320px; } +.emoji-1F40E { background-position: -480px -340px; } +.emoji-1F40F { background-position: -480px -360px; } +.emoji-1F410 { background-position: -480px -380px; } +.emoji-1F411 { background-position: -480px -400px; } +.emoji-1F412 { background-position: -480px -420px; } +.emoji-1F413 { background-position: -480px -440px; } +.emoji-1F414 { background-position: -480px -460px; } +.emoji-1F415 { background-position: 0 -480px; } +.emoji-1F416 { background-position: -20px -480px; } +.emoji-1F417 { background-position: -40px -480px; } +.emoji-1F418 { background-position: -60px -480px; } +.emoji-1F419 { background-position: -80px -480px; } +.emoji-1F41A { background-position: -100px -480px; } +.emoji-1F41B { background-position: -120px -480px; } +.emoji-1F41C { background-position: -140px -480px; } +.emoji-1F41D { background-position: -160px -480px; } +.emoji-1F41E { background-position: -180px -480px; } +.emoji-1F41F { background-position: -200px -480px; } +.emoji-1F420 { background-position: -220px -480px; } +.emoji-1F421 { background-position: -240px -480px; } +.emoji-1F422 { background-position: -260px -480px; } +.emoji-1F423 { background-position: -280px -480px; } +.emoji-1F424 { background-position: -300px -480px; } +.emoji-1F425 { background-position: -320px -480px; } +.emoji-1F426 { background-position: -340px -480px; } +.emoji-1F427 { background-position: -360px -480px; } +.emoji-1F428 { background-position: -380px -480px; } +.emoji-1F429 { background-position: -400px -480px; } +.emoji-1F42A { background-position: -420px -480px; } +.emoji-1F42B { background-position: -440px -480px; } +.emoji-1F42C { background-position: -460px -480px; } +.emoji-1F42D { background-position: -480px -480px; } +.emoji-1F42E { background-position: -500px 0; } +.emoji-1F42F { background-position: -500px -20px; } +.emoji-1F430 { background-position: -500px -40px; } +.emoji-1F431 { background-position: -500px -60px; } +.emoji-1F432 { background-position: -500px -80px; } +.emoji-1F433 { background-position: -500px -100px; } +.emoji-1F434 { background-position: -500px -120px; } +.emoji-1F435 { background-position: -500px -140px; } +.emoji-1F436 { background-position: -500px -160px; } +.emoji-1F437 { background-position: -500px -180px; } +.emoji-1F438 { background-position: -500px -200px; } +.emoji-1F439 { background-position: -500px -220px; } +.emoji-1F43A { background-position: -500px -240px; } +.emoji-1F43B { background-position: -500px -260px; } +.emoji-1F43C { background-position: -500px -280px; } +.emoji-1F43D { background-position: -500px -300px; } +.emoji-1F43E { background-position: -500px -320px; } +.emoji-1F43F { background-position: -500px -340px; } +.emoji-1F440 { background-position: -500px -360px; } +.emoji-1F441 { background-position: -500px -380px; } +.emoji-1F441-1F5E8 { background-position: -500px -400px; } +.emoji-1F442 { background-position: -500px -420px; } +.emoji-1F442-1F3FB { background-position: -500px -440px; } +.emoji-1F442-1F3FC { background-position: -500px -460px; } +.emoji-1F442-1F3FD { background-position: -500px -480px; } +.emoji-1F442-1F3FE { background-position: 0 -500px; } +.emoji-1F442-1F3FF { background-position: -20px -500px; } +.emoji-1F443 { background-position: -40px -500px; } +.emoji-1F443-1F3FB { background-position: -60px -500px; } +.emoji-1F443-1F3FC { background-position: -80px -500px; } +.emoji-1F443-1F3FD { background-position: -100px -500px; } +.emoji-1F443-1F3FE { background-position: -120px -500px; } +.emoji-1F443-1F3FF { background-position: -140px -500px; } +.emoji-1F444 { background-position: -160px -500px; } +.emoji-1F445 { background-position: -180px -500px; } +.emoji-1F446 { background-position: -200px -500px; } +.emoji-1F446-1F3FB { background-position: -220px -500px; } +.emoji-1F446-1F3FC { background-position: -240px -500px; } +.emoji-1F446-1F3FD { background-position: -260px -500px; } +.emoji-1F446-1F3FE { background-position: -280px -500px; } +.emoji-1F446-1F3FF { background-position: -300px -500px; } +.emoji-1F447 { background-position: -320px -500px; } +.emoji-1F447-1F3FB { background-position: -340px -500px; } +.emoji-1F447-1F3FC { background-position: -360px -500px; } +.emoji-1F447-1F3FD { background-position: -380px -500px; } +.emoji-1F447-1F3FE { background-position: -400px -500px; } +.emoji-1F447-1F3FF { background-position: -420px -500px; } +.emoji-1F448 { background-position: -440px -500px; } +.emoji-1F448-1F3FB { background-position: -460px -500px; } +.emoji-1F448-1F3FC { background-position: -480px -500px; } +.emoji-1F448-1F3FD { background-position: -500px -500px; } +.emoji-1F448-1F3FE { background-position: -520px 0; } +.emoji-1F448-1F3FF { background-position: -520px -20px; } +.emoji-1F449 { background-position: -520px -40px; } +.emoji-1F449-1F3FB { background-position: -520px -60px; } +.emoji-1F449-1F3FC { background-position: -520px -80px; } +.emoji-1F449-1F3FD { background-position: -520px -100px; } +.emoji-1F449-1F3FE { background-position: -520px -120px; } +.emoji-1F449-1F3FF { background-position: -520px -140px; } +.emoji-1F44A { background-position: -520px -160px; } +.emoji-1F44A-1F3FB { background-position: -520px -180px; } +.emoji-1F44A-1F3FC { background-position: -520px -200px; } +.emoji-1F44A-1F3FD { background-position: -520px -220px; } +.emoji-1F44A-1F3FE { background-position: -520px -240px; } +.emoji-1F44A-1F3FF { background-position: -520px -260px; } +.emoji-1F44B { background-position: -520px -280px; } +.emoji-1F44B-1F3FB { background-position: -520px -300px; } +.emoji-1F44B-1F3FC { background-position: -520px -320px; } +.emoji-1F44B-1F3FD { background-position: -520px -340px; } +.emoji-1F44B-1F3FE { background-position: -520px -360px; } +.emoji-1F44B-1F3FF { background-position: -520px -380px; } +.emoji-1F44C { background-position: -520px -400px; } +.emoji-1F44C-1F3FB { background-position: -520px -420px; } +.emoji-1F44C-1F3FC { background-position: -520px -440px; } +.emoji-1F44C-1F3FD { background-position: -520px -460px; } +.emoji-1F44C-1F3FE { background-position: -520px -480px; } +.emoji-1F44C-1F3FF { background-position: -520px -500px; } +.emoji-1F44D { background-position: 0 -520px; } +.emoji-1F44D-1F3FB { background-position: -20px -520px; } +.emoji-1F44D-1F3FC { background-position: -40px -520px; } +.emoji-1F44D-1F3FD { background-position: -60px -520px; } +.emoji-1F44D-1F3FE { background-position: -80px -520px; } +.emoji-1F44D-1F3FF { background-position: -100px -520px; } +.emoji-1F44E { background-position: -120px -520px; } +.emoji-1F44E-1F3FB { background-position: -140px -520px; } +.emoji-1F44E-1F3FC { background-position: -160px -520px; } +.emoji-1F44E-1F3FD { background-position: -180px -520px; } +.emoji-1F44E-1F3FE { background-position: -200px -520px; } +.emoji-1F44E-1F3FF { background-position: -220px -520px; } +.emoji-1F44F { background-position: -240px -520px; } +.emoji-1F44F-1F3FB { background-position: -260px -520px; } +.emoji-1F44F-1F3FC { background-position: -280px -520px; } +.emoji-1F44F-1F3FD { background-position: -300px -520px; } +.emoji-1F44F-1F3FE { background-position: -320px -520px; } +.emoji-1F44F-1F3FF { background-position: -340px -520px; } +.emoji-1F450 { background-position: -360px -520px; } +.emoji-1F450-1F3FB { background-position: -380px -520px; } +.emoji-1F450-1F3FC { background-position: -400px -520px; } +.emoji-1F450-1F3FD { background-position: -420px -520px; } +.emoji-1F450-1F3FE { background-position: -440px -520px; } +.emoji-1F450-1F3FF { background-position: -460px -520px; } +.emoji-1F451 { background-position: -480px -520px; } +.emoji-1F452 { background-position: -500px -520px; } +.emoji-1F453 { background-position: -520px -520px; } +.emoji-1F454 { background-position: -540px 0; } +.emoji-1F455 { background-position: -540px -20px; } +.emoji-1F456 { background-position: -540px -40px; } +.emoji-1F457 { background-position: -540px -60px; } +.emoji-1F458 { background-position: -540px -80px; } +.emoji-1F459 { background-position: -540px -100px; } +.emoji-1F45A { background-position: -540px -120px; } +.emoji-1F45B { background-position: -540px -140px; } +.emoji-1F45C { background-position: -540px -160px; } +.emoji-1F45D { background-position: -540px -180px; } +.emoji-1F45E { background-position: -540px -200px; } +.emoji-1F45F { background-position: -540px -220px; } +.emoji-1F460 { background-position: -540px -240px; } +.emoji-1F461 { background-position: -540px -260px; } +.emoji-1F462 { background-position: -540px -280px; } +.emoji-1F463 { background-position: -540px -300px; } +.emoji-1F464 { background-position: -540px -320px; } +.emoji-1F465 { background-position: -540px -340px; } +.emoji-1F466 { background-position: -540px -360px; } +.emoji-1F466-1F3FB { background-position: -540px -380px; } +.emoji-1F466-1F3FC { background-position: -540px -400px; } +.emoji-1F466-1F3FD { background-position: -540px -420px; } +.emoji-1F466-1F3FE { background-position: -540px -440px; } +.emoji-1F466-1F3FF { background-position: -540px -460px; } +.emoji-1F467 { background-position: -540px -480px; } +.emoji-1F467-1F3FB { background-position: -540px -500px; } +.emoji-1F467-1F3FC { background-position: -540px -520px; } +.emoji-1F467-1F3FD { background-position: 0 -540px; } +.emoji-1F467-1F3FE { background-position: -20px -540px; } +.emoji-1F467-1F3FF { background-position: -40px -540px; } +.emoji-1F468 { background-position: -60px -540px; } +.emoji-1F468-1F3FB { background-position: -80px -540px; } +.emoji-1F468-1F3FC { background-position: -100px -540px; } +.emoji-1F468-1F3FD { background-position: -120px -540px; } +.emoji-1F468-1F3FE { background-position: -140px -540px; } +.emoji-1F468-1F3FF { background-position: -160px -540px; } +.emoji-1F468-1F468-1F466 { background-position: -180px -540px; } +.emoji-1F468-1F468-1F466-1F466 { background-position: -200px -540px; } +.emoji-1F468-1F468-1F467 { background-position: -220px -540px; } +.emoji-1F468-1F468-1F467-1F466 { background-position: -240px -540px; } +.emoji-1F468-1F468-1F467-1F467 { background-position: -260px -540px; } +.emoji-1F468-1F469-1F466-1F466 { background-position: -280px -540px; } +.emoji-1F468-1F469-1F467 { background-position: -300px -540px; } +.emoji-1F468-1F469-1F467-1F466 { background-position: -320px -540px; } +.emoji-1F468-1F469-1F467-1F467 { background-position: -340px -540px; } +.emoji-1F468-2764-1F468 { background-position: -360px -540px; } +.emoji-1F468-2764-1F48B-1F468 { background-position: -380px -540px; } +.emoji-1F469 { background-position: -400px -540px; } +.emoji-1F469-1F3FB { background-position: -420px -540px; } +.emoji-1F469-1F3FC { background-position: -440px -540px; } +.emoji-1F469-1F3FD { background-position: -460px -540px; } +.emoji-1F469-1F3FE { background-position: -480px -540px; } +.emoji-1F469-1F3FF { background-position: -500px -540px; } +.emoji-1F469-1F469-1F466 { background-position: -520px -540px; } +.emoji-1F469-1F469-1F466-1F466 { background-position: -540px -540px; } +.emoji-1F469-1F469-1F467 { background-position: -560px 0; } +.emoji-1F469-1F469-1F467-1F466 { background-position: -560px -20px; } +.emoji-1F469-1F469-1F467-1F467 { background-position: -560px -40px; } +.emoji-1F469-2764-1F469 { background-position: -560px -60px; } +.emoji-1F469-2764-1F48B-1F469 { background-position: -560px -80px; } +.emoji-1F46A { background-position: -560px -100px; } +.emoji-1F46B { background-position: -560px -120px; } +.emoji-1F46C { background-position: -560px -140px; } +.emoji-1F46D { background-position: -560px -160px; } +.emoji-1F46E { background-position: -560px -180px; } +.emoji-1F46E-1F3FB { background-position: -560px -200px; } +.emoji-1F46E-1F3FC { background-position: -560px -220px; } +.emoji-1F46E-1F3FD { background-position: -560px -240px; } +.emoji-1F46E-1F3FE { background-position: -560px -260px; } +.emoji-1F46E-1F3FF { background-position: -560px -280px; } +.emoji-1F46F { background-position: -560px -300px; } +.emoji-1F470 { background-position: -560px -320px; } +.emoji-1F470-1F3FB { background-position: -560px -340px; } +.emoji-1F470-1F3FC { background-position: -560px -360px; } +.emoji-1F470-1F3FD { background-position: -560px -380px; } +.emoji-1F470-1F3FE { background-position: -560px -400px; } +.emoji-1F470-1F3FF { background-position: -560px -420px; } +.emoji-1F471 { background-position: -560px -440px; } +.emoji-1F471-1F3FB { background-position: -560px -460px; } +.emoji-1F471-1F3FC { background-position: -560px -480px; } +.emoji-1F471-1F3FD { background-position: -560px -500px; } +.emoji-1F471-1F3FE { background-position: -560px -520px; } +.emoji-1F471-1F3FF { background-position: -560px -540px; } +.emoji-1F472 { background-position: 0 -560px; } +.emoji-1F472-1F3FB { background-position: -20px -560px; } +.emoji-1F472-1F3FC { background-position: -40px -560px; } +.emoji-1F472-1F3FD { background-position: -60px -560px; } +.emoji-1F472-1F3FE { background-position: -80px -560px; } +.emoji-1F472-1F3FF { background-position: -100px -560px; } +.emoji-1F473 { background-position: -120px -560px; } +.emoji-1F473-1F3FB { background-position: -140px -560px; } +.emoji-1F473-1F3FC { background-position: -160px -560px; } +.emoji-1F473-1F3FD { background-position: -180px -560px; } +.emoji-1F473-1F3FE { background-position: -200px -560px; } +.emoji-1F473-1F3FF { background-position: -220px -560px; } +.emoji-1F474 { background-position: -240px -560px; } +.emoji-1F474-1F3FB { background-position: -260px -560px; } +.emoji-1F474-1F3FC { background-position: -280px -560px; } +.emoji-1F474-1F3FD { background-position: -300px -560px; } +.emoji-1F474-1F3FE { background-position: -320px -560px; } +.emoji-1F474-1F3FF { background-position: -340px -560px; } +.emoji-1F475 { background-position: -360px -560px; } +.emoji-1F475-1F3FB { background-position: -380px -560px; } +.emoji-1F475-1F3FC { background-position: -400px -560px; } +.emoji-1F475-1F3FD { background-position: -420px -560px; } +.emoji-1F475-1F3FE { background-position: -440px -560px; } +.emoji-1F475-1F3FF { background-position: -460px -560px; } +.emoji-1F476 { background-position: -480px -560px; } +.emoji-1F476-1F3FB { background-position: -500px -560px; } +.emoji-1F476-1F3FC { background-position: -520px -560px; } +.emoji-1F476-1F3FD { background-position: -540px -560px; } +.emoji-1F476-1F3FE { background-position: -560px -560px; } +.emoji-1F476-1F3FF { background-position: -580px 0; } +.emoji-1F477 { background-position: -580px -20px; } +.emoji-1F477-1F3FB { background-position: -580px -40px; } +.emoji-1F477-1F3FC { background-position: -580px -60px; } +.emoji-1F477-1F3FD { background-position: -580px -80px; } +.emoji-1F477-1F3FE { background-position: -580px -100px; } +.emoji-1F477-1F3FF { background-position: -580px -120px; } +.emoji-1F478 { background-position: -580px -140px; } +.emoji-1F478-1F3FB { background-position: -580px -160px; } +.emoji-1F478-1F3FC { background-position: -580px -180px; } +.emoji-1F478-1F3FD { background-position: -580px -200px; } +.emoji-1F478-1F3FE { background-position: -580px -220px; } +.emoji-1F478-1F3FF { background-position: -580px -240px; } +.emoji-1F479 { background-position: -580px -260px; } +.emoji-1F47A { background-position: -580px -280px; } +.emoji-1F47B { background-position: -580px -300px; } +.emoji-1F47C { background-position: -580px -320px; } +.emoji-1F47C-1F3FB { background-position: -580px -340px; } +.emoji-1F47C-1F3FC { background-position: -580px -360px; } +.emoji-1F47C-1F3FD { background-position: -580px -380px; } +.emoji-1F47C-1F3FE { background-position: -580px -400px; } +.emoji-1F47C-1F3FF { background-position: -580px -420px; } +.emoji-1F47D { background-position: -580px -440px; } +.emoji-1F47E { background-position: -580px -460px; } +.emoji-1F47F { background-position: -580px -480px; } +.emoji-1F480 { background-position: -580px -500px; } +.emoji-1F481 { background-position: -580px -520px; } +.emoji-1F481-1F3FB { background-position: -580px -540px; } +.emoji-1F481-1F3FC { background-position: -580px -560px; } +.emoji-1F481-1F3FD { background-position: 0 -580px; } +.emoji-1F481-1F3FE { background-position: -20px -580px; } +.emoji-1F481-1F3FF { background-position: -40px -580px; } +.emoji-1F482 { background-position: -60px -580px; } +.emoji-1F482-1F3FB { background-position: -80px -580px; } +.emoji-1F482-1F3FC { background-position: -100px -580px; } +.emoji-1F482-1F3FD { background-position: -120px -580px; } +.emoji-1F482-1F3FE { background-position: -140px -580px; } +.emoji-1F482-1F3FF { background-position: -160px -580px; } +.emoji-1F483 { background-position: -180px -580px; } +.emoji-1F483-1F3FB { background-position: -200px -580px; } +.emoji-1F483-1F3FC { background-position: -220px -580px; } +.emoji-1F483-1F3FD { background-position: -240px -580px; } +.emoji-1F483-1F3FE { background-position: -260px -580px; } +.emoji-1F483-1F3FF { background-position: -280px -580px; } +.emoji-1F484 { background-position: -300px -580px; } +.emoji-1F485 { background-position: -320px -580px; } +.emoji-1F485-1F3FB { background-position: -340px -580px; } +.emoji-1F485-1F3FC { background-position: -360px -580px; } +.emoji-1F485-1F3FD { background-position: -380px -580px; } +.emoji-1F485-1F3FE { background-position: -400px -580px; } +.emoji-1F485-1F3FF { background-position: -420px -580px; } +.emoji-1F486 { background-position: -440px -580px; } +.emoji-1F486-1F3FB { background-position: -460px -580px; } +.emoji-1F486-1F3FC { background-position: -480px -580px; } +.emoji-1F486-1F3FD { background-position: -500px -580px; } +.emoji-1F486-1F3FE { background-position: -520px -580px; } +.emoji-1F486-1F3FF { background-position: -540px -580px; } +.emoji-1F487 { background-position: -560px -580px; } +.emoji-1F487-1F3FB { background-position: -580px -580px; } +.emoji-1F487-1F3FC { background-position: -600px 0; } +.emoji-1F487-1F3FD { background-position: -600px -20px; } +.emoji-1F487-1F3FE { background-position: -600px -40px; } +.emoji-1F487-1F3FF { background-position: -600px -60px; } +.emoji-1F488 { background-position: -600px -80px; } +.emoji-1F489 { background-position: -600px -100px; } +.emoji-1F48A { background-position: -600px -120px; } +.emoji-1F48B { background-position: -600px -140px; } +.emoji-1F48C { background-position: -600px -160px; } +.emoji-1F48D { background-position: -600px -180px; } +.emoji-1F48E { background-position: -600px -200px; } +.emoji-1F48F { background-position: -600px -220px; } +.emoji-1F490 { background-position: -600px -240px; } +.emoji-1F491 { background-position: -600px -260px; } +.emoji-1F492 { background-position: -600px -280px; } +.emoji-1F493 { background-position: -600px -300px; } +.emoji-1F494 { background-position: -600px -320px; } +.emoji-1F495 { background-position: -600px -340px; } +.emoji-1F496 { background-position: -600px -360px; } +.emoji-1F497 { background-position: -600px -380px; } +.emoji-1F498 { background-position: -600px -400px; } +.emoji-1F499 { background-position: -600px -420px; } +.emoji-1F49A { background-position: -600px -440px; } +.emoji-1F49B { background-position: -600px -460px; } +.emoji-1F49C { background-position: -600px -480px; } +.emoji-1F49D { background-position: -600px -500px; } +.emoji-1F49E { background-position: -600px -520px; } +.emoji-1F49F { background-position: -600px -540px; } +.emoji-1F4A0 { background-position: -600px -560px; } +.emoji-1F4A1 { background-position: -600px -580px; } +.emoji-1F4A2 { background-position: 0 -600px; } +.emoji-1F4A3 { background-position: -20px -600px; } +.emoji-1F4A4 { background-position: -40px -600px; } +.emoji-1F4A5 { background-position: -60px -600px; } +.emoji-1F4A6 { background-position: -80px -600px; } +.emoji-1F4A7 { background-position: -100px -600px; } +.emoji-1F4A8 { background-position: -120px -600px; } +.emoji-1F4A9 { background-position: -140px -600px; } +.emoji-1F4AA { background-position: -160px -600px; } +.emoji-1F4AA-1F3FB { background-position: -180px -600px; } +.emoji-1F4AA-1F3FC { background-position: -200px -600px; } +.emoji-1F4AA-1F3FD { background-position: -220px -600px; } +.emoji-1F4AA-1F3FE { background-position: -240px -600px; } +.emoji-1F4AA-1F3FF { background-position: -260px -600px; } +.emoji-1F4AB { background-position: -280px -600px; } +.emoji-1F4AC { background-position: -300px -600px; } +.emoji-1F4AD { background-position: -320px -600px; } +.emoji-1F4AE { background-position: -340px -600px; } +.emoji-1F4AF { background-position: -360px -600px; } +.emoji-1F4B0 { background-position: -380px -600px; } +.emoji-1F4B1 { background-position: -400px -600px; } +.emoji-1F4B2 { background-position: -420px -600px; } +.emoji-1F4B3 { background-position: -440px -600px; } +.emoji-1F4B4 { background-position: -460px -600px; } +.emoji-1F4B5 { background-position: -480px -600px; } +.emoji-1F4B6 { background-position: -500px -600px; } +.emoji-1F4B7 { background-position: -520px -600px; } +.emoji-1F4B8 { background-position: -540px -600px; } +.emoji-1F4B9 { background-position: -560px -600px; } +.emoji-1F4BA { background-position: -580px -600px; } +.emoji-1F4BB { background-position: -600px -600px; } +.emoji-1F4BC { background-position: -620px 0; } +.emoji-1F4BD { background-position: -620px -20px; } +.emoji-1F4BE { background-position: -620px -40px; } +.emoji-1F4BF { background-position: -620px -60px; } +.emoji-1F4C0 { background-position: -620px -80px; } +.emoji-1F4C1 { background-position: -620px -100px; } +.emoji-1F4C2 { background-position: -620px -120px; } +.emoji-1F4C3 { background-position: -620px -140px; } +.emoji-1F4C4 { background-position: -620px -160px; } +.emoji-1F4C5 { background-position: -620px -180px; } +.emoji-1F4C6 { background-position: -620px -200px; } +.emoji-1F4C7 { background-position: -620px -220px; } +.emoji-1F4C8 { background-position: -620px -240px; } +.emoji-1F4C9 { background-position: -620px -260px; } +.emoji-1F4CA { background-position: -620px -280px; } +.emoji-1F4CB { background-position: -620px -300px; } +.emoji-1F4CC { background-position: -620px -320px; } +.emoji-1F4CD { background-position: -620px -340px; } +.emoji-1F4CE { background-position: -620px -360px; } +.emoji-1F4CF { background-position: -620px -380px; } +.emoji-1F4D0 { background-position: -620px -400px; } +.emoji-1F4D1 { background-position: -620px -420px; } +.emoji-1F4D2 { background-position: -620px -440px; } +.emoji-1F4D3 { background-position: -620px -460px; } +.emoji-1F4D4 { background-position: -620px -480px; } +.emoji-1F4D5 { background-position: -620px -500px; } +.emoji-1F4D6 { background-position: -620px -520px; } +.emoji-1F4D7 { background-position: -620px -540px; } +.emoji-1F4D8 { background-position: -620px -560px; } +.emoji-1F4D9 { background-position: -620px -580px; } +.emoji-1F4DA { background-position: -620px -600px; } +.emoji-1F4DB { background-position: 0 -620px; } +.emoji-1F4DC { background-position: -20px -620px; } +.emoji-1F4DD { background-position: -40px -620px; } +.emoji-1F4DE { background-position: -60px -620px; } +.emoji-1F4DF { background-position: -80px -620px; } +.emoji-1F4E0 { background-position: -100px -620px; } +.emoji-1F4E1 { background-position: -120px -620px; } +.emoji-1F4E2 { background-position: -140px -620px; } +.emoji-1F4E3 { background-position: -160px -620px; } +.emoji-1F4E4 { background-position: -180px -620px; } +.emoji-1F4E5 { background-position: -200px -620px; } +.emoji-1F4E6 { background-position: -220px -620px; } +.emoji-1F4E7 { background-position: -240px -620px; } +.emoji-1F4E8 { background-position: -260px -620px; } +.emoji-1F4E9 { background-position: -280px -620px; } +.emoji-1F4EA { background-position: -300px -620px; } +.emoji-1F4EB { background-position: -320px -620px; } +.emoji-1F4EC { background-position: -340px -620px; } +.emoji-1F4ED { background-position: -360px -620px; } +.emoji-1F4EE { background-position: -380px -620px; } +.emoji-1F4EF { background-position: -400px -620px; } +.emoji-1F4F0 { background-position: -420px -620px; } +.emoji-1F4F1 { background-position: -440px -620px; } +.emoji-1F4F2 { background-position: -460px -620px; } +.emoji-1F4F3 { background-position: -480px -620px; } +.emoji-1F4F4 { background-position: -500px -620px; } +.emoji-1F4F5 { background-position: -520px -620px; } +.emoji-1F4F6 { background-position: -540px -620px; } +.emoji-1F4F7 { background-position: -560px -620px; } +.emoji-1F4F8 { background-position: -580px -620px; } +.emoji-1F4F9 { background-position: -600px -620px; } +.emoji-1F4FA { background-position: -620px -620px; } +.emoji-1F4FB { background-position: -640px 0; } +.emoji-1F4FC { background-position: -640px -20px; } +.emoji-1F4FD { background-position: -640px -40px; } +.emoji-1F4FF { background-position: -640px -60px; } +.emoji-1F500 { background-position: -640px -80px; } +.emoji-1F501 { background-position: -640px -100px; } +.emoji-1F502 { background-position: -640px -120px; } +.emoji-1F503 { background-position: -640px -140px; } +.emoji-1F504 { background-position: -640px -160px; } +.emoji-1F505 { background-position: -640px -180px; } +.emoji-1F506 { background-position: -640px -200px; } +.emoji-1F507 { background-position: -640px -220px; } +.emoji-1F508 { background-position: -640px -240px; } +.emoji-1F509 { background-position: -640px -260px; } +.emoji-1F50A { background-position: -640px -280px; } +.emoji-1F50B { background-position: -640px -300px; } +.emoji-1F50C { background-position: -640px -320px; } +.emoji-1F50D { background-position: -640px -340px; } +.emoji-1F50E { background-position: -640px -360px; } +.emoji-1F50F { background-position: -640px -380px; } +.emoji-1F510 { background-position: -640px -400px; } +.emoji-1F511 { background-position: -640px -420px; } +.emoji-1F512 { background-position: -640px -440px; } +.emoji-1F513 { background-position: -640px -460px; } +.emoji-1F514 { background-position: -640px -480px; } +.emoji-1F515 { background-position: -640px -500px; } +.emoji-1F516 { background-position: -640px -520px; } +.emoji-1F517 { background-position: -640px -540px; } +.emoji-1F518 { background-position: -640px -560px; } +.emoji-1F519 { background-position: -640px -580px; } +.emoji-1F51A { background-position: -640px -600px; } +.emoji-1F51B { background-position: -640px -620px; } +.emoji-1F51C { background-position: 0 -640px; } +.emoji-1F51D { background-position: -20px -640px; } +.emoji-1F51E { background-position: -40px -640px; } +.emoji-1F51F { background-position: -60px -640px; } +.emoji-1F520 { background-position: -80px -640px; } +.emoji-1F521 { background-position: -100px -640px; } +.emoji-1F522 { background-position: -120px -640px; } +.emoji-1F523 { background-position: -140px -640px; } +.emoji-1F524 { background-position: -160px -640px; } +.emoji-1F525 { background-position: -180px -640px; } +.emoji-1F526 { background-position: -200px -640px; } +.emoji-1F527 { background-position: -220px -640px; } +.emoji-1F528 { background-position: -240px -640px; } +.emoji-1F529 { background-position: -260px -640px; } +.emoji-1F52A { background-position: -280px -640px; } +.emoji-1F52B { background-position: -300px -640px; } +.emoji-1F52C { background-position: -320px -640px; } +.emoji-1F52D { background-position: -340px -640px; } +.emoji-1F52E { background-position: -360px -640px; } +.emoji-1F52F { background-position: -380px -640px; } +.emoji-1F530 { background-position: -400px -640px; } +.emoji-1F531 { background-position: -420px -640px; } +.emoji-1F532 { background-position: -440px -640px; } +.emoji-1F533 { background-position: -460px -640px; } +.emoji-1F534 { background-position: -480px -640px; } +.emoji-1F535 { background-position: -500px -640px; } +.emoji-1F536 { background-position: -520px -640px; } +.emoji-1F537 { background-position: -540px -640px; } +.emoji-1F538 { background-position: -560px -640px; } +.emoji-1F539 { background-position: -580px -640px; } +.emoji-1F53A { background-position: -600px -640px; } +.emoji-1F53B { background-position: -620px -640px; } +.emoji-1F53C { background-position: -640px -640px; } +.emoji-1F53D { background-position: -660px 0; } +.emoji-1F549 { background-position: -660px -20px; } +.emoji-1F54A { background-position: -660px -40px; } +.emoji-1F54B { background-position: -660px -60px; } +.emoji-1F54C { background-position: -660px -80px; } +.emoji-1F54D { background-position: -660px -100px; } +.emoji-1F54E { background-position: -660px -120px; } +.emoji-1F550 { background-position: -660px -140px; } +.emoji-1F551 { background-position: -660px -160px; } +.emoji-1F552 { background-position: -660px -180px; } +.emoji-1F553 { background-position: -660px -200px; } +.emoji-1F554 { background-position: -660px -220px; } +.emoji-1F555 { background-position: -660px -240px; } +.emoji-1F556 { background-position: -660px -260px; } +.emoji-1F557 { background-position: -660px -280px; } +.emoji-1F558 { background-position: -660px -300px; } +.emoji-1F559 { background-position: -660px -320px; } +.emoji-1F55A { background-position: -660px -340px; } +.emoji-1F55B { background-position: -660px -360px; } +.emoji-1F55C { background-position: -660px -380px; } +.emoji-1F55D { background-position: -660px -400px; } +.emoji-1F55E { background-position: -660px -420px; } +.emoji-1F55F { background-position: -660px -440px; } +.emoji-1F560 { background-position: -660px -460px; } +.emoji-1F561 { background-position: -660px -480px; } +.emoji-1F562 { background-position: -660px -500px; } +.emoji-1F563 { background-position: -660px -520px; } +.emoji-1F564 { background-position: -660px -540px; } +.emoji-1F565 { background-position: -660px -560px; } +.emoji-1F566 { background-position: -660px -580px; } +.emoji-1F567 { background-position: -660px -600px; } +.emoji-1F56F { background-position: -660px -620px; } +.emoji-1F570 { background-position: -660px -640px; } +.emoji-1F573 { background-position: 0 -660px; } +.emoji-1F574 { background-position: -20px -660px; } +.emoji-1F575 { background-position: -40px -660px; } +.emoji-1F575-1F3FB { background-position: -60px -660px; } +.emoji-1F575-1F3FC { background-position: -80px -660px; } +.emoji-1F575-1F3FD { background-position: -100px -660px; } +.emoji-1F575-1F3FE { background-position: -120px -660px; } +.emoji-1F575-1F3FF { background-position: -140px -660px; } +.emoji-1F576 { background-position: -160px -660px; } +.emoji-1F577 { background-position: -180px -660px; } +.emoji-1F578 { background-position: -200px -660px; } +.emoji-1F579 { background-position: -220px -660px; } +.emoji-1F57A { background-position: -240px -660px; } +.emoji-1F57A-1F3FB { background-position: -260px -660px; } +.emoji-1F57A-1F3FC { background-position: -280px -660px; } +.emoji-1F57A-1F3FD { background-position: -300px -660px; } +.emoji-1F57A-1F3FE { background-position: -320px -660px; } +.emoji-1F57A-1F3FF { background-position: -340px -660px; } +.emoji-1F587 { background-position: -360px -660px; } +.emoji-1F58A { background-position: -380px -660px; } +.emoji-1F58B { background-position: -400px -660px; } +.emoji-1F58C { background-position: -420px -660px; } +.emoji-1F58D { background-position: -440px -660px; } +.emoji-1F590 { background-position: -460px -660px; } +.emoji-1F590-1F3FB { background-position: -480px -660px; } +.emoji-1F590-1F3FC { background-position: -500px -660px; } +.emoji-1F590-1F3FD { background-position: -520px -660px; } +.emoji-1F590-1F3FE { background-position: -540px -660px; } +.emoji-1F590-1F3FF { background-position: -560px -660px; } +.emoji-1F595 { background-position: -580px -660px; } +.emoji-1F595-1F3FB { background-position: -600px -660px; } +.emoji-1F595-1F3FC { background-position: -620px -660px; } +.emoji-1F595-1F3FD { background-position: -640px -660px; } +.emoji-1F595-1F3FE { background-position: -660px -660px; } +.emoji-1F595-1F3FF { background-position: -680px 0; } +.emoji-1F596 { background-position: -680px -20px; } +.emoji-1F596-1F3FB { background-position: -680px -40px; } +.emoji-1F596-1F3FC { background-position: -680px -60px; } +.emoji-1F596-1F3FD { background-position: -680px -80px; } +.emoji-1F596-1F3FE { background-position: -680px -100px; } +.emoji-1F596-1F3FF { background-position: -680px -120px; } +.emoji-1F5A4 { background-position: -680px -140px; } +.emoji-1F5A5 { background-position: -680px -160px; } +.emoji-1F5A8 { background-position: -680px -180px; } +.emoji-1F5B1 { background-position: -680px -200px; } +.emoji-1F5B2 { background-position: -680px -220px; } +.emoji-1F5BC { background-position: -680px -240px; } +.emoji-1F5C2 { background-position: -680px -260px; } +.emoji-1F5C3 { background-position: -680px -280px; } +.emoji-1F5C4 { background-position: -680px -300px; } +.emoji-1F5D1 { background-position: -680px -320px; } +.emoji-1F5D2 { background-position: -680px -340px; } +.emoji-1F5D3 { background-position: -680px -360px; } +.emoji-1F5DC { background-position: -680px -380px; } +.emoji-1F5DD { background-position: -680px -400px; } +.emoji-1F5DE { background-position: -680px -420px; } +.emoji-1F5E1 { background-position: -680px -440px; } +.emoji-1F5E3 { background-position: -680px -460px; } +.emoji-1F5EF { background-position: -680px -480px; } +.emoji-1F5F3 { background-position: -680px -500px; } +.emoji-1F5FA { background-position: -680px -520px; } +.emoji-1F5FB { background-position: -680px -540px; } +.emoji-1F5FC { background-position: -680px -560px; } +.emoji-1F5FD { background-position: -680px -580px; } +.emoji-1F5FE { background-position: -680px -600px; } +.emoji-1F5FF { background-position: -680px -620px; } +.emoji-1F600 { background-position: -680px -640px; } +.emoji-1F601 { background-position: -680px -660px; } +.emoji-1F602 { background-position: 0 -680px; } +.emoji-1F603 { background-position: -20px -680px; } +.emoji-1F604 { background-position: -40px -680px; } +.emoji-1F605 { background-position: -60px -680px; } +.emoji-1F606 { background-position: -80px -680px; } +.emoji-1F607 { background-position: -100px -680px; } +.emoji-1F608 { background-position: -120px -680px; } +.emoji-1F609 { background-position: -140px -680px; } +.emoji-1F60A { background-position: -160px -680px; } +.emoji-1F60B { background-position: -180px -680px; } +.emoji-1F60C { background-position: -200px -680px; } +.emoji-1F60D { background-position: -220px -680px; } +.emoji-1F60E { background-position: -240px -680px; } +.emoji-1F60F { background-position: -260px -680px; } +.emoji-1F610 { background-position: -280px -680px; } +.emoji-1F611 { background-position: -300px -680px; } +.emoji-1F612 { background-position: -320px -680px; } +.emoji-1F613 { background-position: -340px -680px; } +.emoji-1F614 { background-position: -360px -680px; } +.emoji-1F615 { background-position: -380px -680px; } +.emoji-1F616 { background-position: -400px -680px; } +.emoji-1F617 { background-position: -420px -680px; } +.emoji-1F618 { background-position: -440px -680px; } +.emoji-1F619 { background-position: -460px -680px; } +.emoji-1F61A { background-position: -480px -680px; } +.emoji-1F61B { background-position: -500px -680px; } +.emoji-1F61C { background-position: -520px -680px; } +.emoji-1F61D { background-position: -540px -680px; } +.emoji-1F61E { background-position: -560px -680px; } +.emoji-1F61F { background-position: -580px -680px; } +.emoji-1F620 { background-position: -600px -680px; } +.emoji-1F621 { background-position: -620px -680px; } +.emoji-1F622 { background-position: -640px -680px; } +.emoji-1F623 { background-position: -660px -680px; } +.emoji-1F624 { background-position: -680px -680px; } +.emoji-1F625 { background-position: -700px 0; } +.emoji-1F626 { background-position: -700px -20px; } +.emoji-1F627 { background-position: -700px -40px; } +.emoji-1F628 { background-position: -700px -60px; } +.emoji-1F629 { background-position: -700px -80px; } +.emoji-1F62A { background-position: -700px -100px; } +.emoji-1F62B { background-position: -700px -120px; } +.emoji-1F62C { background-position: -700px -140px; } +.emoji-1F62D { background-position: -700px -160px; } +.emoji-1F62E { background-position: -700px -180px; } +.emoji-1F62F { background-position: -700px -200px; } +.emoji-1F630 { background-position: -700px -220px; } +.emoji-1F631 { background-position: -700px -240px; } +.emoji-1F632 { background-position: -700px -260px; } +.emoji-1F633 { background-position: -700px -280px; } +.emoji-1F634 { background-position: -700px -300px; } +.emoji-1F635 { background-position: -700px -320px; } +.emoji-1F636 { background-position: -700px -340px; } +.emoji-1F637 { background-position: -700px -360px; } +.emoji-1F638 { background-position: -700px -380px; } +.emoji-1F639 { background-position: -700px -400px; } +.emoji-1F63A { background-position: -700px -420px; } +.emoji-1F63B { background-position: -700px -440px; } +.emoji-1F63C { background-position: -700px -460px; } +.emoji-1F63D { background-position: -700px -480px; } +.emoji-1F63E { background-position: -700px -500px; } +.emoji-1F63F { background-position: -700px -520px; } +.emoji-1F640 { background-position: -700px -540px; } +.emoji-1F641 { background-position: -700px -560px; } +.emoji-1F642 { background-position: -700px -580px; } +.emoji-1F643 { background-position: -700px -600px; } +.emoji-1F644 { background-position: -700px -620px; } +.emoji-1F645 { background-position: -700px -640px; } +.emoji-1F645-1F3FB { background-position: -700px -660px; } +.emoji-1F645-1F3FC { background-position: -700px -680px; } +.emoji-1F645-1F3FD { background-position: 0 -700px; } +.emoji-1F645-1F3FE { background-position: -20px -700px; } +.emoji-1F645-1F3FF { background-position: -40px -700px; } +.emoji-1F646 { background-position: -60px -700px; } +.emoji-1F646-1F3FB { background-position: -80px -700px; } +.emoji-1F646-1F3FC { background-position: -100px -700px; } +.emoji-1F646-1F3FD { background-position: -120px -700px; } +.emoji-1F646-1F3FE { background-position: -140px -700px; } +.emoji-1F646-1F3FF { background-position: -160px -700px; } +.emoji-1F647 { background-position: -180px -700px; } +.emoji-1F647-1F3FB { background-position: -200px -700px; } +.emoji-1F647-1F3FC { background-position: -220px -700px; } +.emoji-1F647-1F3FD { background-position: -240px -700px; } +.emoji-1F647-1F3FE { background-position: -260px -700px; } +.emoji-1F647-1F3FF { background-position: -280px -700px; } +.emoji-1F648 { background-position: -300px -700px; } +.emoji-1F649 { background-position: -320px -700px; } +.emoji-1F64A { background-position: -340px -700px; } +.emoji-1F64B { background-position: -360px -700px; } +.emoji-1F64B-1F3FB { background-position: -380px -700px; } +.emoji-1F64B-1F3FC { background-position: -400px -700px; } +.emoji-1F64B-1F3FD { background-position: -420px -700px; } +.emoji-1F64B-1F3FE { background-position: -440px -700px; } +.emoji-1F64B-1F3FF { background-position: -460px -700px; } +.emoji-1F64C { background-position: -480px -700px; } +.emoji-1F64C-1F3FB { background-position: -500px -700px; } +.emoji-1F64C-1F3FC { background-position: -520px -700px; } +.emoji-1F64C-1F3FD { background-position: -540px -700px; } +.emoji-1F64C-1F3FE { background-position: -560px -700px; } +.emoji-1F64C-1F3FF { background-position: -580px -700px; } +.emoji-1F64D { background-position: -600px -700px; } +.emoji-1F64D-1F3FB { background-position: -620px -700px; } +.emoji-1F64D-1F3FC { background-position: -640px -700px; } +.emoji-1F64D-1F3FD { background-position: -660px -700px; } +.emoji-1F64D-1F3FE { background-position: -680px -700px; } +.emoji-1F64D-1F3FF { background-position: -700px -700px; } +.emoji-1F64E { background-position: -720px 0; } +.emoji-1F64E-1F3FB { background-position: -720px -20px; } +.emoji-1F64E-1F3FC { background-position: -720px -40px; } +.emoji-1F64E-1F3FD { background-position: -720px -60px; } +.emoji-1F64E-1F3FE { background-position: -720px -80px; } +.emoji-1F64E-1F3FF { background-position: -720px -100px; } +.emoji-1F64F { background-position: -720px -120px; } +.emoji-1F64F-1F3FB { background-position: -720px -140px; } +.emoji-1F64F-1F3FC { background-position: -720px -160px; } +.emoji-1F64F-1F3FD { background-position: -720px -180px; } +.emoji-1F64F-1F3FE { background-position: -720px -200px; } +.emoji-1F64F-1F3FF { background-position: -720px -220px; } +.emoji-1F680 { background-position: -720px -240px; } +.emoji-1F681 { background-position: -720px -260px; } +.emoji-1F682 { background-position: -720px -280px; } +.emoji-1F683 { background-position: -720px -300px; } +.emoji-1F684 { background-position: -720px -320px; } +.emoji-1F685 { background-position: -720px -340px; } +.emoji-1F686 { background-position: -720px -360px; } +.emoji-1F687 { background-position: -720px -380px; } +.emoji-1F688 { background-position: -720px -400px; } +.emoji-1F689 { background-position: -720px -420px; } +.emoji-1F68A { background-position: -720px -440px; } +.emoji-1F68B { background-position: -720px -460px; } +.emoji-1F68C { background-position: -720px -480px; } +.emoji-1F68D { background-position: -720px -500px; } +.emoji-1F68E { background-position: -720px -520px; } +.emoji-1F68F { background-position: -720px -540px; } +.emoji-1F690 { background-position: -720px -560px; } +.emoji-1F691 { background-position: -720px -580px; } +.emoji-1F692 { background-position: -720px -600px; } +.emoji-1F693 { background-position: -720px -620px; } +.emoji-1F694 { background-position: -720px -640px; } +.emoji-1F695 { background-position: -720px -660px; } +.emoji-1F696 { background-position: -720px -680px; } +.emoji-1F697 { background-position: -720px -700px; } +.emoji-1F698 { background-position: 0 -720px; } +.emoji-1F699 { background-position: -20px -720px; } +.emoji-1F69A { background-position: -40px -720px; } +.emoji-1F69B { background-position: -60px -720px; } +.emoji-1F69C { background-position: -80px -720px; } +.emoji-1F69D { background-position: -100px -720px; } +.emoji-1F69E { background-position: -120px -720px; } +.emoji-1F69F { background-position: -140px -720px; } +.emoji-1F6A0 { background-position: -160px -720px; } +.emoji-1F6A1 { background-position: -180px -720px; } +.emoji-1F6A2 { background-position: -200px -720px; } +.emoji-1F6A3 { background-position: -220px -720px; } +.emoji-1F6A3-1F3FB { background-position: -240px -720px; } +.emoji-1F6A3-1F3FC { background-position: -260px -720px; } +.emoji-1F6A3-1F3FD { background-position: -280px -720px; } +.emoji-1F6A3-1F3FE { background-position: -300px -720px; } +.emoji-1F6A3-1F3FF { background-position: -320px -720px; } +.emoji-1F6A4 { background-position: -340px -720px; } +.emoji-1F6A5 { background-position: -360px -720px; } +.emoji-1F6A6 { background-position: -380px -720px; } +.emoji-1F6A7 { background-position: -400px -720px; } +.emoji-1F6A8 { background-position: -420px -720px; } +.emoji-1F6A9 { background-position: -440px -720px; } +.emoji-1F6AA { background-position: -460px -720px; } +.emoji-1F6AB { background-position: -480px -720px; } +.emoji-1F6AC { background-position: -500px -720px; } +.emoji-1F6AD { background-position: -520px -720px; } +.emoji-1F6AE { background-position: -540px -720px; } +.emoji-1F6AF { background-position: -560px -720px; } +.emoji-1F6B0 { background-position: -580px -720px; } +.emoji-1F6B1 { background-position: -600px -720px; } +.emoji-1F6B2 { background-position: -620px -720px; } +.emoji-1F6B3 { background-position: -640px -720px; } +.emoji-1F6B4 { background-position: -660px -720px; } +.emoji-1F6B4-1F3FB { background-position: -680px -720px; } +.emoji-1F6B4-1F3FC { background-position: -700px -720px; } +.emoji-1F6B4-1F3FD { background-position: -720px -720px; } +.emoji-1F6B4-1F3FE { background-position: -740px 0; } +.emoji-1F6B4-1F3FF { background-position: -740px -20px; } +.emoji-1F6B5 { background-position: -740px -40px; } +.emoji-1F6B5-1F3FB { background-position: -740px -60px; } +.emoji-1F6B5-1F3FC { background-position: -740px -80px; } +.emoji-1F6B5-1F3FD { background-position: -740px -100px; } +.emoji-1F6B5-1F3FE { background-position: -740px -120px; } +.emoji-1F6B5-1F3FF { background-position: -740px -140px; } +.emoji-1F6B6 { background-position: -740px -160px; } +.emoji-1F6B6-1F3FB { background-position: -740px -180px; } +.emoji-1F6B6-1F3FC { background-position: -740px -200px; } +.emoji-1F6B6-1F3FD { background-position: -740px -220px; } +.emoji-1F6B6-1F3FE { background-position: -740px -240px; } +.emoji-1F6B6-1F3FF { background-position: -740px -260px; } +.emoji-1F6B7 { background-position: -740px -280px; } +.emoji-1F6B8 { background-position: -740px -300px; } +.emoji-1F6B9 { background-position: -740px -320px; } +.emoji-1F6BA { background-position: -740px -340px; } +.emoji-1F6BB { background-position: -740px -360px; } +.emoji-1F6BC { background-position: -740px -380px; } +.emoji-1F6BD { background-position: -740px -400px; } +.emoji-1F6BE { background-position: -740px -420px; } +.emoji-1F6BF { background-position: -740px -440px; } +.emoji-1F6C0 { background-position: -740px -460px; } +.emoji-1F6C0-1F3FB { background-position: -740px -480px; } +.emoji-1F6C0-1F3FC { background-position: -740px -500px; } +.emoji-1F6C0-1F3FD { background-position: -740px -520px; } +.emoji-1F6C0-1F3FE { background-position: -740px -540px; } +.emoji-1F6C0-1F3FF { background-position: -740px -560px; } +.emoji-1F6C1 { background-position: -740px -580px; } +.emoji-1F6C2 { background-position: -740px -600px; } +.emoji-1F6C3 { background-position: -740px -620px; } +.emoji-1F6C4 { background-position: -740px -640px; } +.emoji-1F6C5 { background-position: -740px -660px; } +.emoji-1F6CB { background-position: -740px -680px; } +.emoji-1F6CC { background-position: -740px -700px; } +.emoji-1F6CD { background-position: -740px -720px; } +.emoji-1F6CE { background-position: 0 -740px; } +.emoji-1F6CF { background-position: -20px -740px; } +.emoji-1F6D0 { background-position: -40px -740px; } +.emoji-1F6D1 { background-position: -60px -740px; } +.emoji-1F6D2 { background-position: -80px -740px; } +.emoji-1F6E0 { background-position: -100px -740px; } +.emoji-1F6E1 { background-position: -120px -740px; } +.emoji-1F6E2 { background-position: -140px -740px; } +.emoji-1F6E3 { background-position: -160px -740px; } +.emoji-1F6E4 { background-position: -180px -740px; } +.emoji-1F6E5 { background-position: -200px -740px; } +.emoji-1F6E9 { background-position: -220px -740px; } +.emoji-1F6EB { background-position: -240px -740px; } +.emoji-1F6EC { background-position: -260px -740px; } +.emoji-1F6F0 { background-position: -280px -740px; } +.emoji-1F6F3 { background-position: -300px -740px; } +.emoji-1F6F4 { background-position: -320px -740px; } +.emoji-1F6F5 { background-position: -340px -740px; } +.emoji-1F6F6 { background-position: -360px -740px; } +.emoji-1F910 { background-position: -380px -740px; } +.emoji-1F911 { background-position: -400px -740px; } +.emoji-1F912 { background-position: -420px -740px; } +.emoji-1F913 { background-position: -440px -740px; } +.emoji-1F914 { background-position: -460px -740px; } +.emoji-1F915 { background-position: -480px -740px; } +.emoji-1F916 { background-position: -500px -740px; } +.emoji-1F917 { background-position: -520px -740px; } +.emoji-1F918 { background-position: -540px -740px; } +.emoji-1F918-1F3FB { background-position: -560px -740px; } +.emoji-1F918-1F3FC { background-position: -580px -740px; } +.emoji-1F918-1F3FD { background-position: -600px -740px; } +.emoji-1F918-1F3FE { background-position: -620px -740px; } +.emoji-1F918-1F3FF { background-position: -640px -740px; } +.emoji-1F919 { background-position: -660px -740px; } +.emoji-1F919-1F3FB { background-position: -680px -740px; } +.emoji-1F919-1F3FC { background-position: -700px -740px; } +.emoji-1F919-1F3FD { background-position: -720px -740px; } +.emoji-1F919-1F3FE { background-position: -740px -740px; } +.emoji-1F919-1F3FF { background-position: -760px 0; } +.emoji-1F91A { background-position: -760px -20px; } +.emoji-1F91A-1F3FB { background-position: -760px -40px; } +.emoji-1F91A-1F3FC { background-position: -760px -60px; } +.emoji-1F91A-1F3FD { background-position: -760px -80px; } +.emoji-1F91A-1F3FE { background-position: -760px -100px; } +.emoji-1F91A-1F3FF { background-position: -760px -120px; } +.emoji-1F91B { background-position: -760px -140px; } +.emoji-1F91B-1F3FB { background-position: -760px -160px; } +.emoji-1F91B-1F3FC { background-position: -760px -180px; } +.emoji-1F91B-1F3FD { background-position: -760px -200px; } +.emoji-1F91B-1F3FE { background-position: -760px -220px; } +.emoji-1F91B-1F3FF { background-position: -760px -240px; } +.emoji-1F91C { background-position: -760px -260px; } +.emoji-1F91C-1F3FB { background-position: -760px -280px; } +.emoji-1F91C-1F3FC { background-position: -760px -300px; } +.emoji-1F91C-1F3FD { background-position: -760px -320px; } +.emoji-1F91C-1F3FE { background-position: -760px -340px; } +.emoji-1F91C-1F3FF { background-position: -760px -360px; } +.emoji-1F91D { background-position: -760px -380px; } +.emoji-1F91D-1F3FB { background-position: -760px -400px; } +.emoji-1F91D-1F3FC { background-position: -760px -420px; } +.emoji-1F91D-1F3FD { background-position: -760px -440px; } +.emoji-1F91D-1F3FE { background-position: -760px -460px; } +.emoji-1F91D-1F3FF { background-position: -760px -480px; } +.emoji-1F91E { background-position: -760px -500px; } +.emoji-1F91E-1F3FB { background-position: -760px -520px; } +.emoji-1F91E-1F3FC { background-position: -760px -540px; } +.emoji-1F91E-1F3FD { background-position: -760px -560px; } +.emoji-1F91E-1F3FE { background-position: -760px -580px; } +.emoji-1F91E-1F3FF { background-position: -760px -600px; } +.emoji-1F920 { background-position: -760px -620px; } +.emoji-1F921 { background-position: -760px -640px; } +.emoji-1F922 { background-position: -760px -660px; } +.emoji-1F923 { background-position: -760px -680px; } +.emoji-1F924 { background-position: -760px -700px; } +.emoji-1F925 { background-position: -760px -720px; } +.emoji-1F926 { background-position: -760px -740px; } +.emoji-1F926-1F3FB { background-position: 0 -760px; } +.emoji-1F926-1F3FC { background-position: -20px -760px; } +.emoji-1F926-1F3FD { background-position: -40px -760px; } +.emoji-1F926-1F3FE { background-position: -60px -760px; } +.emoji-1F926-1F3FF { background-position: -80px -760px; } +.emoji-1F927 { background-position: -100px -760px; } +.emoji-1F930 { background-position: -120px -760px; } +.emoji-1F930-1F3FB { background-position: -140px -760px; } +.emoji-1F930-1F3FC { background-position: -160px -760px; } +.emoji-1F930-1F3FD { background-position: -180px -760px; } +.emoji-1F930-1F3FE { background-position: -200px -760px; } +.emoji-1F930-1F3FF { background-position: -220px -760px; } +.emoji-1F933 { background-position: -240px -760px; } +.emoji-1F933-1F3FB { background-position: -260px -760px; } +.emoji-1F933-1F3FC { background-position: -280px -760px; } +.emoji-1F933-1F3FD { background-position: -300px -760px; } +.emoji-1F933-1F3FE { background-position: -320px -760px; } +.emoji-1F933-1F3FF { background-position: -340px -760px; } +.emoji-1F934 { background-position: -360px -760px; } +.emoji-1F934-1F3FB { background-position: -380px -760px; } +.emoji-1F934-1F3FC { background-position: -400px -760px; } +.emoji-1F934-1F3FD { background-position: -420px -760px; } +.emoji-1F934-1F3FE { background-position: -440px -760px; } +.emoji-1F934-1F3FF { background-position: -460px -760px; } +.emoji-1F935 { background-position: -480px -760px; } +.emoji-1F935-1F3FB { background-position: -500px -760px; } +.emoji-1F935-1F3FC { background-position: -520px -760px; } +.emoji-1F935-1F3FD { background-position: -540px -760px; } +.emoji-1F935-1F3FE { background-position: -560px -760px; } +.emoji-1F935-1F3FF { background-position: -580px -760px; } +.emoji-1F936 { background-position: -600px -760px; } +.emoji-1F936-1F3FB { background-position: -620px -760px; } +.emoji-1F936-1F3FC { background-position: -640px -760px; } +.emoji-1F936-1F3FD { background-position: -660px -760px; } +.emoji-1F936-1F3FE { background-position: -680px -760px; } +.emoji-1F936-1F3FF { background-position: -700px -760px; } +.emoji-1F937 { background-position: -720px -760px; } +.emoji-1F937-1F3FB { background-position: -740px -760px; } +.emoji-1F937-1F3FC { background-position: -760px -760px; } +.emoji-1F937-1F3FD { background-position: -780px 0; } +.emoji-1F937-1F3FE { background-position: -780px -20px; } +.emoji-1F937-1F3FF { background-position: -780px -40px; } +.emoji-1F938 { background-position: -780px -60px; } +.emoji-1F938-1F3FB { background-position: -780px -80px; } +.emoji-1F938-1F3FC { background-position: -780px -100px; } +.emoji-1F938-1F3FD { background-position: -780px -120px; } +.emoji-1F938-1F3FE { background-position: -780px -140px; } +.emoji-1F938-1F3FF { background-position: -780px -160px; } +.emoji-1F939 { background-position: -780px -180px; } +.emoji-1F939-1F3FB { background-position: -780px -200px; } +.emoji-1F939-1F3FC { background-position: -780px -220px; } +.emoji-1F939-1F3FD { background-position: -780px -240px; } +.emoji-1F939-1F3FE { background-position: -780px -260px; } +.emoji-1F939-1F3FF { background-position: -780px -280px; } +.emoji-1F93A { background-position: -780px -300px; } +.emoji-1F93C { background-position: -780px -320px; } +.emoji-1F93C-1F3FB { background-position: -780px -340px; } +.emoji-1F93C-1F3FC { background-position: -780px -360px; } +.emoji-1F93C-1F3FD { background-position: -780px -380px; } +.emoji-1F93C-1F3FE { background-position: -780px -400px; } +.emoji-1F93C-1F3FF { background-position: -780px -420px; } +.emoji-1F93D { background-position: -780px -440px; } +.emoji-1F93D-1F3FB { background-position: -780px -460px; } +.emoji-1F93D-1F3FC { background-position: -780px -480px; } +.emoji-1F93D-1F3FD { background-position: -780px -500px; } +.emoji-1F93D-1F3FE { background-position: -780px -520px; } +.emoji-1F93D-1F3FF { background-position: -780px -540px; } +.emoji-1F93E { background-position: -780px -560px; } +.emoji-1F93E-1F3FB { background-position: -780px -580px; } +.emoji-1F93E-1F3FC { background-position: -780px -600px; } +.emoji-1F93E-1F3FD { background-position: -780px -620px; } +.emoji-1F93E-1F3FE { background-position: -780px -640px; } +.emoji-1F93E-1F3FF { background-position: -780px -660px; } +.emoji-1F940 { background-position: -780px -680px; } +.emoji-1F941 { background-position: -780px -700px; } +.emoji-1F942 { background-position: -780px -720px; } +.emoji-1F943 { background-position: -780px -740px; } +.emoji-1F944 { background-position: -780px -760px; } +.emoji-1F945 { background-position: 0 -780px; } +.emoji-1F947 { background-position: -20px -780px; } +.emoji-1F948 { background-position: -40px -780px; } +.emoji-1F949 { background-position: -60px -780px; } +.emoji-1F94A { background-position: -80px -780px; } +.emoji-1F94B { background-position: -100px -780px; } +.emoji-1F950 { background-position: -120px -780px; } +.emoji-1F951 { background-position: -140px -780px; } +.emoji-1F952 { background-position: -160px -780px; } +.emoji-1F953 { background-position: -180px -780px; } +.emoji-1F954 { background-position: -200px -780px; } +.emoji-1F955 { background-position: -220px -780px; } +.emoji-1F956 { background-position: -240px -780px; } +.emoji-1F957 { background-position: -260px -780px; } +.emoji-1F958 { background-position: -280px -780px; } +.emoji-1F959 { background-position: -300px -780px; } +.emoji-1F95A { background-position: -320px -780px; } +.emoji-1F95B { background-position: -340px -780px; } +.emoji-1F95C { background-position: -360px -780px; } +.emoji-1F95D { background-position: -380px -780px; } +.emoji-1F95E { background-position: -400px -780px; } +.emoji-1F980 { background-position: -420px -780px; } +.emoji-1F981 { background-position: -440px -780px; } +.emoji-1F982 { background-position: -460px -780px; } +.emoji-1F983 { background-position: -480px -780px; } +.emoji-1F984 { background-position: -500px -780px; } +.emoji-1F985 { background-position: -520px -780px; } +.emoji-1F986 { background-position: -540px -780px; } +.emoji-1F987 { background-position: -560px -780px; } +.emoji-1F988 { background-position: -580px -780px; } +.emoji-1F989 { background-position: -600px -780px; } +.emoji-1F98A { background-position: -620px -780px; } +.emoji-1F98B { background-position: -640px -780px; } +.emoji-1F98C { background-position: -660px -780px; } +.emoji-1F98D { background-position: -680px -780px; } +.emoji-1F98E { background-position: -700px -780px; } +.emoji-1F98F { background-position: -720px -780px; } +.emoji-1F990 { background-position: -740px -780px; } +.emoji-1F991 { background-position: -760px -780px; } +.emoji-1F9C0 { background-position: -780px -780px; } +.emoji-203C { background-position: -800px 0; } +.emoji-2049 { background-position: -800px -20px; } +.emoji-2122 { background-position: -800px -40px; } +.emoji-2139 { background-position: -800px -60px; } +.emoji-2194 { background-position: -800px -80px; } +.emoji-2195 { background-position: -800px -100px; } +.emoji-2196 { background-position: -800px -120px; } +.emoji-2197 { background-position: -800px -140px; } +.emoji-2198 { background-position: -800px -160px; } +.emoji-2199 { background-position: -800px -180px; } +.emoji-21A9 { background-position: -800px -200px; } +.emoji-21AA { background-position: -800px -220px; } +.emoji-231A { background-position: -800px -240px; } +.emoji-231B { background-position: -800px -260px; } +.emoji-2328 { background-position: -800px -280px; } +.emoji-23CF { background-position: -800px -300px; } +.emoji-23E9 { background-position: -800px -320px; } +.emoji-23EA { background-position: -800px -340px; } +.emoji-23EB { background-position: -800px -360px; } +.emoji-23EC { background-position: -800px -380px; } +.emoji-23ED { background-position: -800px -400px; } +.emoji-23EE { background-position: -800px -420px; } +.emoji-23EF { background-position: -800px -440px; } +.emoji-23F0 { background-position: -800px -460px; } +.emoji-23F1 { background-position: -800px -480px; } +.emoji-23F2 { background-position: -800px -500px; } +.emoji-23F3 { background-position: -800px -520px; } +.emoji-23F8 { background-position: -800px -540px; } +.emoji-23F9 { background-position: -800px -560px; } +.emoji-23FA { background-position: -800px -580px; } +.emoji-24C2 { background-position: -800px -600px; } +.emoji-25AA { background-position: -800px -620px; } +.emoji-25AB { background-position: -800px -640px; } +.emoji-25B6 { background-position: -800px -660px; } +.emoji-25C0 { background-position: -800px -680px; } +.emoji-25FB { background-position: -800px -700px; } +.emoji-25FC { background-position: -800px -720px; } +.emoji-25FD { background-position: -800px -740px; } +.emoji-25FE { background-position: -800px -760px; } +.emoji-2600 { background-position: -800px -780px; } +.emoji-2601 { background-position: 0 -800px; } +.emoji-2602 { background-position: -20px -800px; } +.emoji-2603 { background-position: -40px -800px; } +.emoji-2604 { background-position: -60px -800px; } +.emoji-260E { background-position: -80px -800px; } +.emoji-2611 { background-position: -100px -800px; } +.emoji-2614 { background-position: -120px -800px; } +.emoji-2615 { background-position: -140px -800px; } +.emoji-2618 { background-position: -160px -800px; } +.emoji-261D { background-position: -180px -800px; } +.emoji-261D-1F3FB { background-position: -200px -800px; } +.emoji-261D-1F3FC { background-position: -220px -800px; } +.emoji-261D-1F3FD { background-position: -240px -800px; } +.emoji-261D-1F3FE { background-position: -260px -800px; } +.emoji-261D-1F3FF { background-position: -280px -800px; } +.emoji-2620 { background-position: -300px -800px; } +.emoji-2622 { background-position: -320px -800px; } +.emoji-2623 { background-position: -340px -800px; } +.emoji-2626 { background-position: -360px -800px; } +.emoji-262A { background-position: -380px -800px; } +.emoji-262E { background-position: -400px -800px; } +.emoji-262F { background-position: -420px -800px; } +.emoji-2638 { background-position: -440px -800px; } +.emoji-2639 { background-position: -460px -800px; } +.emoji-263A { background-position: -480px -800px; } +.emoji-2648 { background-position: -500px -800px; } +.emoji-2649 { background-position: -520px -800px; } +.emoji-264A { background-position: -540px -800px; } +.emoji-264B { background-position: -560px -800px; } +.emoji-264C { background-position: -580px -800px; } +.emoji-264D { background-position: -600px -800px; } +.emoji-264E { background-position: -620px -800px; } +.emoji-264F { background-position: -640px -800px; } +.emoji-2650 { background-position: -660px -800px; } +.emoji-2651 { background-position: -680px -800px; } +.emoji-2652 { background-position: -700px -800px; } +.emoji-2653 { background-position: -720px -800px; } +.emoji-2660 { background-position: -740px -800px; } +.emoji-2663 { background-position: -760px -800px; } +.emoji-2665 { background-position: -780px -800px; } +.emoji-2666 { background-position: -800px -800px; } +.emoji-2668 { background-position: -820px 0; } +.emoji-267B { background-position: -820px -20px; } +.emoji-267F { background-position: -820px -40px; } +.emoji-2692 { background-position: -820px -60px; } +.emoji-2693 { background-position: -820px -80px; } +.emoji-2694 { background-position: -820px -100px; } +.emoji-2696 { background-position: -820px -120px; } +.emoji-2697 { background-position: -820px -140px; } +.emoji-2699 { background-position: -820px -160px; } +.emoji-269B { background-position: -820px -180px; } +.emoji-269C { background-position: -820px -200px; } +.emoji-26A0 { background-position: -820px -220px; } +.emoji-26A1 { background-position: -820px -240px; } +.emoji-26AA { background-position: -820px -260px; } +.emoji-26AB { background-position: -820px -280px; } +.emoji-26B0 { background-position: -820px -300px; } +.emoji-26B1 { background-position: -820px -320px; } +.emoji-26BD { background-position: -820px -340px; } +.emoji-26BE { background-position: -820px -360px; } +.emoji-26C4 { background-position: -820px -380px; } +.emoji-26C5 { background-position: -820px -400px; } +.emoji-26C8 { background-position: -820px -420px; } +.emoji-26CE { background-position: -820px -440px; } +.emoji-26CF { background-position: -820px -460px; } +.emoji-26D1 { background-position: -820px -480px; } +.emoji-26D3 { background-position: -820px -500px; } +.emoji-26D4 { background-position: -820px -520px; } +.emoji-26E9 { background-position: -820px -540px; } +.emoji-26EA { background-position: -820px -560px; } +.emoji-26F0 { background-position: -820px -580px; } +.emoji-26F1 { background-position: -820px -600px; } +.emoji-26F2 { background-position: -820px -620px; } +.emoji-26F3 { background-position: -820px -640px; } +.emoji-26F4 { background-position: -820px -660px; } +.emoji-26F5 { background-position: -820px -680px; } +.emoji-26F7 { background-position: -820px -700px; } +.emoji-26F8 { background-position: -820px -720px; } +.emoji-26F9 { background-position: -820px -740px; } +.emoji-26F9-1F3FB { background-position: -820px -760px; } +.emoji-26F9-1F3FC { background-position: -820px -780px; } +.emoji-26F9-1F3FD { background-position: -820px -800px; } +.emoji-26F9-1F3FE { background-position: 0 -820px; } +.emoji-26F9-1F3FF { background-position: -20px -820px; } +.emoji-26FA { background-position: -40px -820px; } +.emoji-26FD { background-position: -60px -820px; } +.emoji-2702 { background-position: -80px -820px; } +.emoji-2705 { background-position: -100px -820px; } +.emoji-2708 { background-position: -120px -820px; } +.emoji-2709 { background-position: -140px -820px; } +.emoji-270A { background-position: -160px -820px; } +.emoji-270A-1F3FB { background-position: -180px -820px; } +.emoji-270A-1F3FC { background-position: -200px -820px; } +.emoji-270A-1F3FD { background-position: -220px -820px; } +.emoji-270A-1F3FE { background-position: -240px -820px; } +.emoji-270A-1F3FF { background-position: -260px -820px; } +.emoji-270B { background-position: -280px -820px; } +.emoji-270B-1F3FB { background-position: -300px -820px; } +.emoji-270B-1F3FC { background-position: -320px -820px; } +.emoji-270B-1F3FD { background-position: -340px -820px; } +.emoji-270B-1F3FE { background-position: -360px -820px; } +.emoji-270B-1F3FF { background-position: -380px -820px; } +.emoji-270C { background-position: -400px -820px; } +.emoji-270C-1F3FB { background-position: -420px -820px; } +.emoji-270C-1F3FC { background-position: -440px -820px; } +.emoji-270C-1F3FD { background-position: -460px -820px; } +.emoji-270C-1F3FE { background-position: -480px -820px; } +.emoji-270C-1F3FF { background-position: -500px -820px; } +.emoji-270D { background-position: -520px -820px; } +.emoji-270D-1F3FB { background-position: -540px -820px; } +.emoji-270D-1F3FC { background-position: -560px -820px; } +.emoji-270D-1F3FD { background-position: -580px -820px; } +.emoji-270D-1F3FE { background-position: -600px -820px; } +.emoji-270D-1F3FF { background-position: -620px -820px; } +.emoji-270F { background-position: -640px -820px; } +.emoji-2712 { background-position: -660px -820px; } +.emoji-2714 { background-position: -680px -820px; } +.emoji-2716 { background-position: -700px -820px; } +.emoji-271D { background-position: -720px -820px; } +.emoji-2721 { background-position: -740px -820px; } +.emoji-2728 { background-position: -760px -820px; } +.emoji-2733 { background-position: -780px -820px; } +.emoji-2734 { background-position: -800px -820px; } +.emoji-2744 { background-position: -820px -820px; } +.emoji-2747 { background-position: -840px 0; } +.emoji-274C { background-position: -840px -20px; } +.emoji-274E { background-position: -840px -40px; } +.emoji-2753 { background-position: -840px -60px; } +.emoji-2754 { background-position: -840px -80px; } +.emoji-2755 { background-position: -840px -100px; } +.emoji-2757 { background-position: -840px -120px; } +.emoji-2763 { background-position: -840px -140px; } +.emoji-2764 { background-position: -840px -160px; } +.emoji-2795 { background-position: -840px -180px; } +.emoji-2796 { background-position: -840px -200px; } +.emoji-2797 { background-position: -840px -220px; } +.emoji-27A1 { background-position: -840px -240px; } +.emoji-27B0 { background-position: -840px -260px; } +.emoji-27BF { background-position: -840px -280px; } +.emoji-2934 { background-position: -840px -300px; } +.emoji-2935 { background-position: -840px -320px; } +.emoji-2B05 { background-position: -840px -340px; } +.emoji-2B06 { background-position: -840px -360px; } +.emoji-2B07 { background-position: -840px -380px; } +.emoji-2B1B { background-position: -840px -400px; } +.emoji-2B1C { background-position: -840px -420px; } +.emoji-2B50 { background-position: -840px -440px; } +.emoji-2B55 { background-position: -840px -460px; } +.emoji-3030 { background-position: -840px -480px; } +.emoji-303D { background-position: -840px -500px; } +.emoji-3297 { background-position: -840px -520px; } +.emoji-3299 { background-position: -840px -540px; } + +.emoji-icon { + background-image: image-url('emoji.png'); + background-repeat: no-repeat; + height: 20px; + width: 20px; + + @media only screen and (-webkit-min-device-pixel-ratio: 2), + only screen and (min--moz-device-pixel-ratio: 2), + only screen and (-o-min-device-pixel-ratio: 2/1), + only screen and (min-device-pixel-ratio: 2), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 2dppx) { + background-image: image-url('emoji@2x.png'); + background-size: 860px 840px; + } +} diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss new file mode 100644 index 00000000000..226bd2ead31 --- /dev/null +++ b/app/assets/stylesheets/framework/icons.scss @@ -0,0 +1,51 @@ +.ci-status-icon-success { + color: $gl-success; + + svg { + fill: $gl-success; + } +} + +.ci-status-icon-failed { + color: $gl-danger; + + svg { + fill: $gl-danger; + } +} + +.ci-status-icon-pending, +.ci-status-icon-success_with_warnings { + color: $gl-warning; + + svg { + fill: $gl-warning; + } +} + +.ci-status-icon-running { + color: $blue-normal; + + svg { + fill: $blue-normal; + } +} + +.ci-status-icon-canceled, +.ci-status-icon-disabled, +.ci-status-icon-not-found { + color: $gl-gray; + + svg { + fill: $gl-gray; + } +} + +.ci-status-icon-created, +.ci-status-icon-skipped { + color: $gray-darkest; + + svg { + fill: $gray-darkest; + } +} diff --git a/app/assets/stylesheets/pages/emojis.scss b/app/assets/stylesheets/pages/emojis.scss deleted file mode 100644 index f17797b2381..00000000000 --- a/app/assets/stylesheets/pages/emojis.scss +++ /dev/null @@ -1,1809 +0,0 @@ -.emoji-0023-20E3 { background-position: 0 0px; } -.emoji-002A-20E3 { background-position: -20px 0; } -.emoji-0030-20E3 { background-position: 0 -20px; } -.emoji-0031-20E3 { background-position: -20px -20px; } -.emoji-0032-20E3 { background-position: -40px 0; } -.emoji-0033-20E3 { background-position: -40px -20px; } -.emoji-0034-20E3 { background-position: 0 -40px; } -.emoji-0035-20E3 { background-position: -20px -40px; } -.emoji-0036-20E3 { background-position: -40px -40px; } -.emoji-0037-20E3 { background-position: -60px 0; } -.emoji-0038-20E3 { background-position: -60px -20px; } -.emoji-0039-20E3 { background-position: -60px -40px; } -.emoji-00A9 { background-position: 0 -60px; } -.emoji-00AE { background-position: -20px -60px; } -.emoji-1F004 { background-position: -40px -60px; } -.emoji-1F0CF { background-position: -60px -60px; } -.emoji-1F170 { background-position: -80px 0; } -.emoji-1F171 { background-position: -80px -20px; } -.emoji-1F17E { background-position: -80px -40px; } -.emoji-1F17F { background-position: -80px -60px; } -.emoji-1F18E { background-position: 0 -80px; } -.emoji-1F191 { background-position: -20px -80px; } -.emoji-1F192 { background-position: -40px -80px; } -.emoji-1F193 { background-position: -60px -80px; } -.emoji-1F194 { background-position: -80px -80px; } -.emoji-1F195 { background-position: -100px 0; } -.emoji-1F196 { background-position: -100px -20px; } -.emoji-1F197 { background-position: -100px -40px; } -.emoji-1F198 { background-position: -100px -60px; } -.emoji-1F199 { background-position: -100px -80px; } -.emoji-1F19A { background-position: 0 -100px; } -.emoji-1F1E6-1F1E8 { background-position: -20px -100px; } -.emoji-1F1E6-1F1E9 { background-position: -40px -100px; } -.emoji-1F1E6-1F1EA { background-position: -60px -100px; } -.emoji-1F1E6-1F1EB { background-position: -80px -100px; } -.emoji-1F1E6-1F1EC { background-position: -100px -100px; } -.emoji-1F1E6-1F1EE { background-position: -120px 0; } -.emoji-1F1E6-1F1F1 { background-position: -120px -20px; } -.emoji-1F1E6-1F1F2 { background-position: -120px -40px; } -.emoji-1F1E6-1F1F4 { background-position: -120px -60px; } -.emoji-1F1E6-1F1F6 { background-position: -120px -80px; } -.emoji-1F1E6-1F1F7 { background-position: -120px -100px; } -.emoji-1F1E6-1F1F8 { background-position: 0 -120px; } -.emoji-1F1E6-1F1F9 { background-position: -20px -120px; } -.emoji-1F1E6-1F1FA { background-position: -40px -120px; } -.emoji-1F1E6-1F1FC { background-position: -60px -120px; } -.emoji-1F1E6-1F1FD { background-position: -80px -120px; } -.emoji-1F1E6-1F1FF { background-position: -100px -120px; } -.emoji-1F1E7-1F1E6 { background-position: -120px -120px; } -.emoji-1F1E7-1F1E7 { background-position: -140px 0; } -.emoji-1F1E7-1F1E9 { background-position: -140px -20px; } -.emoji-1F1E7-1F1EA { background-position: -140px -40px; } -.emoji-1F1E7-1F1EB { background-position: -140px -60px; } -.emoji-1F1E7-1F1EC { background-position: -140px -80px; } -.emoji-1F1E7-1F1ED { background-position: -140px -100px; } -.emoji-1F1E7-1F1EE { background-position: -140px -120px; } -.emoji-1F1E7-1F1EF { background-position: 0 -140px; } -.emoji-1F1E7-1F1F1 { background-position: -20px -140px; } -.emoji-1F1E7-1F1F2 { background-position: -40px -140px; } -.emoji-1F1E7-1F1F3 { background-position: -60px -140px; } -.emoji-1F1E7-1F1F4 { background-position: -80px -140px; } -.emoji-1F1E7-1F1F6 { background-position: -100px -140px; } -.emoji-1F1E7-1F1F7 { background-position: -120px -140px; } -.emoji-1F1E7-1F1F8 { background-position: -140px -140px; } -.emoji-1F1E7-1F1F9 { background-position: -160px 0; } -.emoji-1F1E7-1F1FB { background-position: -160px -20px; } -.emoji-1F1E7-1F1FC { background-position: -160px -40px; } -.emoji-1F1E7-1F1FE { background-position: -160px -60px; } -.emoji-1F1E7-1F1FF { background-position: -160px -80px; } -.emoji-1F1E8-1F1E6 { background-position: -160px -100px; } -.emoji-1F1E8-1F1E8 { background-position: -160px -120px; } -.emoji-1F1E8-1F1E9 { background-position: -160px -140px; } -.emoji-1F1E8-1F1EB { background-position: 0 -160px; } -.emoji-1F1E8-1F1EC { background-position: -20px -160px; } -.emoji-1F1E8-1F1ED { background-position: -40px -160px; } -.emoji-1F1E8-1F1EE { background-position: -60px -160px; } -.emoji-1F1E8-1F1F0 { background-position: -80px -160px; } -.emoji-1F1E8-1F1F1 { background-position: -100px -160px; } -.emoji-1F1E8-1F1F2 { background-position: -120px -160px; } -.emoji-1F1E8-1F1F3 { background-position: -140px -160px; } -.emoji-1F1E8-1F1F4 { background-position: -160px -160px; } -.emoji-1F1E8-1F1F5 { background-position: -180px 0; } -.emoji-1F1E8-1F1F7 { background-position: -180px -20px; } -.emoji-1F1E8-1F1FA { background-position: -180px -40px; } -.emoji-1F1E8-1F1FB { background-position: -180px -60px; } -.emoji-1F1E8-1F1FC { background-position: -180px -80px; } -.emoji-1F1E8-1F1FD { background-position: -180px -100px; } -.emoji-1F1E8-1F1FE { background-position: -180px -120px; } -.emoji-1F1E8-1F1FF { background-position: -180px -140px; } -.emoji-1F1E9-1F1EA { background-position: -180px -160px; } -.emoji-1F1E9-1F1EC { background-position: 0 -180px; } -.emoji-1F1E9-1F1EF { background-position: -20px -180px; } -.emoji-1F1E9-1F1F0 { background-position: -40px -180px; } -.emoji-1F1E9-1F1F2 { background-position: -60px -180px; } -.emoji-1F1E9-1F1F4 { background-position: -80px -180px; } -.emoji-1F1E9-1F1FF { background-position: -100px -180px; } -.emoji-1F1EA-1F1E6 { background-position: -120px -180px; } -.emoji-1F1EA-1F1E8 { background-position: -140px -180px; } -.emoji-1F1EA-1F1EA { background-position: -160px -180px; } -.emoji-1F1EA-1F1EC { background-position: -180px -180px; } -.emoji-1F1EA-1F1ED { background-position: -200px 0; } -.emoji-1F1EA-1F1F7 { background-position: -200px -20px; } -.emoji-1F1EA-1F1F8 { background-position: -200px -40px; } -.emoji-1F1EA-1F1F9 { background-position: -200px -60px; } -.emoji-1F1EA-1F1FA { background-position: -200px -80px; } -.emoji-1F1EB-1F1EE { background-position: -200px -100px; } -.emoji-1F1EB-1F1EF { background-position: -200px -120px; } -.emoji-1F1EB-1F1F0 { background-position: -200px -140px; } -.emoji-1F1EB-1F1F2 { background-position: -200px -160px; } -.emoji-1F1EB-1F1F4 { background-position: -200px -180px; } -.emoji-1F1EB-1F1F7 { background-position: 0 -200px; } -.emoji-1F1EC-1F1E6 { background-position: -20px -200px; } -.emoji-1F1EC-1F1E7 { background-position: -40px -200px; } -.emoji-1F1EC-1F1E9 { background-position: -60px -200px; } -.emoji-1F1EC-1F1EA { background-position: -80px -200px; } -.emoji-1F1EC-1F1EB { background-position: -100px -200px; } -.emoji-1F1EC-1F1EC { background-position: -120px -200px; } -.emoji-1F1EC-1F1ED { background-position: -140px -200px; } -.emoji-1F1EC-1F1EE { background-position: -160px -200px; } -.emoji-1F1EC-1F1F1 { background-position: -180px -200px; } -.emoji-1F1EC-1F1F2 { background-position: -200px -200px; } -.emoji-1F1EC-1F1F3 { background-position: -220px 0; } -.emoji-1F1EC-1F1F5 { background-position: -220px -20px; } -.emoji-1F1EC-1F1F6 { background-position: -220px -40px; } -.emoji-1F1EC-1F1F7 { background-position: -220px -60px; } -.emoji-1F1EC-1F1F8 { background-position: -220px -80px; } -.emoji-1F1EC-1F1F9 { background-position: -220px -100px; } -.emoji-1F1EC-1F1FA { background-position: -220px -120px; } -.emoji-1F1EC-1F1FC { background-position: -220px -140px; } -.emoji-1F1EC-1F1FE { background-position: -220px -160px; } -.emoji-1F1ED-1F1F0 { background-position: -220px -180px; } -.emoji-1F1ED-1F1F2 { background-position: -220px -200px; } -.emoji-1F1ED-1F1F3 { background-position: 0 -220px; } -.emoji-1F1ED-1F1F7 { background-position: -20px -220px; } -.emoji-1F1ED-1F1F9 { background-position: -40px -220px; } -.emoji-1F1ED-1F1FA { background-position: -60px -220px; } -.emoji-1F1EE-1F1E8 { background-position: -80px -220px; } -.emoji-1F1EE-1F1E9 { background-position: -100px -220px; } -.emoji-1F1EE-1F1EA { background-position: -120px -220px; } -.emoji-1F1EE-1F1F1 { background-position: -140px -220px; } -.emoji-1F1EE-1F1F2 { background-position: -160px -220px; } -.emoji-1F1EE-1F1F3 { background-position: -180px -220px; } -.emoji-1F1EE-1F1F4 { background-position: -200px -220px; } -.emoji-1F1EE-1F1F6 { background-position: -220px -220px; } -.emoji-1F1EE-1F1F7 { background-position: -240px 0; } -.emoji-1F1EE-1F1F8 { background-position: -240px -20px; } -.emoji-1F1EE-1F1F9 { background-position: -240px -40px; } -.emoji-1F1EF-1F1EA { background-position: -240px -60px; } -.emoji-1F1EF-1F1F2 { background-position: -240px -80px; } -.emoji-1F1EF-1F1F4 { background-position: -240px -100px; } -.emoji-1F1EF-1F1F5 { background-position: -240px -120px; } -.emoji-1F1F0-1F1EA { background-position: -240px -140px; } -.emoji-1F1F0-1F1EC { background-position: -240px -160px; } -.emoji-1F1F0-1F1ED { background-position: -240px -180px; } -.emoji-1F1F0-1F1EE { background-position: -240px -200px; } -.emoji-1F1F0-1F1F2 { background-position: -240px -220px; } -.emoji-1F1F0-1F1F3 { background-position: 0 -240px; } -.emoji-1F1F0-1F1F5 { background-position: -20px -240px; } -.emoji-1F1F0-1F1F7 { background-position: -40px -240px; } -.emoji-1F1F0-1F1FC { background-position: -60px -240px; } -.emoji-1F1F0-1F1FE { background-position: -80px -240px; } -.emoji-1F1F0-1F1FF { background-position: -100px -240px; } -.emoji-1F1F1-1F1E6 { background-position: -120px -240px; } -.emoji-1F1F1-1F1E7 { background-position: -140px -240px; } -.emoji-1F1F1-1F1E8 { background-position: -160px -240px; } -.emoji-1F1F1-1F1EE { background-position: -180px -240px; } -.emoji-1F1F1-1F1F0 { background-position: -200px -240px; } -.emoji-1F1F1-1F1F7 { background-position: -220px -240px; } -.emoji-1F1F1-1F1F8 { background-position: -240px -240px; } -.emoji-1F1F1-1F1F9 { background-position: -260px 0; } -.emoji-1F1F1-1F1FA { background-position: -260px -20px; } -.emoji-1F1F1-1F1FB { background-position: -260px -40px; } -.emoji-1F1F1-1F1FE { background-position: -260px -60px; } -.emoji-1F1F2-1F1E6 { background-position: -260px -80px; } -.emoji-1F1F2-1F1E8 { background-position: -260px -100px; } -.emoji-1F1F2-1F1E9 { background-position: -260px -120px; } -.emoji-1F1F2-1F1EA { background-position: -260px -140px; } -.emoji-1F1F2-1F1EB { background-position: -260px -160px; } -.emoji-1F1F2-1F1EC { background-position: -260px -180px; } -.emoji-1F1F2-1F1ED { background-position: -260px -200px; } -.emoji-1F1F2-1F1F0 { background-position: -260px -220px; } -.emoji-1F1F2-1F1F1 { background-position: -260px -240px; } -.emoji-1F1F2-1F1F2 { background-position: 0 -260px; } -.emoji-1F1F2-1F1F3 { background-position: -20px -260px; } -.emoji-1F1F2-1F1F4 { background-position: -40px -260px; } -.emoji-1F1F2-1F1F5 { background-position: -60px -260px; } -.emoji-1F1F2-1F1F6 { background-position: -80px -260px; } -.emoji-1F1F2-1F1F7 { background-position: -100px -260px; } -.emoji-1F1F2-1F1F8 { background-position: -120px -260px; } -.emoji-1F1F2-1F1F9 { background-position: -140px -260px; } -.emoji-1F1F2-1F1FA { background-position: -160px -260px; } -.emoji-1F1F2-1F1FB { background-position: -180px -260px; } -.emoji-1F1F2-1F1FC { background-position: -200px -260px; } -.emoji-1F1F2-1F1FD { background-position: -220px -260px; } -.emoji-1F1F2-1F1FE { background-position: -240px -260px; } -.emoji-1F1F2-1F1FF { background-position: -260px -260px; } -.emoji-1F1F3-1F1E6 { background-position: -280px 0; } -.emoji-1F1F3-1F1E8 { background-position: -280px -20px; } -.emoji-1F1F3-1F1EA { background-position: -280px -40px; } -.emoji-1F1F3-1F1EB { background-position: -280px -60px; } -.emoji-1F1F3-1F1EC { background-position: -280px -80px; } -.emoji-1F1F3-1F1EE { background-position: -280px -100px; } -.emoji-1F1F3-1F1F1 { background-position: -280px -120px; } -.emoji-1F1F3-1F1F4 { background-position: -280px -140px; } -.emoji-1F1F3-1F1F5 { background-position: -280px -160px; } -.emoji-1F1F3-1F1F7 { background-position: -280px -180px; } -.emoji-1F1F3-1F1FA { background-position: -280px -200px; } -.emoji-1F1F3-1F1FF { background-position: -280px -220px; } -.emoji-1F1F4-1F1F2 { background-position: -280px -240px; } -.emoji-1F1F5-1F1E6 { background-position: -280px -260px; } -.emoji-1F1F5-1F1EA { background-position: 0 -280px; } -.emoji-1F1F5-1F1EB { background-position: -20px -280px; } -.emoji-1F1F5-1F1EC { background-position: -40px -280px; } -.emoji-1F1F5-1F1ED { background-position: -60px -280px; } -.emoji-1F1F5-1F1F0 { background-position: -80px -280px; } -.emoji-1F1F5-1F1F1 { background-position: -100px -280px; } -.emoji-1F1F5-1F1F2 { background-position: -120px -280px; } -.emoji-1F1F5-1F1F3 { background-position: -140px -280px; } -.emoji-1F1F5-1F1F7 { background-position: -160px -280px; } -.emoji-1F1F5-1F1F8 { background-position: -180px -280px; } -.emoji-1F1F5-1F1F9 { background-position: -200px -280px; } -.emoji-1F1F5-1F1FC { background-position: -220px -280px; } -.emoji-1F1F5-1F1FE { background-position: -240px -280px; } -.emoji-1F1F6-1F1E6 { background-position: -260px -280px; } -.emoji-1F1F7-1F1EA { background-position: -280px -280px; } -.emoji-1F1F7-1F1F4 { background-position: -300px 0; } -.emoji-1F1F7-1F1F8 { background-position: -300px -20px; } -.emoji-1F1F7-1F1FA { background-position: -300px -40px; } -.emoji-1F1F7-1F1FC { background-position: -300px -60px; } -.emoji-1F1F8-1F1E6 { background-position: -300px -80px; } -.emoji-1F1F8-1F1E7 { background-position: -300px -100px; } -.emoji-1F1F8-1F1E8 { background-position: -300px -120px; } -.emoji-1F1F8-1F1E9 { background-position: -300px -140px; } -.emoji-1F1F8-1F1EA { background-position: -300px -160px; } -.emoji-1F1F8-1F1EC { background-position: -300px -180px; } -.emoji-1F1F8-1F1ED { background-position: -300px -200px; } -.emoji-1F1F8-1F1EE { background-position: -300px -220px; } -.emoji-1F1F8-1F1EF { background-position: -300px -240px; } -.emoji-1F1F8-1F1F0 { background-position: -300px -260px; } -.emoji-1F1F8-1F1F1 { background-position: -300px -280px; } -.emoji-1F1F8-1F1F2 { background-position: 0 -300px; } -.emoji-1F1F8-1F1F3 { background-position: -20px -300px; } -.emoji-1F1F8-1F1F4 { background-position: -40px -300px; } -.emoji-1F1F8-1F1F7 { background-position: -60px -300px; } -.emoji-1F1F8-1F1F8 { background-position: -80px -300px; } -.emoji-1F1F8-1F1F9 { background-position: -100px -300px; } -.emoji-1F1F8-1F1FB { background-position: -120px -300px; } -.emoji-1F1F8-1F1FD { background-position: -140px -300px; } -.emoji-1F1F8-1F1FE { background-position: -160px -300px; } -.emoji-1F1F8-1F1FF { background-position: -180px -300px; } -.emoji-1F1F9-1F1E6 { background-position: -200px -300px; } -.emoji-1F1F9-1F1E8 { background-position: -220px -300px; } -.emoji-1F1F9-1F1E9 { background-position: -240px -300px; } -.emoji-1F1F9-1F1EB { background-position: -260px -300px; } -.emoji-1F1F9-1F1EC { background-position: -280px -300px; } -.emoji-1F1F9-1F1ED { background-position: -300px -300px; } -.emoji-1F1F9-1F1EF { background-position: -320px 0; } -.emoji-1F1F9-1F1F0 { background-position: -320px -20px; } -.emoji-1F1F9-1F1F1 { background-position: -320px -40px; } -.emoji-1F1F9-1F1F2 { background-position: -320px -60px; } -.emoji-1F1F9-1F1F3 { background-position: -320px -80px; } -.emoji-1F1F9-1F1F4 { background-position: -320px -100px; } -.emoji-1F1F9-1F1F7 { background-position: -320px -120px; } -.emoji-1F1F9-1F1F9 { background-position: -320px -140px; } -.emoji-1F1F9-1F1FB { background-position: -320px -160px; } -.emoji-1F1F9-1F1FC { background-position: -320px -180px; } -.emoji-1F1F9-1F1FF { background-position: -320px -200px; } -.emoji-1F1FA-1F1E6 { background-position: -320px -220px; } -.emoji-1F1FA-1F1EC { background-position: -320px -240px; } -.emoji-1F1FA-1F1F2 { background-position: -320px -260px; } -.emoji-1F1FA-1F1F8 { background-position: -320px -280px; } -.emoji-1F1FA-1F1FE { background-position: -320px -300px; } -.emoji-1F1FA-1F1FF { background-position: 0 -320px; } -.emoji-1F1FB-1F1E6 { background-position: -20px -320px; } -.emoji-1F1FB-1F1E8 { background-position: -40px -320px; } -.emoji-1F1FB-1F1EA { background-position: -60px -320px; } -.emoji-1F1FB-1F1EC { background-position: -80px -320px; } -.emoji-1F1FB-1F1EE { background-position: -100px -320px; } -.emoji-1F1FB-1F1F3 { background-position: -120px -320px; } -.emoji-1F1FB-1F1FA { background-position: -140px -320px; } -.emoji-1F1FC-1F1EB { background-position: -160px -320px; } -.emoji-1F1FC-1F1F8 { background-position: -180px -320px; } -.emoji-1F1FD-1F1F0 { background-position: -200px -320px; } -.emoji-1F1FE-1F1EA { background-position: -220px -320px; } -.emoji-1F1FE-1F1F9 { background-position: -240px -320px; } -.emoji-1F1FF-1F1E6 { background-position: -260px -320px; } -.emoji-1F1FF-1F1F2 { background-position: -280px -320px; } -.emoji-1F1FF-1F1FC { background-position: -300px -320px; } -.emoji-1F201 { background-position: -320px -320px; } -.emoji-1F202 { background-position: -340px 0; } -.emoji-1F21A { background-position: -340px -20px; } -.emoji-1F22F { background-position: -340px -40px; } -.emoji-1F232 { background-position: -340px -60px; } -.emoji-1F233 { background-position: -340px -80px; } -.emoji-1F234 { background-position: -340px -100px; } -.emoji-1F235 { background-position: -340px -120px; } -.emoji-1F236 { background-position: -340px -140px; } -.emoji-1F237 { background-position: -340px -160px; } -.emoji-1F238 { background-position: -340px -180px; } -.emoji-1F239 { background-position: -340px -200px; } -.emoji-1F23A { background-position: -340px -220px; } -.emoji-1F250 { background-position: -340px -240px; } -.emoji-1F251 { background-position: -340px -260px; } -.emoji-1F300 { background-position: -340px -280px; } -.emoji-1F301 { background-position: -340px -300px; } -.emoji-1F302 { background-position: -340px -320px; } -.emoji-1F303 { background-position: 0 -340px; } -.emoji-1F304 { background-position: -20px -340px; } -.emoji-1F305 { background-position: -40px -340px; } -.emoji-1F306 { background-position: -60px -340px; } -.emoji-1F307 { background-position: -80px -340px; } -.emoji-1F308 { background-position: -100px -340px; } -.emoji-1F309 { background-position: -120px -340px; } -.emoji-1F30A { background-position: -140px -340px; } -.emoji-1F30B { background-position: -160px -340px; } -.emoji-1F30C { background-position: -180px -340px; } -.emoji-1F30D { background-position: -200px -340px; } -.emoji-1F30E { background-position: -220px -340px; } -.emoji-1F30F { background-position: -240px -340px; } -.emoji-1F310 { background-position: -260px -340px; } -.emoji-1F311 { background-position: -280px -340px; } -.emoji-1F312 { background-position: -300px -340px; } -.emoji-1F313 { background-position: -320px -340px; } -.emoji-1F314 { background-position: -340px -340px; } -.emoji-1F315 { background-position: -360px 0; } -.emoji-1F316 { background-position: -360px -20px; } -.emoji-1F317 { background-position: -360px -40px; } -.emoji-1F318 { background-position: -360px -60px; } -.emoji-1F319 { background-position: -360px -80px; } -.emoji-1F31A { background-position: -360px -100px; } -.emoji-1F31B { background-position: -360px -120px; } -.emoji-1F31C { background-position: -360px -140px; } -.emoji-1F31D { background-position: -360px -160px; } -.emoji-1F31E { background-position: -360px -180px; } -.emoji-1F31F { background-position: -360px -200px; } -.emoji-1F320 { background-position: -360px -220px; } -.emoji-1F321 { background-position: -360px -240px; } -.emoji-1F324 { background-position: -360px -260px; } -.emoji-1F325 { background-position: -360px -280px; } -.emoji-1F326 { background-position: -360px -300px; } -.emoji-1F327 { background-position: -360px -320px; } -.emoji-1F328 { background-position: -360px -340px; } -.emoji-1F329 { background-position: 0 -360px; } -.emoji-1F32A { background-position: -20px -360px; } -.emoji-1F32B { background-position: -40px -360px; } -.emoji-1F32C { background-position: -60px -360px; } -.emoji-1F32D { background-position: -80px -360px; } -.emoji-1F32E { background-position: -100px -360px; } -.emoji-1F32F { background-position: -120px -360px; } -.emoji-1F330 { background-position: -140px -360px; } -.emoji-1F331 { background-position: -160px -360px; } -.emoji-1F332 { background-position: -180px -360px; } -.emoji-1F333 { background-position: -200px -360px; } -.emoji-1F334 { background-position: -220px -360px; } -.emoji-1F335 { background-position: -240px -360px; } -.emoji-1F336 { background-position: -260px -360px; } -.emoji-1F337 { background-position: -280px -360px; } -.emoji-1F338 { background-position: -300px -360px; } -.emoji-1F339 { background-position: -320px -360px; } -.emoji-1F33A { background-position: -340px -360px; } -.emoji-1F33B { background-position: -360px -360px; } -.emoji-1F33C { background-position: -380px 0; } -.emoji-1F33D { background-position: -380px -20px; } -.emoji-1F33E { background-position: -380px -40px; } -.emoji-1F33F { background-position: -380px -60px; } -.emoji-1F340 { background-position: -380px -80px; } -.emoji-1F341 { background-position: -380px -100px; } -.emoji-1F342 { background-position: -380px -120px; } -.emoji-1F343 { background-position: -380px -140px; } -.emoji-1F344 { background-position: -380px -160px; } -.emoji-1F345 { background-position: -380px -180px; } -.emoji-1F346 { background-position: -380px -200px; } -.emoji-1F347 { background-position: -380px -220px; } -.emoji-1F348 { background-position: -380px -240px; } -.emoji-1F349 { background-position: -380px -260px; } -.emoji-1F34A { background-position: -380px -280px; } -.emoji-1F34B { background-position: -380px -300px; } -.emoji-1F34C { background-position: -380px -320px; } -.emoji-1F34D { background-position: -380px -340px; } -.emoji-1F34E { background-position: -380px -360px; } -.emoji-1F34F { background-position: 0 -380px; } -.emoji-1F350 { background-position: -20px -380px; } -.emoji-1F351 { background-position: -40px -380px; } -.emoji-1F352 { background-position: -60px -380px; } -.emoji-1F353 { background-position: -80px -380px; } -.emoji-1F354 { background-position: -100px -380px; } -.emoji-1F355 { background-position: -120px -380px; } -.emoji-1F356 { background-position: -140px -380px; } -.emoji-1F357 { background-position: -160px -380px; } -.emoji-1F358 { background-position: -180px -380px; } -.emoji-1F359 { background-position: -200px -380px; } -.emoji-1F35A { background-position: -220px -380px; } -.emoji-1F35B { background-position: -240px -380px; } -.emoji-1F35C { background-position: -260px -380px; } -.emoji-1F35D { background-position: -280px -380px; } -.emoji-1F35E { background-position: -300px -380px; } -.emoji-1F35F { background-position: -320px -380px; } -.emoji-1F360 { background-position: -340px -380px; } -.emoji-1F361 { background-position: -360px -380px; } -.emoji-1F362 { background-position: -380px -380px; } -.emoji-1F363 { background-position: -400px 0; } -.emoji-1F364 { background-position: -400px -20px; } -.emoji-1F365 { background-position: -400px -40px; } -.emoji-1F366 { background-position: -400px -60px; } -.emoji-1F367 { background-position: -400px -80px; } -.emoji-1F368 { background-position: -400px -100px; } -.emoji-1F369 { background-position: -400px -120px; } -.emoji-1F36A { background-position: -400px -140px; } -.emoji-1F36B { background-position: -400px -160px; } -.emoji-1F36C { background-position: -400px -180px; } -.emoji-1F36D { background-position: -400px -200px; } -.emoji-1F36E { background-position: -400px -220px; } -.emoji-1F36F { background-position: -400px -240px; } -.emoji-1F370 { background-position: -400px -260px; } -.emoji-1F371 { background-position: -400px -280px; } -.emoji-1F372 { background-position: -400px -300px; } -.emoji-1F373 { background-position: -400px -320px; } -.emoji-1F374 { background-position: -400px -340px; } -.emoji-1F375 { background-position: -400px -360px; } -.emoji-1F376 { background-position: -400px -380px; } -.emoji-1F377 { background-position: 0 -400px; } -.emoji-1F378 { background-position: -20px -400px; } -.emoji-1F379 { background-position: -40px -400px; } -.emoji-1F37A { background-position: -60px -400px; } -.emoji-1F37B { background-position: -80px -400px; } -.emoji-1F37C { background-position: -100px -400px; } -.emoji-1F37D { background-position: -120px -400px; } -.emoji-1F37E { background-position: -140px -400px; } -.emoji-1F37F { background-position: -160px -400px; } -.emoji-1F380 { background-position: -180px -400px; } -.emoji-1F381 { background-position: -200px -400px; } -.emoji-1F382 { background-position: -220px -400px; } -.emoji-1F383 { background-position: -240px -400px; } -.emoji-1F384 { background-position: -260px -400px; } -.emoji-1F385 { background-position: -280px -400px; } -.emoji-1F385-1F3FB { background-position: -300px -400px; } -.emoji-1F385-1F3FC { background-position: -320px -400px; } -.emoji-1F385-1F3FD { background-position: -340px -400px; } -.emoji-1F385-1F3FE { background-position: -360px -400px; } -.emoji-1F385-1F3FF { background-position: -380px -400px; } -.emoji-1F386 { background-position: -400px -400px; } -.emoji-1F387 { background-position: -420px 0; } -.emoji-1F388 { background-position: -420px -20px; } -.emoji-1F389 { background-position: -420px -40px; } -.emoji-1F38A { background-position: -420px -60px; } -.emoji-1F38B { background-position: -420px -80px; } -.emoji-1F38C { background-position: -420px -100px; } -.emoji-1F38D { background-position: -420px -120px; } -.emoji-1F38E { background-position: -420px -140px; } -.emoji-1F38F { background-position: -420px -160px; } -.emoji-1F390 { background-position: -420px -180px; } -.emoji-1F391 { background-position: -420px -200px; } -.emoji-1F392 { background-position: -420px -220px; } -.emoji-1F393 { background-position: -420px -240px; } -.emoji-1F396 { background-position: -420px -260px; } -.emoji-1F397 { background-position: -420px -280px; } -.emoji-1F399 { background-position: -420px -300px; } -.emoji-1F39A { background-position: -420px -320px; } -.emoji-1F39B { background-position: -420px -340px; } -.emoji-1F39E { background-position: -420px -360px; } -.emoji-1F39F { background-position: -420px -380px; } -.emoji-1F3A0 { background-position: -420px -400px; } -.emoji-1F3A1 { background-position: 0 -420px; } -.emoji-1F3A2 { background-position: -20px -420px; } -.emoji-1F3A3 { background-position: -40px -420px; } -.emoji-1F3A4 { background-position: -60px -420px; } -.emoji-1F3A5 { background-position: -80px -420px; } -.emoji-1F3A6 { background-position: -100px -420px; } -.emoji-1F3A7 { background-position: -120px -420px; } -.emoji-1F3A8 { background-position: -140px -420px; } -.emoji-1F3A9 { background-position: -160px -420px; } -.emoji-1F3AA { background-position: -180px -420px; } -.emoji-1F3AB { background-position: -200px -420px; } -.emoji-1F3AC { background-position: -220px -420px; } -.emoji-1F3AD { background-position: -240px -420px; } -.emoji-1F3AE { background-position: -260px -420px; } -.emoji-1F3AF { background-position: -280px -420px; } -.emoji-1F3B0 { background-position: -300px -420px; } -.emoji-1F3B1 { background-position: -320px -420px; } -.emoji-1F3B2 { background-position: -340px -420px; } -.emoji-1F3B3 { background-position: -360px -420px; } -.emoji-1F3B4 { background-position: -380px -420px; } -.emoji-1F3B5 { background-position: -400px -420px; } -.emoji-1F3B6 { background-position: -420px -420px; } -.emoji-1F3B7 { background-position: -440px 0; } -.emoji-1F3B8 { background-position: -440px -20px; } -.emoji-1F3B9 { background-position: -440px -40px; } -.emoji-1F3BA { background-position: -440px -60px; } -.emoji-1F3BB { background-position: -440px -80px; } -.emoji-1F3BC { background-position: -440px -100px; } -.emoji-1F3BD { background-position: -440px -120px; } -.emoji-1F3BE { background-position: -440px -140px; } -.emoji-1F3BF { background-position: -440px -160px; } -.emoji-1F3C0 { background-position: -440px -180px; } -.emoji-1F3C1 { background-position: -440px -200px; } -.emoji-1F3C2 { background-position: -440px -220px; } -.emoji-1F3C3 { background-position: -440px -240px; } -.emoji-1F3C3-1F3FB { background-position: -440px -260px; } -.emoji-1F3C3-1F3FC { background-position: -440px -280px; } -.emoji-1F3C3-1F3FD { background-position: -440px -300px; } -.emoji-1F3C3-1F3FE { background-position: -440px -320px; } -.emoji-1F3C3-1F3FF { background-position: -440px -340px; } -.emoji-1F3C4 { background-position: -440px -360px; } -.emoji-1F3C4-1F3FB { background-position: -440px -380px; } -.emoji-1F3C4-1F3FC { background-position: -440px -400px; } -.emoji-1F3C4-1F3FD { background-position: -440px -420px; } -.emoji-1F3C4-1F3FE { background-position: 0 -440px; } -.emoji-1F3C4-1F3FF { background-position: -20px -440px; } -.emoji-1F3C5 { background-position: -40px -440px; } -.emoji-1F3C6 { background-position: -60px -440px; } -.emoji-1F3C7 { background-position: -80px -440px; } -.emoji-1F3C7-1F3FB { background-position: -100px -440px; } -.emoji-1F3C7-1F3FC { background-position: -120px -440px; } -.emoji-1F3C7-1F3FD { background-position: -140px -440px; } -.emoji-1F3C7-1F3FE { background-position: -160px -440px; } -.emoji-1F3C7-1F3FF { background-position: -180px -440px; } -.emoji-1F3C8 { background-position: -200px -440px; } -.emoji-1F3C9 { background-position: -220px -440px; } -.emoji-1F3CA { background-position: -240px -440px; } -.emoji-1F3CA-1F3FB { background-position: -260px -440px; } -.emoji-1F3CA-1F3FC { background-position: -280px -440px; } -.emoji-1F3CA-1F3FD { background-position: -300px -440px; } -.emoji-1F3CA-1F3FE { background-position: -320px -440px; } -.emoji-1F3CA-1F3FF { background-position: -340px -440px; } -.emoji-1F3CB { background-position: -360px -440px; } -.emoji-1F3CB-1F3FB { background-position: -380px -440px; } -.emoji-1F3CB-1F3FC { background-position: -400px -440px; } -.emoji-1F3CB-1F3FD { background-position: -420px -440px; } -.emoji-1F3CB-1F3FE { background-position: -440px -440px; } -.emoji-1F3CB-1F3FF { background-position: -460px 0; } -.emoji-1F3CC { background-position: -460px -20px; } -.emoji-1F3CD { background-position: -460px -40px; } -.emoji-1F3CE { background-position: -460px -60px; } -.emoji-1F3CF { background-position: -460px -80px; } -.emoji-1F3D0 { background-position: -460px -100px; } -.emoji-1F3D1 { background-position: -460px -120px; } -.emoji-1F3D2 { background-position: -460px -140px; } -.emoji-1F3D3 { background-position: -460px -160px; } -.emoji-1F3D4 { background-position: -460px -180px; } -.emoji-1F3D5 { background-position: -460px -200px; } -.emoji-1F3D6 { background-position: -460px -220px; } -.emoji-1F3D7 { background-position: -460px -240px; } -.emoji-1F3D8 { background-position: -460px -260px; } -.emoji-1F3D9 { background-position: -460px -280px; } -.emoji-1F3DA { background-position: -460px -300px; } -.emoji-1F3DB { background-position: -460px -320px; } -.emoji-1F3DC { background-position: -460px -340px; } -.emoji-1F3DD { background-position: -460px -360px; } -.emoji-1F3DE { background-position: -460px -380px; } -.emoji-1F3DF { background-position: -460px -400px; } -.emoji-1F3E0 { background-position: -460px -420px; } -.emoji-1F3E1 { background-position: -460px -440px; } -.emoji-1F3E2 { background-position: 0 -460px; } -.emoji-1F3E3 { background-position: -20px -460px; } -.emoji-1F3E4 { background-position: -40px -460px; } -.emoji-1F3E5 { background-position: -60px -460px; } -.emoji-1F3E6 { background-position: -80px -460px; } -.emoji-1F3E7 { background-position: -100px -460px; } -.emoji-1F3E8 { background-position: -120px -460px; } -.emoji-1F3E9 { background-position: -140px -460px; } -.emoji-1F3EA { background-position: -160px -460px; } -.emoji-1F3EB { background-position: -180px -460px; } -.emoji-1F3EC { background-position: -200px -460px; } -.emoji-1F3ED { background-position: -220px -460px; } -.emoji-1F3EE { background-position: -240px -460px; } -.emoji-1F3EF { background-position: -260px -460px; } -.emoji-1F3F0 { background-position: -280px -460px; } -.emoji-1F3F3 { background-position: -300px -460px; } -.emoji-1F3F4 { background-position: -320px -460px; } -.emoji-1F3F5 { background-position: -340px -460px; } -.emoji-1F3F7 { background-position: -360px -460px; } -.emoji-1F3F8 { background-position: -380px -460px; } -.emoji-1F3F9 { background-position: -400px -460px; } -.emoji-1F3FA { background-position: -420px -460px; } -.emoji-1F3FB { background-position: -440px -460px; } -.emoji-1F3FC { background-position: -460px -460px; } -.emoji-1F3FD { background-position: -480px 0; } -.emoji-1F3FE { background-position: -480px -20px; } -.emoji-1F3FF { background-position: -480px -40px; } -.emoji-1F400 { background-position: -480px -60px; } -.emoji-1F401 { background-position: -480px -80px; } -.emoji-1F402 { background-position: -480px -100px; } -.emoji-1F403 { background-position: -480px -120px; } -.emoji-1F404 { background-position: -480px -140px; } -.emoji-1F405 { background-position: -480px -160px; } -.emoji-1F406 { background-position: -480px -180px; } -.emoji-1F407 { background-position: -480px -200px; } -.emoji-1F408 { background-position: -480px -220px; } -.emoji-1F409 { background-position: -480px -240px; } -.emoji-1F40A { background-position: -480px -260px; } -.emoji-1F40B { background-position: -480px -280px; } -.emoji-1F40C { background-position: -480px -300px; } -.emoji-1F40D { background-position: -480px -320px; } -.emoji-1F40E { background-position: -480px -340px; } -.emoji-1F40F { background-position: -480px -360px; } -.emoji-1F410 { background-position: -480px -380px; } -.emoji-1F411 { background-position: -480px -400px; } -.emoji-1F412 { background-position: -480px -420px; } -.emoji-1F413 { background-position: -480px -440px; } -.emoji-1F414 { background-position: -480px -460px; } -.emoji-1F415 { background-position: 0 -480px; } -.emoji-1F416 { background-position: -20px -480px; } -.emoji-1F417 { background-position: -40px -480px; } -.emoji-1F418 { background-position: -60px -480px; } -.emoji-1F419 { background-position: -80px -480px; } -.emoji-1F41A { background-position: -100px -480px; } -.emoji-1F41B { background-position: -120px -480px; } -.emoji-1F41C { background-position: -140px -480px; } -.emoji-1F41D { background-position: -160px -480px; } -.emoji-1F41E { background-position: -180px -480px; } -.emoji-1F41F { background-position: -200px -480px; } -.emoji-1F420 { background-position: -220px -480px; } -.emoji-1F421 { background-position: -240px -480px; } -.emoji-1F422 { background-position: -260px -480px; } -.emoji-1F423 { background-position: -280px -480px; } -.emoji-1F424 { background-position: -300px -480px; } -.emoji-1F425 { background-position: -320px -480px; } -.emoji-1F426 { background-position: -340px -480px; } -.emoji-1F427 { background-position: -360px -480px; } -.emoji-1F428 { background-position: -380px -480px; } -.emoji-1F429 { background-position: -400px -480px; } -.emoji-1F42A { background-position: -420px -480px; } -.emoji-1F42B { background-position: -440px -480px; } -.emoji-1F42C { background-position: -460px -480px; } -.emoji-1F42D { background-position: -480px -480px; } -.emoji-1F42E { background-position: -500px 0; } -.emoji-1F42F { background-position: -500px -20px; } -.emoji-1F430 { background-position: -500px -40px; } -.emoji-1F431 { background-position: -500px -60px; } -.emoji-1F432 { background-position: -500px -80px; } -.emoji-1F433 { background-position: -500px -100px; } -.emoji-1F434 { background-position: -500px -120px; } -.emoji-1F435 { background-position: -500px -140px; } -.emoji-1F436 { background-position: -500px -160px; } -.emoji-1F437 { background-position: -500px -180px; } -.emoji-1F438 { background-position: -500px -200px; } -.emoji-1F439 { background-position: -500px -220px; } -.emoji-1F43A { background-position: -500px -240px; } -.emoji-1F43B { background-position: -500px -260px; } -.emoji-1F43C { background-position: -500px -280px; } -.emoji-1F43D { background-position: -500px -300px; } -.emoji-1F43E { background-position: -500px -320px; } -.emoji-1F43F { background-position: -500px -340px; } -.emoji-1F440 { background-position: -500px -360px; } -.emoji-1F441 { background-position: -500px -380px; } -.emoji-1F441-1F5E8 { background-position: -500px -400px; } -.emoji-1F442 { background-position: -500px -420px; } -.emoji-1F442-1F3FB { background-position: -500px -440px; } -.emoji-1F442-1F3FC { background-position: -500px -460px; } -.emoji-1F442-1F3FD { background-position: -500px -480px; } -.emoji-1F442-1F3FE { background-position: 0 -500px; } -.emoji-1F442-1F3FF { background-position: -20px -500px; } -.emoji-1F443 { background-position: -40px -500px; } -.emoji-1F443-1F3FB { background-position: -60px -500px; } -.emoji-1F443-1F3FC { background-position: -80px -500px; } -.emoji-1F443-1F3FD { background-position: -100px -500px; } -.emoji-1F443-1F3FE { background-position: -120px -500px; } -.emoji-1F443-1F3FF { background-position: -140px -500px; } -.emoji-1F444 { background-position: -160px -500px; } -.emoji-1F445 { background-position: -180px -500px; } -.emoji-1F446 { background-position: -200px -500px; } -.emoji-1F446-1F3FB { background-position: -220px -500px; } -.emoji-1F446-1F3FC { background-position: -240px -500px; } -.emoji-1F446-1F3FD { background-position: -260px -500px; } -.emoji-1F446-1F3FE { background-position: -280px -500px; } -.emoji-1F446-1F3FF { background-position: -300px -500px; } -.emoji-1F447 { background-position: -320px -500px; } -.emoji-1F447-1F3FB { background-position: -340px -500px; } -.emoji-1F447-1F3FC { background-position: -360px -500px; } -.emoji-1F447-1F3FD { background-position: -380px -500px; } -.emoji-1F447-1F3FE { background-position: -400px -500px; } -.emoji-1F447-1F3FF { background-position: -420px -500px; } -.emoji-1F448 { background-position: -440px -500px; } -.emoji-1F448-1F3FB { background-position: -460px -500px; } -.emoji-1F448-1F3FC { background-position: -480px -500px; } -.emoji-1F448-1F3FD { background-position: -500px -500px; } -.emoji-1F448-1F3FE { background-position: -520px 0; } -.emoji-1F448-1F3FF { background-position: -520px -20px; } -.emoji-1F449 { background-position: -520px -40px; } -.emoji-1F449-1F3FB { background-position: -520px -60px; } -.emoji-1F449-1F3FC { background-position: -520px -80px; } -.emoji-1F449-1F3FD { background-position: -520px -100px; } -.emoji-1F449-1F3FE { background-position: -520px -120px; } -.emoji-1F449-1F3FF { background-position: -520px -140px; } -.emoji-1F44A { background-position: -520px -160px; } -.emoji-1F44A-1F3FB { background-position: -520px -180px; } -.emoji-1F44A-1F3FC { background-position: -520px -200px; } -.emoji-1F44A-1F3FD { background-position: -520px -220px; } -.emoji-1F44A-1F3FE { background-position: -520px -240px; } -.emoji-1F44A-1F3FF { background-position: -520px -260px; } -.emoji-1F44B { background-position: -520px -280px; } -.emoji-1F44B-1F3FB { background-position: -520px -300px; } -.emoji-1F44B-1F3FC { background-position: -520px -320px; } -.emoji-1F44B-1F3FD { background-position: -520px -340px; } -.emoji-1F44B-1F3FE { background-position: -520px -360px; } -.emoji-1F44B-1F3FF { background-position: -520px -380px; } -.emoji-1F44C { background-position: -520px -400px; } -.emoji-1F44C-1F3FB { background-position: -520px -420px; } -.emoji-1F44C-1F3FC { background-position: -520px -440px; } -.emoji-1F44C-1F3FD { background-position: -520px -460px; } -.emoji-1F44C-1F3FE { background-position: -520px -480px; } -.emoji-1F44C-1F3FF { background-position: -520px -500px; } -.emoji-1F44D { background-position: 0 -520px; } -.emoji-1F44D-1F3FB { background-position: -20px -520px; } -.emoji-1F44D-1F3FC { background-position: -40px -520px; } -.emoji-1F44D-1F3FD { background-position: -60px -520px; } -.emoji-1F44D-1F3FE { background-position: -80px -520px; } -.emoji-1F44D-1F3FF { background-position: -100px -520px; } -.emoji-1F44E { background-position: -120px -520px; } -.emoji-1F44E-1F3FB { background-position: -140px -520px; } -.emoji-1F44E-1F3FC { background-position: -160px -520px; } -.emoji-1F44E-1F3FD { background-position: -180px -520px; } -.emoji-1F44E-1F3FE { background-position: -200px -520px; } -.emoji-1F44E-1F3FF { background-position: -220px -520px; } -.emoji-1F44F { background-position: -240px -520px; } -.emoji-1F44F-1F3FB { background-position: -260px -520px; } -.emoji-1F44F-1F3FC { background-position: -280px -520px; } -.emoji-1F44F-1F3FD { background-position: -300px -520px; } -.emoji-1F44F-1F3FE { background-position: -320px -520px; } -.emoji-1F44F-1F3FF { background-position: -340px -520px; } -.emoji-1F450 { background-position: -360px -520px; } -.emoji-1F450-1F3FB { background-position: -380px -520px; } -.emoji-1F450-1F3FC { background-position: -400px -520px; } -.emoji-1F450-1F3FD { background-position: -420px -520px; } -.emoji-1F450-1F3FE { background-position: -440px -520px; } -.emoji-1F450-1F3FF { background-position: -460px -520px; } -.emoji-1F451 { background-position: -480px -520px; } -.emoji-1F452 { background-position: -500px -520px; } -.emoji-1F453 { background-position: -520px -520px; } -.emoji-1F454 { background-position: -540px 0; } -.emoji-1F455 { background-position: -540px -20px; } -.emoji-1F456 { background-position: -540px -40px; } -.emoji-1F457 { background-position: -540px -60px; } -.emoji-1F458 { background-position: -540px -80px; } -.emoji-1F459 { background-position: -540px -100px; } -.emoji-1F45A { background-position: -540px -120px; } -.emoji-1F45B { background-position: -540px -140px; } -.emoji-1F45C { background-position: -540px -160px; } -.emoji-1F45D { background-position: -540px -180px; } -.emoji-1F45E { background-position: -540px -200px; } -.emoji-1F45F { background-position: -540px -220px; } -.emoji-1F460 { background-position: -540px -240px; } -.emoji-1F461 { background-position: -540px -260px; } -.emoji-1F462 { background-position: -540px -280px; } -.emoji-1F463 { background-position: -540px -300px; } -.emoji-1F464 { background-position: -540px -320px; } -.emoji-1F465 { background-position: -540px -340px; } -.emoji-1F466 { background-position: -540px -360px; } -.emoji-1F466-1F3FB { background-position: -540px -380px; } -.emoji-1F466-1F3FC { background-position: -540px -400px; } -.emoji-1F466-1F3FD { background-position: -540px -420px; } -.emoji-1F466-1F3FE { background-position: -540px -440px; } -.emoji-1F466-1F3FF { background-position: -540px -460px; } -.emoji-1F467 { background-position: -540px -480px; } -.emoji-1F467-1F3FB { background-position: -540px -500px; } -.emoji-1F467-1F3FC { background-position: -540px -520px; } -.emoji-1F467-1F3FD { background-position: 0 -540px; } -.emoji-1F467-1F3FE { background-position: -20px -540px; } -.emoji-1F467-1F3FF { background-position: -40px -540px; } -.emoji-1F468 { background-position: -60px -540px; } -.emoji-1F468-1F3FB { background-position: -80px -540px; } -.emoji-1F468-1F3FC { background-position: -100px -540px; } -.emoji-1F468-1F3FD { background-position: -120px -540px; } -.emoji-1F468-1F3FE { background-position: -140px -540px; } -.emoji-1F468-1F3FF { background-position: -160px -540px; } -.emoji-1F468-1F468-1F466 { background-position: -180px -540px; } -.emoji-1F468-1F468-1F466-1F466 { background-position: -200px -540px; } -.emoji-1F468-1F468-1F467 { background-position: -220px -540px; } -.emoji-1F468-1F468-1F467-1F466 { background-position: -240px -540px; } -.emoji-1F468-1F468-1F467-1F467 { background-position: -260px -540px; } -.emoji-1F468-1F469-1F466-1F466 { background-position: -280px -540px; } -.emoji-1F468-1F469-1F467 { background-position: -300px -540px; } -.emoji-1F468-1F469-1F467-1F466 { background-position: -320px -540px; } -.emoji-1F468-1F469-1F467-1F467 { background-position: -340px -540px; } -.emoji-1F468-2764-1F468 { background-position: -360px -540px; } -.emoji-1F468-2764-1F48B-1F468 { background-position: -380px -540px; } -.emoji-1F469 { background-position: -400px -540px; } -.emoji-1F469-1F3FB { background-position: -420px -540px; } -.emoji-1F469-1F3FC { background-position: -440px -540px; } -.emoji-1F469-1F3FD { background-position: -460px -540px; } -.emoji-1F469-1F3FE { background-position: -480px -540px; } -.emoji-1F469-1F3FF { background-position: -500px -540px; } -.emoji-1F469-1F469-1F466 { background-position: -520px -540px; } -.emoji-1F469-1F469-1F466-1F466 { background-position: -540px -540px; } -.emoji-1F469-1F469-1F467 { background-position: -560px 0; } -.emoji-1F469-1F469-1F467-1F466 { background-position: -560px -20px; } -.emoji-1F469-1F469-1F467-1F467 { background-position: -560px -40px; } -.emoji-1F469-2764-1F469 { background-position: -560px -60px; } -.emoji-1F469-2764-1F48B-1F469 { background-position: -560px -80px; } -.emoji-1F46A { background-position: -560px -100px; } -.emoji-1F46B { background-position: -560px -120px; } -.emoji-1F46C { background-position: -560px -140px; } -.emoji-1F46D { background-position: -560px -160px; } -.emoji-1F46E { background-position: -560px -180px; } -.emoji-1F46E-1F3FB { background-position: -560px -200px; } -.emoji-1F46E-1F3FC { background-position: -560px -220px; } -.emoji-1F46E-1F3FD { background-position: -560px -240px; } -.emoji-1F46E-1F3FE { background-position: -560px -260px; } -.emoji-1F46E-1F3FF { background-position: -560px -280px; } -.emoji-1F46F { background-position: -560px -300px; } -.emoji-1F470 { background-position: -560px -320px; } -.emoji-1F470-1F3FB { background-position: -560px -340px; } -.emoji-1F470-1F3FC { background-position: -560px -360px; } -.emoji-1F470-1F3FD { background-position: -560px -380px; } -.emoji-1F470-1F3FE { background-position: -560px -400px; } -.emoji-1F470-1F3FF { background-position: -560px -420px; } -.emoji-1F471 { background-position: -560px -440px; } -.emoji-1F471-1F3FB { background-position: -560px -460px; } -.emoji-1F471-1F3FC { background-position: -560px -480px; } -.emoji-1F471-1F3FD { background-position: -560px -500px; } -.emoji-1F471-1F3FE { background-position: -560px -520px; } -.emoji-1F471-1F3FF { background-position: -560px -540px; } -.emoji-1F472 { background-position: 0 -560px; } -.emoji-1F472-1F3FB { background-position: -20px -560px; } -.emoji-1F472-1F3FC { background-position: -40px -560px; } -.emoji-1F472-1F3FD { background-position: -60px -560px; } -.emoji-1F472-1F3FE { background-position: -80px -560px; } -.emoji-1F472-1F3FF { background-position: -100px -560px; } -.emoji-1F473 { background-position: -120px -560px; } -.emoji-1F473-1F3FB { background-position: -140px -560px; } -.emoji-1F473-1F3FC { background-position: -160px -560px; } -.emoji-1F473-1F3FD { background-position: -180px -560px; } -.emoji-1F473-1F3FE { background-position: -200px -560px; } -.emoji-1F473-1F3FF { background-position: -220px -560px; } -.emoji-1F474 { background-position: -240px -560px; } -.emoji-1F474-1F3FB { background-position: -260px -560px; } -.emoji-1F474-1F3FC { background-position: -280px -560px; } -.emoji-1F474-1F3FD { background-position: -300px -560px; } -.emoji-1F474-1F3FE { background-position: -320px -560px; } -.emoji-1F474-1F3FF { background-position: -340px -560px; } -.emoji-1F475 { background-position: -360px -560px; } -.emoji-1F475-1F3FB { background-position: -380px -560px; } -.emoji-1F475-1F3FC { background-position: -400px -560px; } -.emoji-1F475-1F3FD { background-position: -420px -560px; } -.emoji-1F475-1F3FE { background-position: -440px -560px; } -.emoji-1F475-1F3FF { background-position: -460px -560px; } -.emoji-1F476 { background-position: -480px -560px; } -.emoji-1F476-1F3FB { background-position: -500px -560px; } -.emoji-1F476-1F3FC { background-position: -520px -560px; } -.emoji-1F476-1F3FD { background-position: -540px -560px; } -.emoji-1F476-1F3FE { background-position: -560px -560px; } -.emoji-1F476-1F3FF { background-position: -580px 0; } -.emoji-1F477 { background-position: -580px -20px; } -.emoji-1F477-1F3FB { background-position: -580px -40px; } -.emoji-1F477-1F3FC { background-position: -580px -60px; } -.emoji-1F477-1F3FD { background-position: -580px -80px; } -.emoji-1F477-1F3FE { background-position: -580px -100px; } -.emoji-1F477-1F3FF { background-position: -580px -120px; } -.emoji-1F478 { background-position: -580px -140px; } -.emoji-1F478-1F3FB { background-position: -580px -160px; } -.emoji-1F478-1F3FC { background-position: -580px -180px; } -.emoji-1F478-1F3FD { background-position: -580px -200px; } -.emoji-1F478-1F3FE { background-position: -580px -220px; } -.emoji-1F478-1F3FF { background-position: -580px -240px; } -.emoji-1F479 { background-position: -580px -260px; } -.emoji-1F47A { background-position: -580px -280px; } -.emoji-1F47B { background-position: -580px -300px; } -.emoji-1F47C { background-position: -580px -320px; } -.emoji-1F47C-1F3FB { background-position: -580px -340px; } -.emoji-1F47C-1F3FC { background-position: -580px -360px; } -.emoji-1F47C-1F3FD { background-position: -580px -380px; } -.emoji-1F47C-1F3FE { background-position: -580px -400px; } -.emoji-1F47C-1F3FF { background-position: -580px -420px; } -.emoji-1F47D { background-position: -580px -440px; } -.emoji-1F47E { background-position: -580px -460px; } -.emoji-1F47F { background-position: -580px -480px; } -.emoji-1F480 { background-position: -580px -500px; } -.emoji-1F481 { background-position: -580px -520px; } -.emoji-1F481-1F3FB { background-position: -580px -540px; } -.emoji-1F481-1F3FC { background-position: -580px -560px; } -.emoji-1F481-1F3FD { background-position: 0 -580px; } -.emoji-1F481-1F3FE { background-position: -20px -580px; } -.emoji-1F481-1F3FF { background-position: -40px -580px; } -.emoji-1F482 { background-position: -60px -580px; } -.emoji-1F482-1F3FB { background-position: -80px -580px; } -.emoji-1F482-1F3FC { background-position: -100px -580px; } -.emoji-1F482-1F3FD { background-position: -120px -580px; } -.emoji-1F482-1F3FE { background-position: -140px -580px; } -.emoji-1F482-1F3FF { background-position: -160px -580px; } -.emoji-1F483 { background-position: -180px -580px; } -.emoji-1F483-1F3FB { background-position: -200px -580px; } -.emoji-1F483-1F3FC { background-position: -220px -580px; } -.emoji-1F483-1F3FD { background-position: -240px -580px; } -.emoji-1F483-1F3FE { background-position: -260px -580px; } -.emoji-1F483-1F3FF { background-position: -280px -580px; } -.emoji-1F484 { background-position: -300px -580px; } -.emoji-1F485 { background-position: -320px -580px; } -.emoji-1F485-1F3FB { background-position: -340px -580px; } -.emoji-1F485-1F3FC { background-position: -360px -580px; } -.emoji-1F485-1F3FD { background-position: -380px -580px; } -.emoji-1F485-1F3FE { background-position: -400px -580px; } -.emoji-1F485-1F3FF { background-position: -420px -580px; } -.emoji-1F486 { background-position: -440px -580px; } -.emoji-1F486-1F3FB { background-position: -460px -580px; } -.emoji-1F486-1F3FC { background-position: -480px -580px; } -.emoji-1F486-1F3FD { background-position: -500px -580px; } -.emoji-1F486-1F3FE { background-position: -520px -580px; } -.emoji-1F486-1F3FF { background-position: -540px -580px; } -.emoji-1F487 { background-position: -560px -580px; } -.emoji-1F487-1F3FB { background-position: -580px -580px; } -.emoji-1F487-1F3FC { background-position: -600px 0; } -.emoji-1F487-1F3FD { background-position: -600px -20px; } -.emoji-1F487-1F3FE { background-position: -600px -40px; } -.emoji-1F487-1F3FF { background-position: -600px -60px; } -.emoji-1F488 { background-position: -600px -80px; } -.emoji-1F489 { background-position: -600px -100px; } -.emoji-1F48A { background-position: -600px -120px; } -.emoji-1F48B { background-position: -600px -140px; } -.emoji-1F48C { background-position: -600px -160px; } -.emoji-1F48D { background-position: -600px -180px; } -.emoji-1F48E { background-position: -600px -200px; } -.emoji-1F48F { background-position: -600px -220px; } -.emoji-1F490 { background-position: -600px -240px; } -.emoji-1F491 { background-position: -600px -260px; } -.emoji-1F492 { background-position: -600px -280px; } -.emoji-1F493 { background-position: -600px -300px; } -.emoji-1F494 { background-position: -600px -320px; } -.emoji-1F495 { background-position: -600px -340px; } -.emoji-1F496 { background-position: -600px -360px; } -.emoji-1F497 { background-position: -600px -380px; } -.emoji-1F498 { background-position: -600px -400px; } -.emoji-1F499 { background-position: -600px -420px; } -.emoji-1F49A { background-position: -600px -440px; } -.emoji-1F49B { background-position: -600px -460px; } -.emoji-1F49C { background-position: -600px -480px; } -.emoji-1F49D { background-position: -600px -500px; } -.emoji-1F49E { background-position: -600px -520px; } -.emoji-1F49F { background-position: -600px -540px; } -.emoji-1F4A0 { background-position: -600px -560px; } -.emoji-1F4A1 { background-position: -600px -580px; } -.emoji-1F4A2 { background-position: 0 -600px; } -.emoji-1F4A3 { background-position: -20px -600px; } -.emoji-1F4A4 { background-position: -40px -600px; } -.emoji-1F4A5 { background-position: -60px -600px; } -.emoji-1F4A6 { background-position: -80px -600px; } -.emoji-1F4A7 { background-position: -100px -600px; } -.emoji-1F4A8 { background-position: -120px -600px; } -.emoji-1F4A9 { background-position: -140px -600px; } -.emoji-1F4AA { background-position: -160px -600px; } -.emoji-1F4AA-1F3FB { background-position: -180px -600px; } -.emoji-1F4AA-1F3FC { background-position: -200px -600px; } -.emoji-1F4AA-1F3FD { background-position: -220px -600px; } -.emoji-1F4AA-1F3FE { background-position: -240px -600px; } -.emoji-1F4AA-1F3FF { background-position: -260px -600px; } -.emoji-1F4AB { background-position: -280px -600px; } -.emoji-1F4AC { background-position: -300px -600px; } -.emoji-1F4AD { background-position: -320px -600px; } -.emoji-1F4AE { background-position: -340px -600px; } -.emoji-1F4AF { background-position: -360px -600px; } -.emoji-1F4B0 { background-position: -380px -600px; } -.emoji-1F4B1 { background-position: -400px -600px; } -.emoji-1F4B2 { background-position: -420px -600px; } -.emoji-1F4B3 { background-position: -440px -600px; } -.emoji-1F4B4 { background-position: -460px -600px; } -.emoji-1F4B5 { background-position: -480px -600px; } -.emoji-1F4B6 { background-position: -500px -600px; } -.emoji-1F4B7 { background-position: -520px -600px; } -.emoji-1F4B8 { background-position: -540px -600px; } -.emoji-1F4B9 { background-position: -560px -600px; } -.emoji-1F4BA { background-position: -580px -600px; } -.emoji-1F4BB { background-position: -600px -600px; } -.emoji-1F4BC { background-position: -620px 0; } -.emoji-1F4BD { background-position: -620px -20px; } -.emoji-1F4BE { background-position: -620px -40px; } -.emoji-1F4BF { background-position: -620px -60px; } -.emoji-1F4C0 { background-position: -620px -80px; } -.emoji-1F4C1 { background-position: -620px -100px; } -.emoji-1F4C2 { background-position: -620px -120px; } -.emoji-1F4C3 { background-position: -620px -140px; } -.emoji-1F4C4 { background-position: -620px -160px; } -.emoji-1F4C5 { background-position: -620px -180px; } -.emoji-1F4C6 { background-position: -620px -200px; } -.emoji-1F4C7 { background-position: -620px -220px; } -.emoji-1F4C8 { background-position: -620px -240px; } -.emoji-1F4C9 { background-position: -620px -260px; } -.emoji-1F4CA { background-position: -620px -280px; } -.emoji-1F4CB { background-position: -620px -300px; } -.emoji-1F4CC { background-position: -620px -320px; } -.emoji-1F4CD { background-position: -620px -340px; } -.emoji-1F4CE { background-position: -620px -360px; } -.emoji-1F4CF { background-position: -620px -380px; } -.emoji-1F4D0 { background-position: -620px -400px; } -.emoji-1F4D1 { background-position: -620px -420px; } -.emoji-1F4D2 { background-position: -620px -440px; } -.emoji-1F4D3 { background-position: -620px -460px; } -.emoji-1F4D4 { background-position: -620px -480px; } -.emoji-1F4D5 { background-position: -620px -500px; } -.emoji-1F4D6 { background-position: -620px -520px; } -.emoji-1F4D7 { background-position: -620px -540px; } -.emoji-1F4D8 { background-position: -620px -560px; } -.emoji-1F4D9 { background-position: -620px -580px; } -.emoji-1F4DA { background-position: -620px -600px; } -.emoji-1F4DB { background-position: 0 -620px; } -.emoji-1F4DC { background-position: -20px -620px; } -.emoji-1F4DD { background-position: -40px -620px; } -.emoji-1F4DE { background-position: -60px -620px; } -.emoji-1F4DF { background-position: -80px -620px; } -.emoji-1F4E0 { background-position: -100px -620px; } -.emoji-1F4E1 { background-position: -120px -620px; } -.emoji-1F4E2 { background-position: -140px -620px; } -.emoji-1F4E3 { background-position: -160px -620px; } -.emoji-1F4E4 { background-position: -180px -620px; } -.emoji-1F4E5 { background-position: -200px -620px; } -.emoji-1F4E6 { background-position: -220px -620px; } -.emoji-1F4E7 { background-position: -240px -620px; } -.emoji-1F4E8 { background-position: -260px -620px; } -.emoji-1F4E9 { background-position: -280px -620px; } -.emoji-1F4EA { background-position: -300px -620px; } -.emoji-1F4EB { background-position: -320px -620px; } -.emoji-1F4EC { background-position: -340px -620px; } -.emoji-1F4ED { background-position: -360px -620px; } -.emoji-1F4EE { background-position: -380px -620px; } -.emoji-1F4EF { background-position: -400px -620px; } -.emoji-1F4F0 { background-position: -420px -620px; } -.emoji-1F4F1 { background-position: -440px -620px; } -.emoji-1F4F2 { background-position: -460px -620px; } -.emoji-1F4F3 { background-position: -480px -620px; } -.emoji-1F4F4 { background-position: -500px -620px; } -.emoji-1F4F5 { background-position: -520px -620px; } -.emoji-1F4F6 { background-position: -540px -620px; } -.emoji-1F4F7 { background-position: -560px -620px; } -.emoji-1F4F8 { background-position: -580px -620px; } -.emoji-1F4F9 { background-position: -600px -620px; } -.emoji-1F4FA { background-position: -620px -620px; } -.emoji-1F4FB { background-position: -640px 0; } -.emoji-1F4FC { background-position: -640px -20px; } -.emoji-1F4FD { background-position: -640px -40px; } -.emoji-1F4FF { background-position: -640px -60px; } -.emoji-1F500 { background-position: -640px -80px; } -.emoji-1F501 { background-position: -640px -100px; } -.emoji-1F502 { background-position: -640px -120px; } -.emoji-1F503 { background-position: -640px -140px; } -.emoji-1F504 { background-position: -640px -160px; } -.emoji-1F505 { background-position: -640px -180px; } -.emoji-1F506 { background-position: -640px -200px; } -.emoji-1F507 { background-position: -640px -220px; } -.emoji-1F508 { background-position: -640px -240px; } -.emoji-1F509 { background-position: -640px -260px; } -.emoji-1F50A { background-position: -640px -280px; } -.emoji-1F50B { background-position: -640px -300px; } -.emoji-1F50C { background-position: -640px -320px; } -.emoji-1F50D { background-position: -640px -340px; } -.emoji-1F50E { background-position: -640px -360px; } -.emoji-1F50F { background-position: -640px -380px; } -.emoji-1F510 { background-position: -640px -400px; } -.emoji-1F511 { background-position: -640px -420px; } -.emoji-1F512 { background-position: -640px -440px; } -.emoji-1F513 { background-position: -640px -460px; } -.emoji-1F514 { background-position: -640px -480px; } -.emoji-1F515 { background-position: -640px -500px; } -.emoji-1F516 { background-position: -640px -520px; } -.emoji-1F517 { background-position: -640px -540px; } -.emoji-1F518 { background-position: -640px -560px; } -.emoji-1F519 { background-position: -640px -580px; } -.emoji-1F51A { background-position: -640px -600px; } -.emoji-1F51B { background-position: -640px -620px; } -.emoji-1F51C { background-position: 0 -640px; } -.emoji-1F51D { background-position: -20px -640px; } -.emoji-1F51E { background-position: -40px -640px; } -.emoji-1F51F { background-position: -60px -640px; } -.emoji-1F520 { background-position: -80px -640px; } -.emoji-1F521 { background-position: -100px -640px; } -.emoji-1F522 { background-position: -120px -640px; } -.emoji-1F523 { background-position: -140px -640px; } -.emoji-1F524 { background-position: -160px -640px; } -.emoji-1F525 { background-position: -180px -640px; } -.emoji-1F526 { background-position: -200px -640px; } -.emoji-1F527 { background-position: -220px -640px; } -.emoji-1F528 { background-position: -240px -640px; } -.emoji-1F529 { background-position: -260px -640px; } -.emoji-1F52A { background-position: -280px -640px; } -.emoji-1F52B { background-position: -300px -640px; } -.emoji-1F52C { background-position: -320px -640px; } -.emoji-1F52D { background-position: -340px -640px; } -.emoji-1F52E { background-position: -360px -640px; } -.emoji-1F52F { background-position: -380px -640px; } -.emoji-1F530 { background-position: -400px -640px; } -.emoji-1F531 { background-position: -420px -640px; } -.emoji-1F532 { background-position: -440px -640px; } -.emoji-1F533 { background-position: -460px -640px; } -.emoji-1F534 { background-position: -480px -640px; } -.emoji-1F535 { background-position: -500px -640px; } -.emoji-1F536 { background-position: -520px -640px; } -.emoji-1F537 { background-position: -540px -640px; } -.emoji-1F538 { background-position: -560px -640px; } -.emoji-1F539 { background-position: -580px -640px; } -.emoji-1F53A { background-position: -600px -640px; } -.emoji-1F53B { background-position: -620px -640px; } -.emoji-1F53C { background-position: -640px -640px; } -.emoji-1F53D { background-position: -660px 0; } -.emoji-1F549 { background-position: -660px -20px; } -.emoji-1F54A { background-position: -660px -40px; } -.emoji-1F54B { background-position: -660px -60px; } -.emoji-1F54C { background-position: -660px -80px; } -.emoji-1F54D { background-position: -660px -100px; } -.emoji-1F54E { background-position: -660px -120px; } -.emoji-1F550 { background-position: -660px -140px; } -.emoji-1F551 { background-position: -660px -160px; } -.emoji-1F552 { background-position: -660px -180px; } -.emoji-1F553 { background-position: -660px -200px; } -.emoji-1F554 { background-position: -660px -220px; } -.emoji-1F555 { background-position: -660px -240px; } -.emoji-1F556 { background-position: -660px -260px; } -.emoji-1F557 { background-position: -660px -280px; } -.emoji-1F558 { background-position: -660px -300px; } -.emoji-1F559 { background-position: -660px -320px; } -.emoji-1F55A { background-position: -660px -340px; } -.emoji-1F55B { background-position: -660px -360px; } -.emoji-1F55C { background-position: -660px -380px; } -.emoji-1F55D { background-position: -660px -400px; } -.emoji-1F55E { background-position: -660px -420px; } -.emoji-1F55F { background-position: -660px -440px; } -.emoji-1F560 { background-position: -660px -460px; } -.emoji-1F561 { background-position: -660px -480px; } -.emoji-1F562 { background-position: -660px -500px; } -.emoji-1F563 { background-position: -660px -520px; } -.emoji-1F564 { background-position: -660px -540px; } -.emoji-1F565 { background-position: -660px -560px; } -.emoji-1F566 { background-position: -660px -580px; } -.emoji-1F567 { background-position: -660px -600px; } -.emoji-1F56F { background-position: -660px -620px; } -.emoji-1F570 { background-position: -660px -640px; } -.emoji-1F573 { background-position: 0 -660px; } -.emoji-1F574 { background-position: -20px -660px; } -.emoji-1F575 { background-position: -40px -660px; } -.emoji-1F575-1F3FB { background-position: -60px -660px; } -.emoji-1F575-1F3FC { background-position: -80px -660px; } -.emoji-1F575-1F3FD { background-position: -100px -660px; } -.emoji-1F575-1F3FE { background-position: -120px -660px; } -.emoji-1F575-1F3FF { background-position: -140px -660px; } -.emoji-1F576 { background-position: -160px -660px; } -.emoji-1F577 { background-position: -180px -660px; } -.emoji-1F578 { background-position: -200px -660px; } -.emoji-1F579 { background-position: -220px -660px; } -.emoji-1F57A { background-position: -240px -660px; } -.emoji-1F57A-1F3FB { background-position: -260px -660px; } -.emoji-1F57A-1F3FC { background-position: -280px -660px; } -.emoji-1F57A-1F3FD { background-position: -300px -660px; } -.emoji-1F57A-1F3FE { background-position: -320px -660px; } -.emoji-1F57A-1F3FF { background-position: -340px -660px; } -.emoji-1F587 { background-position: -360px -660px; } -.emoji-1F58A { background-position: -380px -660px; } -.emoji-1F58B { background-position: -400px -660px; } -.emoji-1F58C { background-position: -420px -660px; } -.emoji-1F58D { background-position: -440px -660px; } -.emoji-1F590 { background-position: -460px -660px; } -.emoji-1F590-1F3FB { background-position: -480px -660px; } -.emoji-1F590-1F3FC { background-position: -500px -660px; } -.emoji-1F590-1F3FD { background-position: -520px -660px; } -.emoji-1F590-1F3FE { background-position: -540px -660px; } -.emoji-1F590-1F3FF { background-position: -560px -660px; } -.emoji-1F595 { background-position: -580px -660px; } -.emoji-1F595-1F3FB { background-position: -600px -660px; } -.emoji-1F595-1F3FC { background-position: -620px -660px; } -.emoji-1F595-1F3FD { background-position: -640px -660px; } -.emoji-1F595-1F3FE { background-position: -660px -660px; } -.emoji-1F595-1F3FF { background-position: -680px 0; } -.emoji-1F596 { background-position: -680px -20px; } -.emoji-1F596-1F3FB { background-position: -680px -40px; } -.emoji-1F596-1F3FC { background-position: -680px -60px; } -.emoji-1F596-1F3FD { background-position: -680px -80px; } -.emoji-1F596-1F3FE { background-position: -680px -100px; } -.emoji-1F596-1F3FF { background-position: -680px -120px; } -.emoji-1F5A4 { background-position: -680px -140px; } -.emoji-1F5A5 { background-position: -680px -160px; } -.emoji-1F5A8 { background-position: -680px -180px; } -.emoji-1F5B1 { background-position: -680px -200px; } -.emoji-1F5B2 { background-position: -680px -220px; } -.emoji-1F5BC { background-position: -680px -240px; } -.emoji-1F5C2 { background-position: -680px -260px; } -.emoji-1F5C3 { background-position: -680px -280px; } -.emoji-1F5C4 { background-position: -680px -300px; } -.emoji-1F5D1 { background-position: -680px -320px; } -.emoji-1F5D2 { background-position: -680px -340px; } -.emoji-1F5D3 { background-position: -680px -360px; } -.emoji-1F5DC { background-position: -680px -380px; } -.emoji-1F5DD { background-position: -680px -400px; } -.emoji-1F5DE { background-position: -680px -420px; } -.emoji-1F5E1 { background-position: -680px -440px; } -.emoji-1F5E3 { background-position: -680px -460px; } -.emoji-1F5EF { background-position: -680px -480px; } -.emoji-1F5F3 { background-position: -680px -500px; } -.emoji-1F5FA { background-position: -680px -520px; } -.emoji-1F5FB { background-position: -680px -540px; } -.emoji-1F5FC { background-position: -680px -560px; } -.emoji-1F5FD { background-position: -680px -580px; } -.emoji-1F5FE { background-position: -680px -600px; } -.emoji-1F5FF { background-position: -680px -620px; } -.emoji-1F600 { background-position: -680px -640px; } -.emoji-1F601 { background-position: -680px -660px; } -.emoji-1F602 { background-position: 0 -680px; } -.emoji-1F603 { background-position: -20px -680px; } -.emoji-1F604 { background-position: -40px -680px; } -.emoji-1F605 { background-position: -60px -680px; } -.emoji-1F606 { background-position: -80px -680px; } -.emoji-1F607 { background-position: -100px -680px; } -.emoji-1F608 { background-position: -120px -680px; } -.emoji-1F609 { background-position: -140px -680px; } -.emoji-1F60A { background-position: -160px -680px; } -.emoji-1F60B { background-position: -180px -680px; } -.emoji-1F60C { background-position: -200px -680px; } -.emoji-1F60D { background-position: -220px -680px; } -.emoji-1F60E { background-position: -240px -680px; } -.emoji-1F60F { background-position: -260px -680px; } -.emoji-1F610 { background-position: -280px -680px; } -.emoji-1F611 { background-position: -300px -680px; } -.emoji-1F612 { background-position: -320px -680px; } -.emoji-1F613 { background-position: -340px -680px; } -.emoji-1F614 { background-position: -360px -680px; } -.emoji-1F615 { background-position: -380px -680px; } -.emoji-1F616 { background-position: -400px -680px; } -.emoji-1F617 { background-position: -420px -680px; } -.emoji-1F618 { background-position: -440px -680px; } -.emoji-1F619 { background-position: -460px -680px; } -.emoji-1F61A { background-position: -480px -680px; } -.emoji-1F61B { background-position: -500px -680px; } -.emoji-1F61C { background-position: -520px -680px; } -.emoji-1F61D { background-position: -540px -680px; } -.emoji-1F61E { background-position: -560px -680px; } -.emoji-1F61F { background-position: -580px -680px; } -.emoji-1F620 { background-position: -600px -680px; } -.emoji-1F621 { background-position: -620px -680px; } -.emoji-1F622 { background-position: -640px -680px; } -.emoji-1F623 { background-position: -660px -680px; } -.emoji-1F624 { background-position: -680px -680px; } -.emoji-1F625 { background-position: -700px 0; } -.emoji-1F626 { background-position: -700px -20px; } -.emoji-1F627 { background-position: -700px -40px; } -.emoji-1F628 { background-position: -700px -60px; } -.emoji-1F629 { background-position: -700px -80px; } -.emoji-1F62A { background-position: -700px -100px; } -.emoji-1F62B { background-position: -700px -120px; } -.emoji-1F62C { background-position: -700px -140px; } -.emoji-1F62D { background-position: -700px -160px; } -.emoji-1F62E { background-position: -700px -180px; } -.emoji-1F62F { background-position: -700px -200px; } -.emoji-1F630 { background-position: -700px -220px; } -.emoji-1F631 { background-position: -700px -240px; } -.emoji-1F632 { background-position: -700px -260px; } -.emoji-1F633 { background-position: -700px -280px; } -.emoji-1F634 { background-position: -700px -300px; } -.emoji-1F635 { background-position: -700px -320px; } -.emoji-1F636 { background-position: -700px -340px; } -.emoji-1F637 { background-position: -700px -360px; } -.emoji-1F638 { background-position: -700px -380px; } -.emoji-1F639 { background-position: -700px -400px; } -.emoji-1F63A { background-position: -700px -420px; } -.emoji-1F63B { background-position: -700px -440px; } -.emoji-1F63C { background-position: -700px -460px; } -.emoji-1F63D { background-position: -700px -480px; } -.emoji-1F63E { background-position: -700px -500px; } -.emoji-1F63F { background-position: -700px -520px; } -.emoji-1F640 { background-position: -700px -540px; } -.emoji-1F641 { background-position: -700px -560px; } -.emoji-1F642 { background-position: -700px -580px; } -.emoji-1F643 { background-position: -700px -600px; } -.emoji-1F644 { background-position: -700px -620px; } -.emoji-1F645 { background-position: -700px -640px; } -.emoji-1F645-1F3FB { background-position: -700px -660px; } -.emoji-1F645-1F3FC { background-position: -700px -680px; } -.emoji-1F645-1F3FD { background-position: 0 -700px; } -.emoji-1F645-1F3FE { background-position: -20px -700px; } -.emoji-1F645-1F3FF { background-position: -40px -700px; } -.emoji-1F646 { background-position: -60px -700px; } -.emoji-1F646-1F3FB { background-position: -80px -700px; } -.emoji-1F646-1F3FC { background-position: -100px -700px; } -.emoji-1F646-1F3FD { background-position: -120px -700px; } -.emoji-1F646-1F3FE { background-position: -140px -700px; } -.emoji-1F646-1F3FF { background-position: -160px -700px; } -.emoji-1F647 { background-position: -180px -700px; } -.emoji-1F647-1F3FB { background-position: -200px -700px; } -.emoji-1F647-1F3FC { background-position: -220px -700px; } -.emoji-1F647-1F3FD { background-position: -240px -700px; } -.emoji-1F647-1F3FE { background-position: -260px -700px; } -.emoji-1F647-1F3FF { background-position: -280px -700px; } -.emoji-1F648 { background-position: -300px -700px; } -.emoji-1F649 { background-position: -320px -700px; } -.emoji-1F64A { background-position: -340px -700px; } -.emoji-1F64B { background-position: -360px -700px; } -.emoji-1F64B-1F3FB { background-position: -380px -700px; } -.emoji-1F64B-1F3FC { background-position: -400px -700px; } -.emoji-1F64B-1F3FD { background-position: -420px -700px; } -.emoji-1F64B-1F3FE { background-position: -440px -700px; } -.emoji-1F64B-1F3FF { background-position: -460px -700px; } -.emoji-1F64C { background-position: -480px -700px; } -.emoji-1F64C-1F3FB { background-position: -500px -700px; } -.emoji-1F64C-1F3FC { background-position: -520px -700px; } -.emoji-1F64C-1F3FD { background-position: -540px -700px; } -.emoji-1F64C-1F3FE { background-position: -560px -700px; } -.emoji-1F64C-1F3FF { background-position: -580px -700px; } -.emoji-1F64D { background-position: -600px -700px; } -.emoji-1F64D-1F3FB { background-position: -620px -700px; } -.emoji-1F64D-1F3FC { background-position: -640px -700px; } -.emoji-1F64D-1F3FD { background-position: -660px -700px; } -.emoji-1F64D-1F3FE { background-position: -680px -700px; } -.emoji-1F64D-1F3FF { background-position: -700px -700px; } -.emoji-1F64E { background-position: -720px 0; } -.emoji-1F64E-1F3FB { background-position: -720px -20px; } -.emoji-1F64E-1F3FC { background-position: -720px -40px; } -.emoji-1F64E-1F3FD { background-position: -720px -60px; } -.emoji-1F64E-1F3FE { background-position: -720px -80px; } -.emoji-1F64E-1F3FF { background-position: -720px -100px; } -.emoji-1F64F { background-position: -720px -120px; } -.emoji-1F64F-1F3FB { background-position: -720px -140px; } -.emoji-1F64F-1F3FC { background-position: -720px -160px; } -.emoji-1F64F-1F3FD { background-position: -720px -180px; } -.emoji-1F64F-1F3FE { background-position: -720px -200px; } -.emoji-1F64F-1F3FF { background-position: -720px -220px; } -.emoji-1F680 { background-position: -720px -240px; } -.emoji-1F681 { background-position: -720px -260px; } -.emoji-1F682 { background-position: -720px -280px; } -.emoji-1F683 { background-position: -720px -300px; } -.emoji-1F684 { background-position: -720px -320px; } -.emoji-1F685 { background-position: -720px -340px; } -.emoji-1F686 { background-position: -720px -360px; } -.emoji-1F687 { background-position: -720px -380px; } -.emoji-1F688 { background-position: -720px -400px; } -.emoji-1F689 { background-position: -720px -420px; } -.emoji-1F68A { background-position: -720px -440px; } -.emoji-1F68B { background-position: -720px -460px; } -.emoji-1F68C { background-position: -720px -480px; } -.emoji-1F68D { background-position: -720px -500px; } -.emoji-1F68E { background-position: -720px -520px; } -.emoji-1F68F { background-position: -720px -540px; } -.emoji-1F690 { background-position: -720px -560px; } -.emoji-1F691 { background-position: -720px -580px; } -.emoji-1F692 { background-position: -720px -600px; } -.emoji-1F693 { background-position: -720px -620px; } -.emoji-1F694 { background-position: -720px -640px; } -.emoji-1F695 { background-position: -720px -660px; } -.emoji-1F696 { background-position: -720px -680px; } -.emoji-1F697 { background-position: -720px -700px; } -.emoji-1F698 { background-position: 0 -720px; } -.emoji-1F699 { background-position: -20px -720px; } -.emoji-1F69A { background-position: -40px -720px; } -.emoji-1F69B { background-position: -60px -720px; } -.emoji-1F69C { background-position: -80px -720px; } -.emoji-1F69D { background-position: -100px -720px; } -.emoji-1F69E { background-position: -120px -720px; } -.emoji-1F69F { background-position: -140px -720px; } -.emoji-1F6A0 { background-position: -160px -720px; } -.emoji-1F6A1 { background-position: -180px -720px; } -.emoji-1F6A2 { background-position: -200px -720px; } -.emoji-1F6A3 { background-position: -220px -720px; } -.emoji-1F6A3-1F3FB { background-position: -240px -720px; } -.emoji-1F6A3-1F3FC { background-position: -260px -720px; } -.emoji-1F6A3-1F3FD { background-position: -280px -720px; } -.emoji-1F6A3-1F3FE { background-position: -300px -720px; } -.emoji-1F6A3-1F3FF { background-position: -320px -720px; } -.emoji-1F6A4 { background-position: -340px -720px; } -.emoji-1F6A5 { background-position: -360px -720px; } -.emoji-1F6A6 { background-position: -380px -720px; } -.emoji-1F6A7 { background-position: -400px -720px; } -.emoji-1F6A8 { background-position: -420px -720px; } -.emoji-1F6A9 { background-position: -440px -720px; } -.emoji-1F6AA { background-position: -460px -720px; } -.emoji-1F6AB { background-position: -480px -720px; } -.emoji-1F6AC { background-position: -500px -720px; } -.emoji-1F6AD { background-position: -520px -720px; } -.emoji-1F6AE { background-position: -540px -720px; } -.emoji-1F6AF { background-position: -560px -720px; } -.emoji-1F6B0 { background-position: -580px -720px; } -.emoji-1F6B1 { background-position: -600px -720px; } -.emoji-1F6B2 { background-position: -620px -720px; } -.emoji-1F6B3 { background-position: -640px -720px; } -.emoji-1F6B4 { background-position: -660px -720px; } -.emoji-1F6B4-1F3FB { background-position: -680px -720px; } -.emoji-1F6B4-1F3FC { background-position: -700px -720px; } -.emoji-1F6B4-1F3FD { background-position: -720px -720px; } -.emoji-1F6B4-1F3FE { background-position: -740px 0; } -.emoji-1F6B4-1F3FF { background-position: -740px -20px; } -.emoji-1F6B5 { background-position: -740px -40px; } -.emoji-1F6B5-1F3FB { background-position: -740px -60px; } -.emoji-1F6B5-1F3FC { background-position: -740px -80px; } -.emoji-1F6B5-1F3FD { background-position: -740px -100px; } -.emoji-1F6B5-1F3FE { background-position: -740px -120px; } -.emoji-1F6B5-1F3FF { background-position: -740px -140px; } -.emoji-1F6B6 { background-position: -740px -160px; } -.emoji-1F6B6-1F3FB { background-position: -740px -180px; } -.emoji-1F6B6-1F3FC { background-position: -740px -200px; } -.emoji-1F6B6-1F3FD { background-position: -740px -220px; } -.emoji-1F6B6-1F3FE { background-position: -740px -240px; } -.emoji-1F6B6-1F3FF { background-position: -740px -260px; } -.emoji-1F6B7 { background-position: -740px -280px; } -.emoji-1F6B8 { background-position: -740px -300px; } -.emoji-1F6B9 { background-position: -740px -320px; } -.emoji-1F6BA { background-position: -740px -340px; } -.emoji-1F6BB { background-position: -740px -360px; } -.emoji-1F6BC { background-position: -740px -380px; } -.emoji-1F6BD { background-position: -740px -400px; } -.emoji-1F6BE { background-position: -740px -420px; } -.emoji-1F6BF { background-position: -740px -440px; } -.emoji-1F6C0 { background-position: -740px -460px; } -.emoji-1F6C0-1F3FB { background-position: -740px -480px; } -.emoji-1F6C0-1F3FC { background-position: -740px -500px; } -.emoji-1F6C0-1F3FD { background-position: -740px -520px; } -.emoji-1F6C0-1F3FE { background-position: -740px -540px; } -.emoji-1F6C0-1F3FF { background-position: -740px -560px; } -.emoji-1F6C1 { background-position: -740px -580px; } -.emoji-1F6C2 { background-position: -740px -600px; } -.emoji-1F6C3 { background-position: -740px -620px; } -.emoji-1F6C4 { background-position: -740px -640px; } -.emoji-1F6C5 { background-position: -740px -660px; } -.emoji-1F6CB { background-position: -740px -680px; } -.emoji-1F6CC { background-position: -740px -700px; } -.emoji-1F6CD { background-position: -740px -720px; } -.emoji-1F6CE { background-position: 0 -740px; } -.emoji-1F6CF { background-position: -20px -740px; } -.emoji-1F6D0 { background-position: -40px -740px; } -.emoji-1F6D1 { background-position: -60px -740px; } -.emoji-1F6D2 { background-position: -80px -740px; } -.emoji-1F6E0 { background-position: -100px -740px; } -.emoji-1F6E1 { background-position: -120px -740px; } -.emoji-1F6E2 { background-position: -140px -740px; } -.emoji-1F6E3 { background-position: -160px -740px; } -.emoji-1F6E4 { background-position: -180px -740px; } -.emoji-1F6E5 { background-position: -200px -740px; } -.emoji-1F6E9 { background-position: -220px -740px; } -.emoji-1F6EB { background-position: -240px -740px; } -.emoji-1F6EC { background-position: -260px -740px; } -.emoji-1F6F0 { background-position: -280px -740px; } -.emoji-1F6F3 { background-position: -300px -740px; } -.emoji-1F6F4 { background-position: -320px -740px; } -.emoji-1F6F5 { background-position: -340px -740px; } -.emoji-1F6F6 { background-position: -360px -740px; } -.emoji-1F910 { background-position: -380px -740px; } -.emoji-1F911 { background-position: -400px -740px; } -.emoji-1F912 { background-position: -420px -740px; } -.emoji-1F913 { background-position: -440px -740px; } -.emoji-1F914 { background-position: -460px -740px; } -.emoji-1F915 { background-position: -480px -740px; } -.emoji-1F916 { background-position: -500px -740px; } -.emoji-1F917 { background-position: -520px -740px; } -.emoji-1F918 { background-position: -540px -740px; } -.emoji-1F918-1F3FB { background-position: -560px -740px; } -.emoji-1F918-1F3FC { background-position: -580px -740px; } -.emoji-1F918-1F3FD { background-position: -600px -740px; } -.emoji-1F918-1F3FE { background-position: -620px -740px; } -.emoji-1F918-1F3FF { background-position: -640px -740px; } -.emoji-1F919 { background-position: -660px -740px; } -.emoji-1F919-1F3FB { background-position: -680px -740px; } -.emoji-1F919-1F3FC { background-position: -700px -740px; } -.emoji-1F919-1F3FD { background-position: -720px -740px; } -.emoji-1F919-1F3FE { background-position: -740px -740px; } -.emoji-1F919-1F3FF { background-position: -760px 0; } -.emoji-1F91A { background-position: -760px -20px; } -.emoji-1F91A-1F3FB { background-position: -760px -40px; } -.emoji-1F91A-1F3FC { background-position: -760px -60px; } -.emoji-1F91A-1F3FD { background-position: -760px -80px; } -.emoji-1F91A-1F3FE { background-position: -760px -100px; } -.emoji-1F91A-1F3FF { background-position: -760px -120px; } -.emoji-1F91B { background-position: -760px -140px; } -.emoji-1F91B-1F3FB { background-position: -760px -160px; } -.emoji-1F91B-1F3FC { background-position: -760px -180px; } -.emoji-1F91B-1F3FD { background-position: -760px -200px; } -.emoji-1F91B-1F3FE { background-position: -760px -220px; } -.emoji-1F91B-1F3FF { background-position: -760px -240px; } -.emoji-1F91C { background-position: -760px -260px; } -.emoji-1F91C-1F3FB { background-position: -760px -280px; } -.emoji-1F91C-1F3FC { background-position: -760px -300px; } -.emoji-1F91C-1F3FD { background-position: -760px -320px; } -.emoji-1F91C-1F3FE { background-position: -760px -340px; } -.emoji-1F91C-1F3FF { background-position: -760px -360px; } -.emoji-1F91D { background-position: -760px -380px; } -.emoji-1F91D-1F3FB { background-position: -760px -400px; } -.emoji-1F91D-1F3FC { background-position: -760px -420px; } -.emoji-1F91D-1F3FD { background-position: -760px -440px; } -.emoji-1F91D-1F3FE { background-position: -760px -460px; } -.emoji-1F91D-1F3FF { background-position: -760px -480px; } -.emoji-1F91E { background-position: -760px -500px; } -.emoji-1F91E-1F3FB { background-position: -760px -520px; } -.emoji-1F91E-1F3FC { background-position: -760px -540px; } -.emoji-1F91E-1F3FD { background-position: -760px -560px; } -.emoji-1F91E-1F3FE { background-position: -760px -580px; } -.emoji-1F91E-1F3FF { background-position: -760px -600px; } -.emoji-1F920 { background-position: -760px -620px; } -.emoji-1F921 { background-position: -760px -640px; } -.emoji-1F922 { background-position: -760px -660px; } -.emoji-1F923 { background-position: -760px -680px; } -.emoji-1F924 { background-position: -760px -700px; } -.emoji-1F925 { background-position: -760px -720px; } -.emoji-1F926 { background-position: -760px -740px; } -.emoji-1F926-1F3FB { background-position: 0 -760px; } -.emoji-1F926-1F3FC { background-position: -20px -760px; } -.emoji-1F926-1F3FD { background-position: -40px -760px; } -.emoji-1F926-1F3FE { background-position: -60px -760px; } -.emoji-1F926-1F3FF { background-position: -80px -760px; } -.emoji-1F927 { background-position: -100px -760px; } -.emoji-1F930 { background-position: -120px -760px; } -.emoji-1F930-1F3FB { background-position: -140px -760px; } -.emoji-1F930-1F3FC { background-position: -160px -760px; } -.emoji-1F930-1F3FD { background-position: -180px -760px; } -.emoji-1F930-1F3FE { background-position: -200px -760px; } -.emoji-1F930-1F3FF { background-position: -220px -760px; } -.emoji-1F933 { background-position: -240px -760px; } -.emoji-1F933-1F3FB { background-position: -260px -760px; } -.emoji-1F933-1F3FC { background-position: -280px -760px; } -.emoji-1F933-1F3FD { background-position: -300px -760px; } -.emoji-1F933-1F3FE { background-position: -320px -760px; } -.emoji-1F933-1F3FF { background-position: -340px -760px; } -.emoji-1F934 { background-position: -360px -760px; } -.emoji-1F934-1F3FB { background-position: -380px -760px; } -.emoji-1F934-1F3FC { background-position: -400px -760px; } -.emoji-1F934-1F3FD { background-position: -420px -760px; } -.emoji-1F934-1F3FE { background-position: -440px -760px; } -.emoji-1F934-1F3FF { background-position: -460px -760px; } -.emoji-1F935 { background-position: -480px -760px; } -.emoji-1F935-1F3FB { background-position: -500px -760px; } -.emoji-1F935-1F3FC { background-position: -520px -760px; } -.emoji-1F935-1F3FD { background-position: -540px -760px; } -.emoji-1F935-1F3FE { background-position: -560px -760px; } -.emoji-1F935-1F3FF { background-position: -580px -760px; } -.emoji-1F936 { background-position: -600px -760px; } -.emoji-1F936-1F3FB { background-position: -620px -760px; } -.emoji-1F936-1F3FC { background-position: -640px -760px; } -.emoji-1F936-1F3FD { background-position: -660px -760px; } -.emoji-1F936-1F3FE { background-position: -680px -760px; } -.emoji-1F936-1F3FF { background-position: -700px -760px; } -.emoji-1F937 { background-position: -720px -760px; } -.emoji-1F937-1F3FB { background-position: -740px -760px; } -.emoji-1F937-1F3FC { background-position: -760px -760px; } -.emoji-1F937-1F3FD { background-position: -780px 0; } -.emoji-1F937-1F3FE { background-position: -780px -20px; } -.emoji-1F937-1F3FF { background-position: -780px -40px; } -.emoji-1F938 { background-position: -780px -60px; } -.emoji-1F938-1F3FB { background-position: -780px -80px; } -.emoji-1F938-1F3FC { background-position: -780px -100px; } -.emoji-1F938-1F3FD { background-position: -780px -120px; } -.emoji-1F938-1F3FE { background-position: -780px -140px; } -.emoji-1F938-1F3FF { background-position: -780px -160px; } -.emoji-1F939 { background-position: -780px -180px; } -.emoji-1F939-1F3FB { background-position: -780px -200px; } -.emoji-1F939-1F3FC { background-position: -780px -220px; } -.emoji-1F939-1F3FD { background-position: -780px -240px; } -.emoji-1F939-1F3FE { background-position: -780px -260px; } -.emoji-1F939-1F3FF { background-position: -780px -280px; } -.emoji-1F93A { background-position: -780px -300px; } -.emoji-1F93C { background-position: -780px -320px; } -.emoji-1F93C-1F3FB { background-position: -780px -340px; } -.emoji-1F93C-1F3FC { background-position: -780px -360px; } -.emoji-1F93C-1F3FD { background-position: -780px -380px; } -.emoji-1F93C-1F3FE { background-position: -780px -400px; } -.emoji-1F93C-1F3FF { background-position: -780px -420px; } -.emoji-1F93D { background-position: -780px -440px; } -.emoji-1F93D-1F3FB { background-position: -780px -460px; } -.emoji-1F93D-1F3FC { background-position: -780px -480px; } -.emoji-1F93D-1F3FD { background-position: -780px -500px; } -.emoji-1F93D-1F3FE { background-position: -780px -520px; } -.emoji-1F93D-1F3FF { background-position: -780px -540px; } -.emoji-1F93E { background-position: -780px -560px; } -.emoji-1F93E-1F3FB { background-position: -780px -580px; } -.emoji-1F93E-1F3FC { background-position: -780px -600px; } -.emoji-1F93E-1F3FD { background-position: -780px -620px; } -.emoji-1F93E-1F3FE { background-position: -780px -640px; } -.emoji-1F93E-1F3FF { background-position: -780px -660px; } -.emoji-1F940 { background-position: -780px -680px; } -.emoji-1F941 { background-position: -780px -700px; } -.emoji-1F942 { background-position: -780px -720px; } -.emoji-1F943 { background-position: -780px -740px; } -.emoji-1F944 { background-position: -780px -760px; } -.emoji-1F945 { background-position: 0 -780px; } -.emoji-1F947 { background-position: -20px -780px; } -.emoji-1F948 { background-position: -40px -780px; } -.emoji-1F949 { background-position: -60px -780px; } -.emoji-1F94A { background-position: -80px -780px; } -.emoji-1F94B { background-position: -100px -780px; } -.emoji-1F950 { background-position: -120px -780px; } -.emoji-1F951 { background-position: -140px -780px; } -.emoji-1F952 { background-position: -160px -780px; } -.emoji-1F953 { background-position: -180px -780px; } -.emoji-1F954 { background-position: -200px -780px; } -.emoji-1F955 { background-position: -220px -780px; } -.emoji-1F956 { background-position: -240px -780px; } -.emoji-1F957 { background-position: -260px -780px; } -.emoji-1F958 { background-position: -280px -780px; } -.emoji-1F959 { background-position: -300px -780px; } -.emoji-1F95A { background-position: -320px -780px; } -.emoji-1F95B { background-position: -340px -780px; } -.emoji-1F95C { background-position: -360px -780px; } -.emoji-1F95D { background-position: -380px -780px; } -.emoji-1F95E { background-position: -400px -780px; } -.emoji-1F980 { background-position: -420px -780px; } -.emoji-1F981 { background-position: -440px -780px; } -.emoji-1F982 { background-position: -460px -780px; } -.emoji-1F983 { background-position: -480px -780px; } -.emoji-1F984 { background-position: -500px -780px; } -.emoji-1F985 { background-position: -520px -780px; } -.emoji-1F986 { background-position: -540px -780px; } -.emoji-1F987 { background-position: -560px -780px; } -.emoji-1F988 { background-position: -580px -780px; } -.emoji-1F989 { background-position: -600px -780px; } -.emoji-1F98A { background-position: -620px -780px; } -.emoji-1F98B { background-position: -640px -780px; } -.emoji-1F98C { background-position: -660px -780px; } -.emoji-1F98D { background-position: -680px -780px; } -.emoji-1F98E { background-position: -700px -780px; } -.emoji-1F98F { background-position: -720px -780px; } -.emoji-1F990 { background-position: -740px -780px; } -.emoji-1F991 { background-position: -760px -780px; } -.emoji-1F9C0 { background-position: -780px -780px; } -.emoji-203C { background-position: -800px 0; } -.emoji-2049 { background-position: -800px -20px; } -.emoji-2122 { background-position: -800px -40px; } -.emoji-2139 { background-position: -800px -60px; } -.emoji-2194 { background-position: -800px -80px; } -.emoji-2195 { background-position: -800px -100px; } -.emoji-2196 { background-position: -800px -120px; } -.emoji-2197 { background-position: -800px -140px; } -.emoji-2198 { background-position: -800px -160px; } -.emoji-2199 { background-position: -800px -180px; } -.emoji-21A9 { background-position: -800px -200px; } -.emoji-21AA { background-position: -800px -220px; } -.emoji-231A { background-position: -800px -240px; } -.emoji-231B { background-position: -800px -260px; } -.emoji-2328 { background-position: -800px -280px; } -.emoji-23CF { background-position: -800px -300px; } -.emoji-23E9 { background-position: -800px -320px; } -.emoji-23EA { background-position: -800px -340px; } -.emoji-23EB { background-position: -800px -360px; } -.emoji-23EC { background-position: -800px -380px; } -.emoji-23ED { background-position: -800px -400px; } -.emoji-23EE { background-position: -800px -420px; } -.emoji-23EF { background-position: -800px -440px; } -.emoji-23F0 { background-position: -800px -460px; } -.emoji-23F1 { background-position: -800px -480px; } -.emoji-23F2 { background-position: -800px -500px; } -.emoji-23F3 { background-position: -800px -520px; } -.emoji-23F8 { background-position: -800px -540px; } -.emoji-23F9 { background-position: -800px -560px; } -.emoji-23FA { background-position: -800px -580px; } -.emoji-24C2 { background-position: -800px -600px; } -.emoji-25AA { background-position: -800px -620px; } -.emoji-25AB { background-position: -800px -640px; } -.emoji-25B6 { background-position: -800px -660px; } -.emoji-25C0 { background-position: -800px -680px; } -.emoji-25FB { background-position: -800px -700px; } -.emoji-25FC { background-position: -800px -720px; } -.emoji-25FD { background-position: -800px -740px; } -.emoji-25FE { background-position: -800px -760px; } -.emoji-2600 { background-position: -800px -780px; } -.emoji-2601 { background-position: 0 -800px; } -.emoji-2602 { background-position: -20px -800px; } -.emoji-2603 { background-position: -40px -800px; } -.emoji-2604 { background-position: -60px -800px; } -.emoji-260E { background-position: -80px -800px; } -.emoji-2611 { background-position: -100px -800px; } -.emoji-2614 { background-position: -120px -800px; } -.emoji-2615 { background-position: -140px -800px; } -.emoji-2618 { background-position: -160px -800px; } -.emoji-261D { background-position: -180px -800px; } -.emoji-261D-1F3FB { background-position: -200px -800px; } -.emoji-261D-1F3FC { background-position: -220px -800px; } -.emoji-261D-1F3FD { background-position: -240px -800px; } -.emoji-261D-1F3FE { background-position: -260px -800px; } -.emoji-261D-1F3FF { background-position: -280px -800px; } -.emoji-2620 { background-position: -300px -800px; } -.emoji-2622 { background-position: -320px -800px; } -.emoji-2623 { background-position: -340px -800px; } -.emoji-2626 { background-position: -360px -800px; } -.emoji-262A { background-position: -380px -800px; } -.emoji-262E { background-position: -400px -800px; } -.emoji-262F { background-position: -420px -800px; } -.emoji-2638 { background-position: -440px -800px; } -.emoji-2639 { background-position: -460px -800px; } -.emoji-263A { background-position: -480px -800px; } -.emoji-2648 { background-position: -500px -800px; } -.emoji-2649 { background-position: -520px -800px; } -.emoji-264A { background-position: -540px -800px; } -.emoji-264B { background-position: -560px -800px; } -.emoji-264C { background-position: -580px -800px; } -.emoji-264D { background-position: -600px -800px; } -.emoji-264E { background-position: -620px -800px; } -.emoji-264F { background-position: -640px -800px; } -.emoji-2650 { background-position: -660px -800px; } -.emoji-2651 { background-position: -680px -800px; } -.emoji-2652 { background-position: -700px -800px; } -.emoji-2653 { background-position: -720px -800px; } -.emoji-2660 { background-position: -740px -800px; } -.emoji-2663 { background-position: -760px -800px; } -.emoji-2665 { background-position: -780px -800px; } -.emoji-2666 { background-position: -800px -800px; } -.emoji-2668 { background-position: -820px 0; } -.emoji-267B { background-position: -820px -20px; } -.emoji-267F { background-position: -820px -40px; } -.emoji-2692 { background-position: -820px -60px; } -.emoji-2693 { background-position: -820px -80px; } -.emoji-2694 { background-position: -820px -100px; } -.emoji-2696 { background-position: -820px -120px; } -.emoji-2697 { background-position: -820px -140px; } -.emoji-2699 { background-position: -820px -160px; } -.emoji-269B { background-position: -820px -180px; } -.emoji-269C { background-position: -820px -200px; } -.emoji-26A0 { background-position: -820px -220px; } -.emoji-26A1 { background-position: -820px -240px; } -.emoji-26AA { background-position: -820px -260px; } -.emoji-26AB { background-position: -820px -280px; } -.emoji-26B0 { background-position: -820px -300px; } -.emoji-26B1 { background-position: -820px -320px; } -.emoji-26BD { background-position: -820px -340px; } -.emoji-26BE { background-position: -820px -360px; } -.emoji-26C4 { background-position: -820px -380px; } -.emoji-26C5 { background-position: -820px -400px; } -.emoji-26C8 { background-position: -820px -420px; } -.emoji-26CE { background-position: -820px -440px; } -.emoji-26CF { background-position: -820px -460px; } -.emoji-26D1 { background-position: -820px -480px; } -.emoji-26D3 { background-position: -820px -500px; } -.emoji-26D4 { background-position: -820px -520px; } -.emoji-26E9 { background-position: -820px -540px; } -.emoji-26EA { background-position: -820px -560px; } -.emoji-26F0 { background-position: -820px -580px; } -.emoji-26F1 { background-position: -820px -600px; } -.emoji-26F2 { background-position: -820px -620px; } -.emoji-26F3 { background-position: -820px -640px; } -.emoji-26F4 { background-position: -820px -660px; } -.emoji-26F5 { background-position: -820px -680px; } -.emoji-26F7 { background-position: -820px -700px; } -.emoji-26F8 { background-position: -820px -720px; } -.emoji-26F9 { background-position: -820px -740px; } -.emoji-26F9-1F3FB { background-position: -820px -760px; } -.emoji-26F9-1F3FC { background-position: -820px -780px; } -.emoji-26F9-1F3FD { background-position: -820px -800px; } -.emoji-26F9-1F3FE { background-position: 0 -820px; } -.emoji-26F9-1F3FF { background-position: -20px -820px; } -.emoji-26FA { background-position: -40px -820px; } -.emoji-26FD { background-position: -60px -820px; } -.emoji-2702 { background-position: -80px -820px; } -.emoji-2705 { background-position: -100px -820px; } -.emoji-2708 { background-position: -120px -820px; } -.emoji-2709 { background-position: -140px -820px; } -.emoji-270A { background-position: -160px -820px; } -.emoji-270A-1F3FB { background-position: -180px -820px; } -.emoji-270A-1F3FC { background-position: -200px -820px; } -.emoji-270A-1F3FD { background-position: -220px -820px; } -.emoji-270A-1F3FE { background-position: -240px -820px; } -.emoji-270A-1F3FF { background-position: -260px -820px; } -.emoji-270B { background-position: -280px -820px; } -.emoji-270B-1F3FB { background-position: -300px -820px; } -.emoji-270B-1F3FC { background-position: -320px -820px; } -.emoji-270B-1F3FD { background-position: -340px -820px; } -.emoji-270B-1F3FE { background-position: -360px -820px; } -.emoji-270B-1F3FF { background-position: -380px -820px; } -.emoji-270C { background-position: -400px -820px; } -.emoji-270C-1F3FB { background-position: -420px -820px; } -.emoji-270C-1F3FC { background-position: -440px -820px; } -.emoji-270C-1F3FD { background-position: -460px -820px; } -.emoji-270C-1F3FE { background-position: -480px -820px; } -.emoji-270C-1F3FF { background-position: -500px -820px; } -.emoji-270D { background-position: -520px -820px; } -.emoji-270D-1F3FB { background-position: -540px -820px; } -.emoji-270D-1F3FC { background-position: -560px -820px; } -.emoji-270D-1F3FD { background-position: -580px -820px; } -.emoji-270D-1F3FE { background-position: -600px -820px; } -.emoji-270D-1F3FF { background-position: -620px -820px; } -.emoji-270F { background-position: -640px -820px; } -.emoji-2712 { background-position: -660px -820px; } -.emoji-2714 { background-position: -680px -820px; } -.emoji-2716 { background-position: -700px -820px; } -.emoji-271D { background-position: -720px -820px; } -.emoji-2721 { background-position: -740px -820px; } -.emoji-2728 { background-position: -760px -820px; } -.emoji-2733 { background-position: -780px -820px; } -.emoji-2734 { background-position: -800px -820px; } -.emoji-2744 { background-position: -820px -820px; } -.emoji-2747 { background-position: -840px 0; } -.emoji-274C { background-position: -840px -20px; } -.emoji-274E { background-position: -840px -40px; } -.emoji-2753 { background-position: -840px -60px; } -.emoji-2754 { background-position: -840px -80px; } -.emoji-2755 { background-position: -840px -100px; } -.emoji-2757 { background-position: -840px -120px; } -.emoji-2763 { background-position: -840px -140px; } -.emoji-2764 { background-position: -840px -160px; } -.emoji-2795 { background-position: -840px -180px; } -.emoji-2796 { background-position: -840px -200px; } -.emoji-2797 { background-position: -840px -220px; } -.emoji-27A1 { background-position: -840px -240px; } -.emoji-27B0 { background-position: -840px -260px; } -.emoji-27BF { background-position: -840px -280px; } -.emoji-2934 { background-position: -840px -300px; } -.emoji-2935 { background-position: -840px -320px; } -.emoji-2B05 { background-position: -840px -340px; } -.emoji-2B06 { background-position: -840px -360px; } -.emoji-2B07 { background-position: -840px -380px; } -.emoji-2B1B { background-position: -840px -400px; } -.emoji-2B1C { background-position: -840px -420px; } -.emoji-2B50 { background-position: -840px -440px; } -.emoji-2B55 { background-position: -840px -460px; } -.emoji-3030 { background-position: -840px -480px; } -.emoji-303D { background-position: -840px -500px; } -.emoji-3297 { background-position: -840px -520px; } -.emoji-3299 { background-position: -840px -540px; } - -.emoji-icon { - background-image: image-url('emoji.png'); - background-repeat: no-repeat; - height: 20px; - width: 20px; - - @media only screen and (-webkit-min-device-pixel-ratio: 2), - only screen and (min--moz-device-pixel-ratio: 2), - only screen and (-o-min-device-pixel-ratio: 2/1), - only screen and (min-device-pixel-ratio: 2), - only screen and (min-resolution: 192dpi), - only screen and (min-resolution: 2dppx) { - background-image: image-url('emoji@2x.png'); - background-size: 860px 840px; - } -} diff --git a/app/assets/stylesheets/pages/explore.scss b/app/assets/stylesheets/pages/explore.scss deleted file mode 100644 index 9b92128624c..00000000000 --- a/app/assets/stylesheets/pages/explore.scss +++ /dev/null @@ -1,8 +0,0 @@ -.explore-title { - text-align: center; - - h3 { - font-weight: normal; - font-size: 30px; - } -} diff --git a/app/assets/stylesheets/pages/icons.scss b/app/assets/stylesheets/pages/icons.scss deleted file mode 100644 index 226bd2ead31..00000000000 --- a/app/assets/stylesheets/pages/icons.scss +++ /dev/null @@ -1,51 +0,0 @@ -.ci-status-icon-success { - color: $gl-success; - - svg { - fill: $gl-success; - } -} - -.ci-status-icon-failed { - color: $gl-danger; - - svg { - fill: $gl-danger; - } -} - -.ci-status-icon-pending, -.ci-status-icon-success_with_warnings { - color: $gl-warning; - - svg { - fill: $gl-warning; - } -} - -.ci-status-icon-running { - color: $blue-normal; - - svg { - fill: $blue-normal; - } -} - -.ci-status-icon-canceled, -.ci-status-icon-disabled, -.ci-status-icon-not-found { - color: $gl-gray; - - svg { - fill: $gl-gray; - } -} - -.ci-status-icon-created, -.ci-status-icon-skipped { - color: $gray-darkest; - - svg { - fill: $gray-darkest; - } -} diff --git a/app/views/explore/_head.html.haml b/app/views/explore/_head.html.haml index d8a57560788..a3b0709e261 100644 --- a/app/views/explore/_head.html.haml +++ b/app/views/explore/_head.html.haml @@ -1,5 +1,5 @@ -.explore-title - %h3 +.explore-title.text-center + %h2 Explore GitLab %p.lead Discover projects, groups and snippets. Share your projects with others -- cgit v1.2.1 From f1d30769f3c3123b93f1e11ea9a76399719435c9 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Tue, 6 Dec 2016 13:29:13 -0600 Subject: Remove unused bootstrap imports --- app/assets/stylesheets/framework/tw_bootstrap.scss | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index 55bc325b858..1a985ee2a66 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -21,7 +21,7 @@ @import "bootstrap/buttons"; // Components -@import "bootstrap/component-animations"; +// @import "bootstrap/component-animations"; // @import "bootstrap/dropdowns"; @import "bootstrap/button-groups"; @import "bootstrap/input-groups"; @@ -33,11 +33,11 @@ @import "bootstrap/labels"; @import "bootstrap/badges"; @import "bootstrap/alerts"; -@import "bootstrap/progress-bars"; +// @import "bootstrap/progress-bars"; @import "bootstrap/list-group"; -@import "bootstrap/wells"; -@import "bootstrap/close"; -@import "bootstrap/panels"; +// @import "bootstrap/wells"; +// @import "bootstrap/close"; +// @import "bootstrap/panels"; // Components w/ JavaScript @import "bootstrap/modals"; -- cgit v1.2.1 From 18489218443ea6b8c6fdfd9ab953ff7ef84f69db Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Wed, 7 Dec 2016 17:05:42 -0600 Subject: Remove pages/snippets css --- app/assets/stylesheets/framework.scss | 1 + app/assets/stylesheets/framework/emojis.scss | 2 +- app/assets/stylesheets/framework/snippets.scss | 48 ++++++++++++++++++ app/assets/stylesheets/framework/tw_bootstrap.scss | 6 +-- app/assets/stylesheets/pages/notifications.scss | 14 +----- app/assets/stylesheets/pages/snippets.scss | 58 ---------------------- app/views/projects/snippets/show.html.haml | 5 +- app/views/snippets/show.html.haml | 4 +- 8 files changed, 59 insertions(+), 79 deletions(-) create mode 100644 app/assets/stylesheets/framework/snippets.scss delete mode 100644 app/assets/stylesheets/pages/snippets.scss diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 928ef408722..40bc0579393 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -46,3 +46,4 @@ @import "framework/broadcast-messages"; @import "framework/emojis.scss"; @import "framework/icons.scss"; +@import "framework/snippets.scss"; diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss index f17797b2381..7158de65143 100644 --- a/app/assets/stylesheets/framework/emojis.scss +++ b/app/assets/stylesheets/framework/emojis.scss @@ -1,4 +1,4 @@ -.emoji-0023-20E3 { background-position: 0 0px; } +.emoji-0023-20E3 { background-position: 0 0; } .emoji-002A-20E3 { background-position: -20px 0; } .emoji-0030-20E3 { background-position: 0 -20px; } .emoji-0031-20E3 { background-position: -20px -20px; } diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss new file mode 100644 index 00000000000..5f7e1b17cc7 --- /dev/null +++ b/app/assets/stylesheets/framework/snippets.scss @@ -0,0 +1,48 @@ +.snippet-row { + .title { + margin-bottom: 2px; + } + + .snippet-filename { + padding: 0 2px; + } +} + +.snippet-form-holder .file-holder .file-title { + padding: 2px; +} + +.markdown-snippet-copy { + position: fixed; + top: -10px; + left: -10px; + max-height: 0; + max-width: 0; +} + +.snippet-file-content { + border-radius: 3px; +} + +.snippet-header { + padding: $gl-padding 0; +} + +.snippet-title { + font-size: 24px; + font-weight: 600; +} + +.snippet-edited-ago { + color: $gray-darkest; +} + +.snippet-actions { + @media (min-width: $screen-sm-min) { + float: right; + } +} + +.snippet-scope-menu .btn-new { + margin-top: 15px; +} diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index 1a985ee2a66..d998d654aa4 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -21,7 +21,7 @@ @import "bootstrap/buttons"; // Components -// @import "bootstrap/component-animations"; +@import "bootstrap/component-animations"; // @import "bootstrap/dropdowns"; @import "bootstrap/button-groups"; @import "bootstrap/input-groups"; @@ -36,8 +36,8 @@ // @import "bootstrap/progress-bars"; @import "bootstrap/list-group"; // @import "bootstrap/wells"; -// @import "bootstrap/close"; -// @import "bootstrap/panels"; +@import "bootstrap/close"; +@import "bootstrap/panels"; // Components w/ JavaScript @import "bootstrap/modals"; diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss index 7d61390a439..bdf07a99daf 100644 --- a/app/assets/stylesheets/pages/notifications.scss +++ b/app/assets/stylesheets/pages/notifications.scss @@ -10,19 +10,7 @@ position: relative; top: 1px; - > .fa { + .fa { font-size: 18px; } } - -.ns-part { - color: $gl-text-green; -} - -.ns-watch { - color: $gl-success; -} - -.ns-mute { - color: $gl-danger; -} diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss deleted file mode 100644 index a6037d76797..00000000000 --- a/app/assets/stylesheets/pages/snippets.scss +++ /dev/null @@ -1,58 +0,0 @@ -.snippet-row { - .title { - margin-bottom: 2px; - } - - .snippet-filename { - padding: 0 2px; - } -} - -.snippet-form-holder .file-holder .file-title { - padding: 2px; -} - -.markdown-snippet-copy { - position: fixed; - top: -10px; - left: -10px; - max-height: 0; - max-width: 0; -} - -.snippet-file-content { - border-radius: 3px; - margin-bottom: $gl-padding; - - .btn-clipboard { - @extend .btn; - } -} - -.project-snippets .awards { - border-bottom: 1px solid $white-normal; - padding-bottom: $gl-padding; -} - -.snippet-header { - padding: $gl-padding 0; -} - -.snippet-title { - font-size: 24px; - font-weight: 600; -} - -.snippet-edited-ago { - color: $gray-darkest; -} - -.snippet-actions { - @media (min-width: $screen-sm-min) { - float: right; - } -} - -.snippet-scope-menu .btn-new { - margin-top: 15px; -} diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index 9503dbded13..79d87b7db12 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -8,10 +8,11 @@ = blob_icon 0, @snippet.file_name = @snippet.file_name .file-actions - = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") + = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']", class: "btn btn-sm") = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" = render 'shared/snippets/blob' - = render 'award_emoji/awards_block', awardable: @snippet, inline: true + .row-content-block.top-block.content-component-block + = render 'award_emoji/awards_block', awardable: @snippet, inline: true %div#notes= render "projects/notes/notes_with_form" diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 27d7a6c5bb6..837a1a0cc8c 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -7,9 +7,9 @@ = blob_icon 0, @snippet.file_name = @snippet.file_name .file-actions - = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") + = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']", class: "btn btn-sm") = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" = link_to 'Download', download_snippet_path(@snippet), class: "btn btn-sm" = render 'shared/snippets/blob' -= render 'award_emoji/awards_block', awardable: @snippet, inline: true \ No newline at end of file += render 'award_emoji/awards_block', awardable: @snippet, inline: true -- cgit v1.2.1 From ad5d86183d4deb1a0e92afddc2621b6222329d07 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra <dimitriehoekstra@gmail.com> Date: Thu, 15 Dec 2016 02:04:45 +0100 Subject: added changelog entry --- changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml diff --git a/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml b/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml new file mode 100644 index 00000000000..754af641add --- /dev/null +++ b/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml @@ -0,0 +1,4 @@ +--- +title: Remove unnecessary commits order message +merge_request: 8004 +author: -- cgit v1.2.1 From 8b83d84bbd7feae8a6a877c06473aeaa8acaafc3 Mon Sep 17 00:00:00 2001 From: Robert Schilling <rschilling@student.tugraz.at> Date: Thu, 15 Dec 2016 10:30:40 +0100 Subject: Remove duplicated parameter description [ci skip] --- doc/api/projects.md | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/api/projects.md b/doc/api/projects.md index 0bc2a5210aa..edffad555a5 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -664,7 +664,6 @@ Parameters: | `path` | string | no | Custom repository name for new project. By default generated based on name | | `default_branch` | string | no | `master` by default | | `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) | -| `default_branch` | string | no | `master` by default | | `description` | string | no | Short project description | | `issues_enabled` | boolean | no | Enable issues for this project | | `merge_requests_enabled` | boolean | no | Enable merge requests for this project | -- cgit v1.2.1 From 089dbc7db1d885a62d2022c0a696b26ad4ae0e17 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 15 Dec 2016 09:31:25 +0000 Subject: Fixed bug with +1 not autocompleting --- app/assets/javascripts/gfm_auto_complete.js.es6 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 076475d3ed4..0e274521525 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -62,14 +62,15 @@ // The below is taken from At.js source // Tweaked to commands to start without a space only if char before is a non-word character // https://github.com/ichord/At.js - var _a, _y, regexp, match; + var _a, _y, regexp, match, atSymbols; + atSymbols = Object.keys(this.app.controllers).join('|'); subtext = subtext.split(' ').pop(); flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); _a = decodeURI("%C3%80"); _y = decodeURI("%C3%BF"); - regexp = new RegExp("(?:\\B|\\W|\\s)" + flag + "(?!\\W)([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)$", 'gi'); + regexp = new RegExp("(?:\\B|\\W|\\s)" + flag + "(?![" + atSymbols + "])([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)$", 'gi'); match = regexp.exec(subtext); -- cgit v1.2.1 From f463ef5ec58fd26ad5368f57a29d758756dafc3f Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Thu, 15 Dec 2016 18:12:33 +0800 Subject: Also use latest_status, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7333#note_20058857 --- lib/gitlab/badge/build/status.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/gitlab/badge/build/status.rb b/lib/gitlab/badge/build/status.rb index f78dfd5b83a..b762d85b6e5 100644 --- a/lib/gitlab/badge/build/status.rb +++ b/lib/gitlab/badge/build/status.rb @@ -21,8 +21,7 @@ module Gitlab def status @project.pipelines .where(sha: @sha) - .latest(@ref) - .status || 'unknown' + .latest_status(@ref) || 'unknown' end def metadata -- cgit v1.2.1 From cc6f578d5fc45f9c3d4cc7df5af72b15ce47b3a8 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Thu, 15 Dec 2016 18:14:48 +0800 Subject: Use described_class and update description Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7333#note_20059124 https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7333#note_20059187 https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7333#note_20059322 --- spec/models/ci/pipeline_spec.rb | 18 +++++++++--------- spec/models/commit_spec.rb | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 21df5df1b76..d06c30a891c 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -381,7 +381,7 @@ describe Ci::Pipeline, models: true do end end - shared_context 'with some empty pipelines' do + shared_context 'with some outdated pipelines' do before do create_pipeline(:canceled, 'ref', 'A') create_pipeline(:success, 'ref', 'A') @@ -395,10 +395,10 @@ describe Ci::Pipeline, models: true do end describe '.latest' do - include_context 'with some empty pipelines' + include_context 'with some outdated pipelines' context 'when no ref is specified' do - let(:pipelines) { Ci::Pipeline.latest.all } + let(:pipelines) { described_class.latest.all } it 'returns the latest pipeline for the same ref and different sha' do expect(pipelines.map(&:sha)).to contain_exactly('A', 'B', 'C') @@ -408,7 +408,7 @@ describe Ci::Pipeline, models: true do end context 'when ref is specified' do - let(:pipelines) { Ci::Pipeline.latest('ref').all } + let(:pipelines) { described_class.latest('ref').all } it 'returns the latest pipeline for ref and different sha' do expect(pipelines.map(&:sha)).to contain_exactly('A', 'B') @@ -419,21 +419,21 @@ describe Ci::Pipeline, models: true do end describe '.latest_status' do - include_context 'with some empty pipelines' + include_context 'with some outdated pipelines' context 'when no ref is specified' do - let(:latest_status) { Ci::Pipeline.latest_status } + let(:latest_status) { described_class.latest_status } it 'returns the latest status for the same ref and different sha' do - expect(latest_status).to eq(Ci::Pipeline.latest.status) + expect(latest_status).to eq(described_class.latest.status) end end context 'when ref is specified' do - let(:latest_status) { Ci::Pipeline.latest_status('ref') } + let(:latest_status) { described_class.latest_status('ref') } it 'returns the latest status for ref and different sha' do - expect(latest_status).to eq(Ci::Pipeline.latest_status('ref')) + expect(latest_status).to eq(described_class.latest_status('ref')) end end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index a2a8392699e..9aac2abfb27 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -213,7 +213,7 @@ eos end describe '#status' do - context 'without arguments' do + context 'without ref argument' do before do %w[success failed created pending].each do |status| create(:ci_empty_pipeline, -- cgit v1.2.1 From b4a7e7cfbf2a79cf6ecdb8b251ad41c09a40e5bd Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Thu, 15 Dec 2016 18:38:26 +0800 Subject: Don't call anything on a block, use simple if Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7333#note_20058743 --- app/models/ci/pipeline.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 9edfc75eac7..48354cdbefb 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -89,13 +89,15 @@ module Ci # ref can't be HEAD or SHA, can only be branch/tag name scope :latest, ->(ref = nil) do - max_id = unscope(:select).select("max(#{quoted_table_name}.id)") + max_id = unscope(:select) + .select("max(#{quoted_table_name}.id)") + .group(:ref, :sha) if ref - where(ref: ref) + where(id: max_id, ref: ref) else - self - end.where(id: max_id.group(:ref, :sha)) + where(id: max_id) + end end def self.latest_status(ref = nil) -- cgit v1.2.1 From d9000184e5e8a63e0c24fe264e864fc27f6515c4 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Thu, 15 Dec 2016 18:52:36 +0800 Subject: Add explicit status test, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7333#note_20058993 https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7333#note_20059060 https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7333#note_20059357 --- spec/models/ci/pipeline_spec.rb | 2 ++ spec/models/commit_spec.rb | 2 ++ 2 files changed, 4 insertions(+) diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index d06c30a891c..52dd41065e9 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -426,6 +426,7 @@ describe Ci::Pipeline, models: true do it 'returns the latest status for the same ref and different sha' do expect(latest_status).to eq(described_class.latest.status) + expect(latest_status).to eq('failed') end end @@ -434,6 +435,7 @@ describe Ci::Pipeline, models: true do it 'returns the latest status for ref and different sha' do expect(latest_status).to eq(described_class.latest_status('ref')) + expect(latest_status).to eq('failed') end end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 9aac2abfb27..74b50d2908d 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -225,6 +225,7 @@ eos it 'gives compound status from latest pipelines' do expect(commit.status).to eq(Ci::Pipeline.latest_status) + expect(commit.status).to eq('pending') end end @@ -252,6 +253,7 @@ eos it 'gives compound status from latest pipelines if ref is nil' do expect(commit.status(nil)).to eq(Ci::Pipeline.latest_status) + expect(commit.status(nil)).to eq('failed') end end end -- cgit v1.2.1 From 48fed57b05b0394ca824bed90c670505b3a8a273 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 15 Dec 2016 10:55:34 +0000 Subject: Fix scss linter error --- app/assets/stylesheets/pages/pipelines.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 4569b91383f..3054f0655db 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -683,7 +683,7 @@ width: 25px; height: 25px; - &:before{ + &::before { top: 1px; left: 1px; } @@ -725,7 +725,7 @@ height: 30px; width: 30px; - &:before { + &::before { position: relative; top: 3px; left: 3px; -- cgit v1.2.1 From b7f297ed8ab9c6bfc6dc5a3c58c9124648b0f60f Mon Sep 17 00:00:00 2001 From: winniehell <git@winniehell.de> Date: Tue, 22 Nov 2016 02:14:59 +0100 Subject: Replace static fixture for abuse_reports_spec (!7644) --- changelogs/unreleased/abuse_report-fixture.yml | 4 +++ spec/javascripts/abuse_reports_spec.js.es6 | 38 ++++++++++++----------- spec/javascripts/fixtures/abuse_reports.html.haml | 16 ---------- spec/javascripts/fixtures/abuse_reports.rb | 27 ++++++++++++++++ 4 files changed, 51 insertions(+), 34 deletions(-) create mode 100644 changelogs/unreleased/abuse_report-fixture.yml delete mode 100644 spec/javascripts/fixtures/abuse_reports.html.haml create mode 100644 spec/javascripts/fixtures/abuse_reports.rb diff --git a/changelogs/unreleased/abuse_report-fixture.yml b/changelogs/unreleased/abuse_report-fixture.yml new file mode 100644 index 00000000000..47478a2048b --- /dev/null +++ b/changelogs/unreleased/abuse_report-fixture.yml @@ -0,0 +1,4 @@ +--- +title: Replace static fixture for abuse_reports_spec +merge_request: 7644 +author: winniehell diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6 index 9e94c9d1d74..49e56249565 100644 --- a/spec/javascripts/abuse_reports_spec.js.es6 +++ b/spec/javascripts/abuse_reports_spec.js.es6 @@ -1,42 +1,44 @@ -/* eslint-disable space-before-function-paren, no-new, padded-blocks */ - +/*= require lib/utils/text_utility */ /*= require abuse_reports */ -/*= require jquery */ ((global) => { - const FIXTURE = 'abuse_reports.html'; - const MAX_MESSAGE_LENGTH = 500; + describe('Abuse Reports', () => { + const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw'; + const MAX_MESSAGE_LENGTH = 500; + + let messages; - function assertMaxLength($message) { - expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH); - } + const assertMaxLength = $message => expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH); + const findMessage = searchText => messages.filter( + (index, element) => element.innerText.indexOf(searchText) > -1, + ).first(); - describe('Abuse Reports', function() { fixture.preload(FIXTURE); - beforeEach(function() { + beforeEach(function () { fixture.load(FIXTURE); - new global.AbuseReports(); + this.abuseReports = new global.AbuseReports(); + messages = $('.abuse-reports .message'); }); - it('should truncate long messages', function() { - const $longMessage = $('#long'); + + it('should truncate long messages', () => { + const $longMessage = findMessage('LONG MESSAGE'); expect($longMessage.data('original-message')).toEqual(jasmine.anything()); assertMaxLength($longMessage); }); - it('should not truncate short messages', function() { - const $shortMessage = $('#short'); + it('should not truncate short messages', () => { + const $shortMessage = findMessage('SHORT MESSAGE'); expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything()); }); - it('should allow clicking a truncated message to expand and collapse the full message', function() { - const $longMessage = $('#long'); + it('should allow clicking a truncated message to expand and collapse the full message', () => { + const $longMessage = findMessage('LONG MESSAGE'); $longMessage.click(); expect($longMessage.data('original-message').length).toEqual($longMessage.text().length); $longMessage.click(); assertMaxLength($longMessage); }); }); - })(window.gl); diff --git a/spec/javascripts/fixtures/abuse_reports.html.haml b/spec/javascripts/fixtures/abuse_reports.html.haml deleted file mode 100644 index 2ec302abcb7..00000000000 --- a/spec/javascripts/fixtures/abuse_reports.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -.abuse-reports - .message#long - Cat ipsum dolor sit amet, hide head under blanket so no one can see. - Gate keepers of hell eat and than sleep on your face but hunt by meowing - loudly at 5am next to human slave food dispenser cats go for world - domination or chase laser, yet poop on grasses chirp at birds. Cat is love, - cat is life chase after silly colored fish toys around the house climb a - tree, wait for a fireman jump to fireman then scratch his face fall asleep - on the washing machine lies down always hungry so caticus cuteicus. Sit on - human. Spot something, big eyes, big eyes, crouch, shake butt, prepare to - pounce sleep in the bathroom sink hiss at vacuum cleaner hide head under - blanket so no one can see throwup on your pillow. - .message#short - Cat ipsum dolor sit amet, groom yourself 4 hours - checked, have your - beauty sleep 18 hours - checked, be fabulous for the rest of the day - - checked! for shake treat bag. diff --git a/spec/javascripts/fixtures/abuse_reports.rb b/spec/javascripts/fixtures/abuse_reports.rb new file mode 100644 index 00000000000..de673f94d72 --- /dev/null +++ b/spec/javascripts/fixtures/abuse_reports.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Admin::AbuseReportsController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let!(:abuse_report) { create(:abuse_report) } + let!(:abuse_report_with_short_message) { create(:abuse_report, message: 'SHORT MESSAGE') } + let!(:abuse_report_with_long_message) { create(:abuse_report, message: "LONG MESSAGE\n" * 50) } + + render_views + + before(:all) do + clean_frontend_fixtures('abuse_reports/') + end + + before(:each) do + sign_in(admin) + end + + it 'abuse_reports/abuse_reports_list.html.raw' do |example| + get :index + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end -- cgit v1.2.1 From 6731ab5d76c34462f0b4424ff03c9646ad916b76 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <jacob@gitlab.com> Date: Thu, 18 Aug 2016 16:31:44 +0200 Subject: Add Gitlab::Middleware::Multipart --- GITLAB_WORKHORSE_VERSION | 2 +- app/uploaders/artifact_uploader.rb | 10 +-- app/uploaders/attachment_uploader.rb | 2 +- app/uploaders/avatar_uploader.rb | 2 +- app/uploaders/file_uploader.rb | 2 +- app/uploaders/gitlab_uploader.rb | 11 +++ app/uploaders/lfs_object_uploader.rb | 10 +-- .../unreleased/gitlab-workhorse-multipart.yml | 4 + config/initializers/workhorse_multipart.rb | 3 + lib/gitlab/gfm/uploads_rewriter.rb | 19 ++++- lib/gitlab/middleware/multipart.rb | 99 ++++++++++++++++++++++ lib/gitlab/workhorse.rb | 6 +- spec/helpers/application_helper_spec.rb | 14 ++- .../gitlab/import_export/avatar_restorer_spec.rb | 4 +- spec/lib/gitlab/middleware/multipart_spec.rb | 74 ++++++++++++++++ spec/requests/api/groups_spec.rb | 4 +- spec/support/upload_helpers.rb | 16 ++++ spec/uploaders/attachment_uploader_spec.rb | 18 ++++ spec/uploaders/avatar_uploader_spec.rb | 18 ++++ spec/uploaders/file_uploader_spec.rb | 12 +++ 20 files changed, 295 insertions(+), 35 deletions(-) create mode 100644 app/uploaders/gitlab_uploader.rb create mode 100644 changelogs/unreleased/gitlab-workhorse-multipart.yml create mode 100644 config/initializers/workhorse_multipart.rb create mode 100644 lib/gitlab/middleware/multipart.rb create mode 100644 spec/lib/gitlab/middleware/multipart_spec.rb create mode 100644 spec/support/upload_helpers.rb create mode 100644 spec/uploaders/attachment_uploader_spec.rb create mode 100644 spec/uploaders/avatar_uploader_spec.rb diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 524cb55242b..26aaba0e866 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -1.1.1 +1.2.0 diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb index b6c52ddac7a..86f317dcd18 100644 --- a/app/uploaders/artifact_uploader.rb +++ b/app/uploaders/artifact_uploader.rb @@ -1,4 +1,4 @@ -class ArtifactUploader < CarrierWave::Uploader::Base +class ArtifactUploader < GitlabUploader storage :file attr_accessor :build, :field @@ -38,12 +38,4 @@ class ArtifactUploader < CarrierWave::Uploader::Base def exists? file.try(:exists?) end - - def move_to_cache - true - end - - def move_to_store - true - end end diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb index fb3b5dfecd0..cfcb877cc3e 100644 --- a/app/uploaders/attachment_uploader.rb +++ b/app/uploaders/attachment_uploader.rb @@ -1,4 +1,4 @@ -class AttachmentUploader < CarrierWave::Uploader::Base +class AttachmentUploader < GitlabUploader include UploaderHelper storage :file diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb index 38683fdf6d7..a1ecb7bc00b 100644 --- a/app/uploaders/avatar_uploader.rb +++ b/app/uploaders/avatar_uploader.rb @@ -1,4 +1,4 @@ -class AvatarUploader < CarrierWave::Uploader::Base +class AvatarUploader < GitlabUploader include UploaderHelper storage :file diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 3ac6030c21c..47bef7cd1e4 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -1,4 +1,4 @@ -class FileUploader < CarrierWave::Uploader::Base +class FileUploader < GitlabUploader include UploaderHelper MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)} diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb new file mode 100644 index 00000000000..02d7c601d6c --- /dev/null +++ b/app/uploaders/gitlab_uploader.rb @@ -0,0 +1,11 @@ +class GitlabUploader < CarrierWave::Uploader::Base + # Reduce disk IO + def move_to_cache + true + end + + # Reduce disk IO + def move_to_store + true + end +end diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb index 4f356dd663e..faab539b8e0 100644 --- a/app/uploaders/lfs_object_uploader.rb +++ b/app/uploaders/lfs_object_uploader.rb @@ -1,4 +1,4 @@ -class LfsObjectUploader < CarrierWave::Uploader::Base +class LfsObjectUploader < GitlabUploader storage :file def store_dir @@ -9,14 +9,6 @@ class LfsObjectUploader < CarrierWave::Uploader::Base "#{Gitlab.config.lfs.storage_path}/tmp/cache" end - def move_to_cache - true - end - - def move_to_store - true - end - def exists? file.try(:exists?) end diff --git a/changelogs/unreleased/gitlab-workhorse-multipart.yml b/changelogs/unreleased/gitlab-workhorse-multipart.yml new file mode 100644 index 00000000000..23c2139cf93 --- /dev/null +++ b/changelogs/unreleased/gitlab-workhorse-multipart.yml @@ -0,0 +1,4 @@ +--- +title: Replace Rack::Multipart with GitLab-Workhorse based solution +merge_request: 5867 +author: diff --git a/config/initializers/workhorse_multipart.rb b/config/initializers/workhorse_multipart.rb new file mode 100644 index 00000000000..3e2f25c354a --- /dev/null +++ b/config/initializers/workhorse_multipart.rb @@ -0,0 +1,3 @@ +Rails.application.configure do |config| + config.middleware.use(Gitlab::Middleware::Multipart) +end diff --git a/lib/gitlab/gfm/uploads_rewriter.rb b/lib/gitlab/gfm/uploads_rewriter.rb index abc8c8c55e6..8fab5489616 100644 --- a/lib/gitlab/gfm/uploads_rewriter.rb +++ b/lib/gitlab/gfm/uploads_rewriter.rb @@ -1,3 +1,5 @@ +require 'fileutils' + module Gitlab module Gfm ## @@ -22,7 +24,9 @@ module Gitlab return markdown unless file.try(:exists?) new_uploader = FileUploader.new(target_project) - new_uploader.store!(file) + with_link_in_tmp_dir(file.file) do |open_tmp_file| + new_uploader.store!(open_tmp_file) + end new_uploader.to_markdown end end @@ -46,6 +50,19 @@ module Gitlab uploader.retrieve_from_store!(file) uploader.file end + + # Because the uploaders use 'move_to_store' we must have a temporary + # file that is allowed to be (re)moved. + def with_link_in_tmp_dir(file) + dir = Dir.mktmpdir('UploadsRewriter', File.dirname(file)) + # The filename matters to Carrierwave so we make sure to preserve it + tmp_file = File.join(dir, File.basename(file)) + File.link(file, tmp_file) + # Open the file to placate Carrierwave + File.open(tmp_file) { |open_file| yield open_file } + ensure + FileUtils.rm_rf(dir) + end end end end diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb new file mode 100644 index 00000000000..65713e73a59 --- /dev/null +++ b/lib/gitlab/middleware/multipart.rb @@ -0,0 +1,99 @@ +# Gitlab::Middleware::Multipart - a Rack::Multipart replacement +# +# Rack::Multipart leaves behind tempfiles in /tmp and uses valuable Ruby +# process time to copy files around. This alternative solution uses +# gitlab-workhorse to clean up the tempfiles and puts the tempfiles in a +# location where copying should not be needed. +# +# When gitlab-workhorse finds files in a multipart MIME body it sends +# a signed message via a request header. This message lists the names of +# the multipart entries that gitlab-workhorse filtered out of the +# multipart structure and saved to tempfiles. Workhorse adds new entries +# in the multipart structure with paths to the tempfiles. +# +# The job of this Rack middleware is to detect and decode the message +# from workhorse. If present, it walks the Rack 'params' hash for the +# current request, opens the respective tempfiles, and inserts the open +# Ruby File objects in the params hash where Rack::Multipart would have +# put them. The goal is that application code deeper down can keep +# working the way it did with Rack::Multipart without changes. +# +# CAVEAT: the code that modifies the params hash is a bit complex. It is +# conceivable that certain Rack params structures will not be modified +# correctly. We are not aware of such bugs at this time though. +# + +module Gitlab + module Middleware + class Multipart + RACK_ENV_KEY = 'HTTP_GITLAB_WORKHORSE_MULTIPART_FIELDS' + + class Handler + def initialize(env, message) + @request = Rack::Request.new(env) + @rewritten_fields = message['rewritten_fields'] + @open_files = [] + end + + def with_open_files + @rewritten_fields.each do |field, tmp_path| + parsed_field = Rack::Utils.parse_nested_query(field) + raise "unexpected field: #{field.inspect}" unless parsed_field.count == 1 + + key, value = parsed_field.first + if value.nil? + value = File.open(tmp_path) + @open_files << value + else + value = decorate_params_value(value, @request.params[key], tmp_path) + end + @request.update_param(key, value) + end + + yield + ensure + @open_files.each(&:close) + end + + # This function calls itself recursively + def decorate_params_value(path_hash, value_hash, tmp_path) + unless path_hash.is_a?(Hash) && path_hash.count == 1 + raise "invalid path: #{path_hash.inspect}" + end + path_key, path_value = path_hash.first + + unless value_hash.is_a?(Hash) && value_hash[path_key] + raise "invalid value hash: #{value_hash.inspect}" + end + + case path_value + when nil + value_hash[path_key] = File.open(tmp_path) + @open_files << value_hash[path_key] + value_hash + when Hash + decorate_params_value(path_value, value_hash[path_key], tmp_path) + value_hash + else + raise "unexpected path value: #{path_value.inspect}" + end + end + end + + def initialize(app) + @app = app + end + + def call(env) + encoded_message = env.delete(RACK_ENV_KEY) + return @app.call(env) if encoded_message.blank? + + message = Gitlab::Workhorse.decode_jwt(encoded_message)[0] + + Handler.new(env, message).with_open_files do + @app.call(env) + end + end + end + end +end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index 594439a5d4b..aeb1a26e1ba 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -117,8 +117,12 @@ module Gitlab end def verify_api_request!(request_headers) + decode_jwt(request_headers[INTERNAL_API_REQUEST_HEADER]) + end + + def decode_jwt(encoded_message) JWT.decode( - request_headers[INTERNAL_API_REQUEST_HEADER], + encoded_message, secret, true, { iss: 'gitlab-workhorse', verify_iss: true, algorithm: 'HS256' }, diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 15863d444f8..92053e5a7c6 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe ApplicationHelper do + include UploadHelpers + describe 'current_controller?' do it 'returns true when controller matches argument' do stub_controller_name('foo') @@ -52,10 +54,8 @@ describe ApplicationHelper do end describe 'project_icon' do - let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') } - it 'returns an url for the avatar' do - project = create(:project, avatar: File.open(avatar_file_path)) + project = create(:project, avatar: File.open(uploaded_image_temp_path)) avatar_url = "http://#{Gitlab.config.gitlab.host}/uploads/project/avatar/#{project.id}/banana_sample.gif" expect(helper.project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s). @@ -74,10 +74,8 @@ describe ApplicationHelper do end describe 'avatar_icon' do - let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') } - it 'returns an url for the avatar' do - user = create(:user, avatar: File.open(avatar_file_path)) + user = create(:user, avatar: File.open(uploaded_image_temp_path)) expect(helper.avatar_icon(user.email).to_s). to match("/uploads/user/avatar/#{user.id}/banana_sample.gif") @@ -88,7 +86,7 @@ describe ApplicationHelper do # Must be stubbed after the stub above, and separately stub_config_setting(url: Settings.send(:build_gitlab_url)) - user = create(:user, avatar: File.open(avatar_file_path)) + user = create(:user, avatar: File.open(uploaded_image_temp_path)) expect(helper.avatar_icon(user.email).to_s). to match("/gitlab/uploads/user/avatar/#{user.id}/banana_sample.gif") @@ -102,7 +100,7 @@ describe ApplicationHelper do describe 'using a User' do it 'returns an URL for the avatar' do - user = create(:user, avatar: File.open(avatar_file_path)) + user = create(:user, avatar: File.open(uploaded_image_temp_path)) expect(helper.avatar_icon(user).to_s). to match("/uploads/user/avatar/#{user.id}/banana_sample.gif") diff --git a/spec/lib/gitlab/import_export/avatar_restorer_spec.rb b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb index 5ae178414cc..08a42fd27a2 100644 --- a/spec/lib/gitlab/import_export/avatar_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb @@ -1,12 +1,14 @@ require 'spec_helper' describe Gitlab::ImportExport::AvatarRestorer, lib: true do + include UploadHelpers + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') } let(:project) { create(:empty_project) } before do allow_any_instance_of(described_class).to receive(:avatar_export_file) - .and_return(Rails.root + "spec/fixtures/dk.png") + .and_return(uploaded_image_temp_path) end after do diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb new file mode 100644 index 00000000000..c79c6494576 --- /dev/null +++ b/spec/lib/gitlab/middleware/multipart_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +require 'tempfile' + +describe Gitlab::Middleware::Multipart do + let(:app) { double(:app) } + let(:middleware) { described_class.new(app) } + + it 'opens top-level files' do + Tempfile.open do |tempfile| + env = post_env({ 'file' => tempfile.path }, { 'file.name' => 'filename' }, Gitlab::Workhorse.secret, 'gitlab-workhorse') + + expect(app).to receive(:call) do |env| + file = Rack::Request.new(env).params['file'] + expect(file).to be_a(File) + expect(file.path).to eq(tempfile.path) + end + + middleware.call(env) + end + end + + it 'rejects headers signed with the wrong secret' do + env = post_env({ 'file' => '/var/empty/nonesuch' }, {}, 'x' * 32, 'gitlab-workhorse') + + expect { middleware.call(env) }.to raise_error(JWT::VerificationError) + end + + it 'rejects headers signed with the wrong issuer' do + env = post_env({ 'file' => '/var/empty/nonesuch' }, {}, Gitlab::Workhorse.secret, 'acme-inc') + + expect { middleware.call(env) }.to raise_error(JWT::InvalidIssuerError) + end + + it 'opens files one level deep' do + Tempfile.open do |tempfile| + in_params = { 'user' => { 'avatar' => { '.name' => 'filename' } } } + env = post_env({ 'user[avatar]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse') + + expect(app).to receive(:call) do |env| + file = Rack::Request.new(env).params['user']['avatar'] + expect(file).to be_a(File) + expect(file.path).to eq(tempfile.path) + end + + middleware.call(env) + end + end + + it 'opens files two levels deep' do + Tempfile.open do |tempfile| + in_params = { 'project' => { 'milestone' => { 'themesong' => { '.name' => 'filename' } } } } + env = post_env({ 'project[milestone][themesong]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse') + + expect(app).to receive(:call) do |env| + file = Rack::Request.new(env).params['project']['milestone']['themesong'] + expect(file).to be_a(File) + expect(file.path).to eq(tempfile.path) + end + + middleware.call(env) + end + end + + def post_env(rewritten_fields, params, secret, issuer) + token = JWT.encode({ 'iss' => issuer, 'rewritten_fields' => rewritten_fields }, secret, 'HS256') + Rack::MockRequest.env_for( + '/', + method: 'post', + params: params, + described_class::RACK_ENV_KEY => token + ) + end +end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index a75ba824e85..cdeb965b413 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -2,13 +2,13 @@ require 'spec_helper' describe API::Groups, api: true do include ApiHelpers + include UploadHelpers let(:user1) { create(:user, can_create_group: false) } let(:user2) { create(:user) } let(:user3) { create(:user) } let(:admin) { create(:admin) } - let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') } - let!(:group1) { create(:group, avatar: File.open(avatar_file_path)) } + let!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) } let!(:group2) { create(:group, :private) } let!(:project1) { create(:project, namespace: group1) } let!(:project2) { create(:project, namespace: group2) } diff --git a/spec/support/upload_helpers.rb b/spec/support/upload_helpers.rb new file mode 100644 index 00000000000..5eead80c935 --- /dev/null +++ b/spec/support/upload_helpers.rb @@ -0,0 +1,16 @@ +require 'fileutils' + +module UploadHelpers + extend self + + def uploaded_image_temp_path + basename = 'banana_sample.gif' + orig_path = File.join(Rails.root, 'spec', 'fixtures', basename) + tmp_path = File.join(Rails.root, 'tmp', 'tests', basename) + # Because we use 'move_to_store' on all uploaders, we create a new + # tempfile on each call: the file we return here will be renamed in most + # cases. + FileUtils.copy(orig_path, tmp_path) + tmp_path + end +end diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb new file mode 100644 index 00000000000..6098be5cd45 --- /dev/null +++ b/spec/uploaders/attachment_uploader_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe AttachmentUploader do + let(:issue) { build(:issue) } + subject { described_class.new(issue) } + + describe '#move_to_cache' do + it 'is true' do + expect(subject.move_to_cache).to eq(true) + end + end + + describe '#move_to_store' do + it 'is true' do + expect(subject.move_to_store).to eq(true) + end + end +end diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb new file mode 100644 index 00000000000..1f0e8732587 --- /dev/null +++ b/spec/uploaders/avatar_uploader_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe AvatarUploader do + let(:user) { build(:user) } + subject { described_class.new(user) } + + describe '#move_to_cache' do + it 'is true' do + expect(subject.move_to_cache).to eq(true) + end + end + + describe '#move_to_store' do + it 'is true' do + expect(subject.move_to_store).to eq(true) + end + end +end diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb index e8300abed5d..bc86adfef68 100644 --- a/spec/uploaders/file_uploader_spec.rb +++ b/spec/uploaders/file_uploader_spec.rb @@ -42,4 +42,16 @@ describe FileUploader do expect(@uploader.image_or_video?).to be false end end + + describe '#move_to_cache' do + it 'is true' do + expect(@uploader.move_to_cache).to eq(true) + end + end + + describe '#move_to_store' do + it 'is true' do + expect(@uploader.move_to_store).to eq(true) + end + end end -- cgit v1.2.1 From 03a95c7070938e0fde67f2def334c08cf72d9d29 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 15 Dec 2016 11:48:05 +0000 Subject: Fix tests --- spec/features/projects/pipelines/pipeline_spec.rb | 39 +++++++++++++++++------ 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 5094fcf33e8..80a596d34c9 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -40,43 +40,62 @@ describe "Pipelines", feature: true, js: true do context 'pipeline graph' do it 'shows a running icon and a cancel action for the running build' do - page.within('.stage-column:nth-child(3) .build:first-child') do + title = "#{@running.name} - #{@running.status}" + + page.within("a[data-title='#{title}']") do expect(page).to have_selector('.ci-status-icon-running') expect(page).to have_content('deploy') + end + + page.within("a[data-title='#{title}'] + .ci-action-icon-container") do expect(page).to have_selector('.ci-action-icon-container .fa-ban') end + end it 'shows the success icon and a retry action for the successfull build' do - page.within('.stage-column:nth-child(2) .build:first-child') do + title = "#{@success.name} - #{@success.status}" + + page.within("a[data-title='#{title}']") do expect(page).to have_selector('.ci-status-icon-success') expect(page).to have_content('build') + end + + page.within("a[data-title='#{title}'] + .ci-action-icon-container") do expect(page).to have_selector('.ci-action-icon-container .fa-refresh') end end it 'shows the failed icon and a retry action for the failed build' do - page.within('.stage-column:first-child .build') do + title = "#{@failed.name} - #{@failed.status}" + + page.within("a[data-title='#{title}']") do expect(page).to have_selector('.ci-status-icon-failed') expect(page).to have_content('test') + end + + page.within("a[data-title='#{title}'] + .ci-action-icon-container") do expect(page).to have_selector('.ci-action-icon-container .fa-refresh') end end it 'shows the skipped icon and a play action for the manual build' do - page.within('.stage-column:nth-child(3) .build:nth-child(2)') do + title = "#{@manual.name} - #{@manual.status}" + + page.within("a[data-title='#{title}']") do expect(page).to have_selector('.ci-status-icon-skipped') expect(page).to have_content('manual') - expect(page).to have_selector('.ci-action-icon-container .ci-play-icon') end - end - it 'shows the success icon for the generic comit status build' do - page.within('.stage-column:nth-child(4) .build') do - expect(page).to have_selector('.ci-status-icon-success') - expect(page).to have_content('jenkins') + page.within("a[data-title='#{title}'] + .ci-action-icon-container") do + expect(page).to have_selector('.ci-action-icon-container .fa-play') end end + + it 'shows the success icon and the generic comit status build' do + expect(page).to have_selector('.ci-status-icon-success') + expect(page).to have_content('jenkins') + end end context 'page tabs' do -- cgit v1.2.1 From 26628fb91a89bbe4998633eec00d2bd76cfb95c0 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Thu, 15 Dec 2016 14:19:28 +0200 Subject: BB importer: Fixed bug with putting expired token to a project.clone_url --- app/controllers/import/bitbucket_controller.rb | 3 +++ lib/bitbucket/client.rb | 4 ++-- lib/bitbucket/connection.rb | 4 ++-- lib/gitlab/bitbucket_import/project_creator.rb | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index b9cc6556140..8e42cdf415f 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -49,6 +49,9 @@ class Import::BitbucketController < Import::BaseController namespace = find_or_create_namespace(@target_namespace, current_user) if current_user.can?(:create_projects, namespace) + # The token in a session can be expired, we need to get most recent one because + # Bitbucket::Connection class refreshes it. + session[:bitbucket_token] = bitbucket_client.connection.token @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @project_name, namespace, current_user, credentials).execute else render 'unauthorized' diff --git a/lib/bitbucket/client.rb b/lib/bitbucket/client.rb index 5c2ef2a4509..f8ee7e0f9ae 100644 --- a/lib/bitbucket/client.rb +++ b/lib/bitbucket/client.rb @@ -1,5 +1,7 @@ module Bitbucket class Client + attr_reader :connection + def initialize(options = {}) @connection = Connection.new(options) end @@ -48,8 +50,6 @@ module Bitbucket private - attr_reader :connection - def get_collection(path, type) paginator = Paginator.new(connection, path, type) Collection.new(paginator) diff --git a/lib/bitbucket/connection.rb b/lib/bitbucket/connection.rb index c150a20761e..7e55cf4deab 100644 --- a/lib/bitbucket/connection.rb +++ b/lib/bitbucket/connection.rb @@ -4,6 +4,8 @@ module Bitbucket DEFAULT_BASE_URI = 'https://api.bitbucket.org/' DEFAULT_QUERY = {} + attr_reader :expires_at, :expires_in, :refresh_token, :token + def initialize(options = {}) @api_version = options.fetch(:api_version, DEFAULT_API_VERSION) @base_uri = options.fetch(:base_uri, DEFAULT_BASE_URI) @@ -38,8 +40,6 @@ module Bitbucket private - attr_reader :expires_at, :expires_in, :refresh_token, :token - def client @client ||= OAuth2::Client.new(provider.app_id, provider.app_secret, options) end diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb index b34be272af3..eb03882ab26 100644 --- a/lib/gitlab/bitbucket_import/project_creator.rb +++ b/lib/gitlab/bitbucket_import/project_creator.rb @@ -21,7 +21,7 @@ module Gitlab visibility_level: repo.visibility_level, import_type: 'bitbucket', import_source: repo.full_name, - import_url: repo.clone_url(@session_data[:token]), + import_url: repo.clone_url(session_data[:token]), import_data: { credentials: session_data } ).execute end -- cgit v1.2.1 From 141faaacf9119ce5d765efe73c6509030ba078cd Mon Sep 17 00:00:00 2001 From: Felipe Artur <felipefac@gmail.com> Date: Fri, 25 Nov 2016 17:36:37 -0200 Subject: Mattermost Notifications Service --- app/models/project.rb | 1 + .../project_services/chat_message/base_message.rb | 34 +++ .../project_services/chat_message/build_message.rb | 82 ++++++ .../project_services/chat_message/issue_message.rb | 69 +++++ .../project_services/chat_message/merge_message.rb | 60 ++++ .../project_services/chat_message/note_message.rb | 83 ++++++ .../chat_message/pipeline_message.rb | 78 +++++ .../project_services/chat_message/push_message.rb | 110 +++++++ .../chat_message/wiki_page_message.rb | 53 ++++ app/models/project_services/chat_service.rb | 141 ++++++++- app/models/project_services/mattermost_service.rb | 41 +++ .../mattermost_slash_commands_service.rb | 12 +- app/models/project_services/slack_service.rb | 174 ++--------- .../project_services/slack_service/base_message.rb | 34 --- .../slack_service/build_message.rb | 82 ------ .../slack_service/issue_message.rb | 69 ----- .../slack_service/merge_message.rb | 60 ---- .../project_services/slack_service/note_message.rb | 83 ------ .../slack_service/pipeline_message.rb | 78 ----- .../project_services/slack_service/push_message.rb | 110 ------- .../slack_service/wiki_page_message.rb | 53 ---- app/models/service.rb | 1 + changelogs/unreleased/issue_22269.yml | 4 + doc/api/services.md | 38 ++- .../img/mattermost_configuration.png | Bin 0 -> 73502 bytes doc/project_services/mattermost.md | 45 +++ doc/project_services/project_services.md | 3 +- doc/project_services/slack.md | 4 +- spec/lib/gitlab/import_export/all_models.yml | 1 + .../chat_message/build_message_spec.rb | 57 ++++ .../chat_message/issue_message_spec.rb | 67 +++++ .../chat_message/merge_message_spec.rb | 51 ++++ .../chat_message/note_message_spec.rb | 130 ++++++++ .../chat_message/pipeline_message_spec.rb | 67 +++++ .../chat_message/push_message_spec.rb | 88 ++++++ .../chat_message/wiki_page_message_spec.rb | 73 +++++ spec/models/project_services/chat_service_spec.rb | 11 +- .../project_services/mattermost_service_spec.rb | 5 + .../slack_service/build_message_spec.rb | 57 ---- .../slack_service/issue_message_spec.rb | 67 ----- .../slack_service/merge_message_spec.rb | 51 ---- .../slack_service/note_message_spec.rb | 130 -------- .../slack_service/pipeline_message_spec.rb | 67 ----- .../slack_service/push_message_spec.rb | 88 ------ .../slack_service/wiki_page_message_spec.rb | 73 ----- spec/models/project_services/slack_service_spec.rb | 324 +------------------- spec/models/project_spec.rb | 1 + spec/support/slack_mattermost_shared_examples.rb | 328 +++++++++++++++++++++ 48 files changed, 1736 insertions(+), 1602 deletions(-) create mode 100644 app/models/project_services/chat_message/base_message.rb create mode 100644 app/models/project_services/chat_message/build_message.rb create mode 100644 app/models/project_services/chat_message/issue_message.rb create mode 100644 app/models/project_services/chat_message/merge_message.rb create mode 100644 app/models/project_services/chat_message/note_message.rb create mode 100644 app/models/project_services/chat_message/pipeline_message.rb create mode 100644 app/models/project_services/chat_message/push_message.rb create mode 100644 app/models/project_services/chat_message/wiki_page_message.rb create mode 100644 app/models/project_services/mattermost_service.rb delete mode 100644 app/models/project_services/slack_service/base_message.rb delete mode 100644 app/models/project_services/slack_service/build_message.rb delete mode 100644 app/models/project_services/slack_service/issue_message.rb delete mode 100644 app/models/project_services/slack_service/merge_message.rb delete mode 100644 app/models/project_services/slack_service/note_message.rb delete mode 100644 app/models/project_services/slack_service/pipeline_message.rb delete mode 100644 app/models/project_services/slack_service/push_message.rb delete mode 100644 app/models/project_services/slack_service/wiki_page_message.rb create mode 100644 changelogs/unreleased/issue_22269.yml create mode 100644 doc/project_services/img/mattermost_configuration.png create mode 100644 doc/project_services/mattermost.md create mode 100644 spec/models/project_services/chat_message/build_message_spec.rb create mode 100644 spec/models/project_services/chat_message/issue_message_spec.rb create mode 100644 spec/models/project_services/chat_message/merge_message_spec.rb create mode 100644 spec/models/project_services/chat_message/note_message_spec.rb create mode 100644 spec/models/project_services/chat_message/pipeline_message_spec.rb create mode 100644 spec/models/project_services/chat_message/push_message_spec.rb create mode 100644 spec/models/project_services/chat_message/wiki_page_message_spec.rb create mode 100644 spec/models/project_services/mattermost_service_spec.rb delete mode 100644 spec/models/project_services/slack_service/build_message_spec.rb delete mode 100644 spec/models/project_services/slack_service/issue_message_spec.rb delete mode 100644 spec/models/project_services/slack_service/merge_message_spec.rb delete mode 100644 spec/models/project_services/slack_service/note_message_spec.rb delete mode 100644 spec/models/project_services/slack_service/pipeline_message_spec.rb delete mode 100644 spec/models/project_services/slack_service/push_message_spec.rb delete mode 100644 spec/models/project_services/slack_service/wiki_page_message_spec.rb create mode 100644 spec/support/slack_mattermost_shared_examples.rb diff --git a/app/models/project.rb b/app/models/project.rb index 2c726cfc5df..19c2d24212d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -95,6 +95,7 @@ class Project < ActiveRecord::Base has_one :asana_service, dependent: :destroy has_one :gemnasium_service, dependent: :destroy has_one :mattermost_slash_commands_service, dependent: :destroy + has_one :mattermost_service, dependent: :destroy has_one :slack_service, dependent: :destroy has_one :buildkite_service, dependent: :destroy has_one :bamboo_service, dependent: :destroy diff --git a/app/models/project_services/chat_message/base_message.rb b/app/models/project_services/chat_message/base_message.rb new file mode 100644 index 00000000000..a03605d01fb --- /dev/null +++ b/app/models/project_services/chat_message/base_message.rb @@ -0,0 +1,34 @@ +require 'slack-notifier' + +module ChatMessage + class BaseMessage + def initialize(params) + raise NotImplementedError + end + + def pretext + format(message) + end + + def fallback + end + + def attachments + raise NotImplementedError + end + + private + + def message + raise NotImplementedError + end + + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end + + def attachment_color + '#345' + end + end +end diff --git a/app/models/project_services/chat_message/build_message.rb b/app/models/project_services/chat_message/build_message.rb new file mode 100644 index 00000000000..53e35cb21bf --- /dev/null +++ b/app/models/project_services/chat_message/build_message.rb @@ -0,0 +1,82 @@ +module ChatMessage + class BuildMessage < BaseMessage + attr_reader :sha + attr_reader :ref_type + attr_reader :ref + attr_reader :status + attr_reader :project_name + attr_reader :project_url + attr_reader :user_name + attr_reader :duration + + def initialize(params) + @sha = params[:sha] + @ref_type = params[:tag] ? 'tag' : 'branch' + @ref = params[:ref] + @project_name = params[:project_name] + @project_url = params[:project_url] + @status = params[:commit][:status] + @user_name = params[:commit][:author_name] + @duration = params[:commit][:duration] + end + + def pretext + '' + end + + def fallback + format(message) + end + + def attachments + [{ text: format(message), color: attachment_color }] + end + + private + + def message + "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}" + end + + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end + + def humanized_status + case status + when 'success' + 'passed' + else + status + end + end + + def attachment_color + if status == 'success' + 'good' + else + 'danger' + end + end + + def branch_url + "#{project_url}/commits/#{ref}" + end + + def branch_link + "[#{ref}](#{branch_url})" + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def commit_url + "#{project_url}/commit/#{sha}/builds" + end + + def commit_link + "[#{Commit.truncate_sha(sha)}](#{commit_url})" + end + end +end diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb new file mode 100644 index 00000000000..14fd64e5332 --- /dev/null +++ b/app/models/project_services/chat_message/issue_message.rb @@ -0,0 +1,69 @@ +module ChatMessage + class IssueMessage < BaseMessage + attr_reader :user_name + attr_reader :title + attr_reader :project_name + attr_reader :project_url + attr_reader :issue_iid + attr_reader :issue_url + attr_reader :action + attr_reader :state + attr_reader :description + + def initialize(params) + @user_name = params[:user][:username] + @project_name = params[:project_name] + @project_url = params[:project_url] + + obj_attr = params[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + @title = obj_attr[:title] + @issue_iid = obj_attr[:iid] + @issue_url = obj_attr[:url] + @action = obj_attr[:action] + @state = obj_attr[:state] + @description = obj_attr[:description] || '' + end + + def attachments + return [] unless opened_issue? + + description_message + end + + private + + def message + case state + when "opened" + "[#{project_link}] Issue #{state} by #{user_name}" + else + "[#{project_link}] Issue #{issue_link} #{state} by #{user_name}" + end + end + + def opened_issue? + action == "open" + end + + def description_message + [{ + title: issue_title, + title_link: issue_url, + text: format(description), + color: "#C95823" }] + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def issue_link + "[#{issue_title}](#{issue_url})" + end + + def issue_title + "##{issue_iid} #{title}" + end + end +end diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb new file mode 100644 index 00000000000..ab5e8b24167 --- /dev/null +++ b/app/models/project_services/chat_message/merge_message.rb @@ -0,0 +1,60 @@ +module ChatMessage + class MergeMessage < BaseMessage + attr_reader :user_name + attr_reader :project_name + attr_reader :project_url + attr_reader :merge_request_id + attr_reader :source_branch + attr_reader :target_branch + attr_reader :state + attr_reader :title + + def initialize(params) + @user_name = params[:user][:username] + @project_name = params[:project_name] + @project_url = params[:project_url] + + obj_attr = params[: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] + @title = format_title(obj_attr[:title]) + end + + def pretext + format(message) + end + + def attachments + [] + end + + private + + def format_title(title) + '*' + title.lines.first.chomp + '*' + end + + def message + merge_request_message + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def merge_request_message + "#{user_name} #{state} #{merge_request_link} in #{project_link}: #{title}" + end + + def merge_request_link + "[merge request !#{merge_request_id}](#{merge_request_url})" + end + + def merge_request_url + "#{project_url}/merge_requests/#{merge_request_id}" + end + end +end diff --git a/app/models/project_services/chat_message/note_message.rb b/app/models/project_services/chat_message/note_message.rb new file mode 100644 index 00000000000..ca1d7207034 --- /dev/null +++ b/app/models/project_services/chat_message/note_message.rb @@ -0,0 +1,83 @@ +module ChatMessage + class NoteMessage < BaseMessage + attr_reader :message + attr_reader :user_name + attr_reader :project_name + attr_reader :project_link + attr_reader :note + attr_reader :note_url + attr_reader :title + + def initialize(params) + params = HashWithIndifferentAccess.new(params) + @user_name = params[:user][:username] + @project_name = params[:project_name] + @project_url = params[:project_url] + + obj_attr = params[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + @note = obj_attr[:note] + @note_url = obj_attr[:url] + noteable_type = obj_attr[:noteable_type] + + case noteable_type + when "Commit" + create_commit_note(HashWithIndifferentAccess.new(params[:commit])) + when "Issue" + create_issue_note(HashWithIndifferentAccess.new(params[:issue])) + when "MergeRequest" + create_merge_note(HashWithIndifferentAccess.new(params[:merge_request])) + when "Snippet" + create_snippet_note(HashWithIndifferentAccess.new(params[:snippet])) + end + end + + def attachments + description_message + end + + private + + def format_title(title) + title.lines.first.chomp + end + + def create_commit_note(commit) + commit_sha = commit[:id] + commit_sha = Commit.truncate_sha(commit_sha) + commented_on_message( + "commit #{commit_sha}", + format_title(commit[:message])) + end + + def create_issue_note(issue) + commented_on_message( + "issue ##{issue[:iid]}", + format_title(issue[:title])) + end + + def create_merge_note(merge_request) + commented_on_message( + "merge request !#{merge_request[:iid]}", + format_title(merge_request[:title])) + end + + def create_snippet_note(snippet) + commented_on_message( + "snippet ##{snippet[:id]}", + format_title(snippet[:title])) + end + + def description_message + [{ text: format(@note), color: attachment_color }] + end + + def project_link + "[#{@project_name}](#{@project_url})" + end + + def commented_on_message(target, title) + @message = "#{@user_name} [commented on #{target}](#{@note_url}) in #{project_link}: *#{title}*" + end + end +end diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb new file mode 100644 index 00000000000..210027565a8 --- /dev/null +++ b/app/models/project_services/chat_message/pipeline_message.rb @@ -0,0 +1,78 @@ +module ChatMessage + class PipelineMessage < BaseMessage + attr_reader :ref_type, :ref, :status, :project_name, :project_url, + :user_name, :duration, :pipeline_id + + def initialize(data) + pipeline_attributes = data[:object_attributes] + @ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch' + @ref = pipeline_attributes[:ref] + @status = pipeline_attributes[:status] + @duration = pipeline_attributes[:duration] + @pipeline_id = pipeline_attributes[:id] + + @project_name = data[:project][:path_with_namespace] + @project_url = data[:project][:web_url] + @user_name = (data[:user] && data[:user][:name]) || 'API' + end + + def pretext + '' + end + + def fallback + format(message) + end + + def attachments + [{ text: format(message), color: attachment_color }] + end + + private + + def message + "#{project_link}: Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}" + end + + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end + + def humanized_status + case status + when 'success' + 'passed' + else + status + end + end + + def attachment_color + if status == 'success' + 'good' + else + 'danger' + end + end + + def branch_url + "#{project_url}/commits/#{ref}" + end + + def branch_link + "[#{ref}](#{branch_url})" + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def pipeline_url + "#{project_url}/pipelines/#{pipeline_id}" + end + + def pipeline_link + "[##{pipeline_id}](#{pipeline_url})" + end + end +end diff --git a/app/models/project_services/chat_message/push_message.rb b/app/models/project_services/chat_message/push_message.rb new file mode 100644 index 00000000000..2d73b71ec37 --- /dev/null +++ b/app/models/project_services/chat_message/push_message.rb @@ -0,0 +1,110 @@ +module ChatMessage + class PushMessage < BaseMessage + attr_reader :after + attr_reader :before + attr_reader :commits + attr_reader :project_name + attr_reader :project_url + attr_reader :ref + attr_reader :ref_type + attr_reader :user_name + + def initialize(params) + @after = params[:after] + @before = params[:before] + @commits = params.fetch(:commits, []) + @project_name = params[:project_name] + @project_url = params[:project_url] + @ref_type = Gitlab::Git.tag_ref?(params[:ref]) ? 'tag' : 'branch' + @ref = Gitlab::Git.ref_name(params[:ref]) + @user_name = params[:user_name] + end + + def pretext + format(message) + end + + def attachments + return [] if new_branch? || removed_branch? + + commit_message_attachments + end + + private + + def message + if new_branch? + new_branch_message + elsif removed_branch? + removed_branch_message + else + push_message + end + end + + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end + + def new_branch_message + "#{user_name} pushed new #{ref_type} #{branch_link} to #{project_link}" + end + + def removed_branch_message + "#{user_name} removed #{ref_type} #{ref} from #{project_link}" + end + + def push_message + "#{user_name} pushed to #{ref_type} #{branch_link} of #{project_link} (#{compare_link})" + end + + def commit_messages + commits.map { |commit| compose_commit_message(commit) }.join("\n") + end + + def commit_message_attachments + [{ text: format(commit_messages), color: attachment_color }] + end + + def compose_commit_message(commit) + author = commit[:author][:name] + id = Commit.truncate_sha(commit[:id]) + message = commit[:message] + url = commit[:url] + + "[#{id}](#{url}): #{message} - #{author}" + end + + def new_branch? + Gitlab::Git.blank_ref?(before) + end + + def removed_branch? + Gitlab::Git.blank_ref?(after) + end + + def branch_url + "#{project_url}/commits/#{ref}" + end + + def compare_url + "#{project_url}/compare/#{before}...#{after}" + end + + def branch_link + "[#{ref}](#{branch_url})" + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def compare_link + "[Compare changes](#{compare_url})" + end + + def attachment_color + '#345' + end + end +end diff --git a/app/models/project_services/chat_message/wiki_page_message.rb b/app/models/project_services/chat_message/wiki_page_message.rb new file mode 100644 index 00000000000..134083e4504 --- /dev/null +++ b/app/models/project_services/chat_message/wiki_page_message.rb @@ -0,0 +1,53 @@ +module ChatMessage + class WikiPageMessage < BaseMessage + attr_reader :user_name + attr_reader :title + attr_reader :project_name + attr_reader :project_url + attr_reader :wiki_page_url + attr_reader :action + attr_reader :description + + def initialize(params) + @user_name = params[:user][:username] + @project_name = params[:project_name] + @project_url = params[:project_url] + + obj_attr = params[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + @title = obj_attr[:title] + @wiki_page_url = obj_attr[:url] + @description = obj_attr[:content] + + @action = + case obj_attr[:action] + when "create" + "created" + when "update" + "edited" + end + end + + def attachments + description_message + end + + private + + def message + "#{user_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*" + end + + def description_message + [{ text: format(@description), color: attachment_color }] + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def wiki_page_link + "[wiki page](#{wiki_page_url})" + end + end +end diff --git a/app/models/project_services/chat_service.rb b/app/models/project_services/chat_service.rb index d36beff5fa6..8ac049ba939 100644 --- a/app/models/project_services/chat_service.rb +++ b/app/models/project_services/chat_service.rb @@ -1,21 +1,148 @@ # Base class for Chat services # This class is not meant to be used directly, but only to inherrit from. class ChatService < Service + include ChatMessage + default_value_for :category, 'chat' - has_many :chat_names, foreign_key: :service_id + prop_accessor :webhook, :username, :channel + boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines + + validates :webhook, presence: true, url: true, if: :activated? + + def initialize_properties + # Custom serialized properties initialization + self.supported_events.each { |event| self.class.prop_accessor(event_channel_name(event)) } - def valid_token?(token) - self.respond_to?(:token) && - self.token.present? && - ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) + if properties.nil? + self.properties = {} + self.notify_only_broken_builds = true + self.notify_only_broken_pipelines = true + end + end + + def can_test? + valid? end def supported_events - [] + %w[push issue confidential_issue merge_request note tag_push + build pipeline wiki_page] end - def trigger(params) + def execute(data) + return unless supported_events.include?(data[:object_kind]) + return unless webhook.present? + + object_kind = data[:object_kind] + + data = data.merge( + project_url: project_url, + project_name: project_name + ) + + # WebHook events often have an 'update' event that follows a 'open' or + # 'close' action. Ignore update events for now to prevent duplicate + # messages from arriving. + + message = get_message(object_kind, data) + + return false unless message + + opt = {} + + opt[:channel] = get_channel_field(object_kind).presence || channel || default_channel + opt[:username] = username if username + + notifier = Slack::Notifier.new(webhook, opt) + notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback) + + true + end + + def event_channel_names + supported_events.map { |event| event_channel_name(event) } + end + + def event_field(event) + fields.find { |field| field[:name] == event_channel_name(event) } + end + + def global_fields + fields.reject { |field| field[:name].end_with?('channel') } + end + + def default_channel raise NotImplementedError end + + private + + def get_message(object_kind, data) + case object_kind + when "push", "tag_push" + PushMessage.new(data) + when "issue" + IssueMessage.new(data) unless is_update?(data) + when "merge_request" + MergeMessage.new(data) unless is_update?(data) + when "note" + NoteMessage.new(data) + when "build" + BuildMessage.new(data) if should_build_be_notified?(data) + when "pipeline" + PipelineMessage.new(data) if should_pipeline_be_notified?(data) + when "wiki_page" + WikiPageMessage.new(data) + end + end + + def get_channel_field(event) + field_name = event_channel_name(event) + self.public_send(field_name) + end + + def build_event_channels + supported_events.reduce([]) do |channels, event| + channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel } + end + end + + def event_channel_name(event) + "#{event}_channel" + end + + def project_name + project.name_with_namespace.gsub(/\s/, '') + end + + def project_url + project.web_url + end + + def is_update?(data) + data[:object_attributes][:action] == 'update' + end + + def should_build_be_notified?(data) + case data[:commit][:status] + when 'success' + !notify_only_broken_builds? + when 'failed' + true + else + false + end + end + + def should_pipeline_be_notified?(data) + case data[:object_attributes][:status] + when 'success' + !notify_only_broken_pipelines? + when 'failed' + true + else + false + end + end end diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb new file mode 100644 index 00000000000..9d61c251a32 --- /dev/null +++ b/app/models/project_services/mattermost_service.rb @@ -0,0 +1,41 @@ +class MattermostService < ChatService + def title + 'Mattermost notifications' + end + + def description + 'Receive event notifications in Mattermost' + end + + def to_param + 'mattermost' + end + + def help + 'This service sends notifications about projects events to Mattermost channels.<br /> + To set up this service: + <ol> + <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation. </li> + <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event. </li> + <li>Paste the webhook <strong>URL</strong> into the field bellow. </li> + <li>Select events below to enable notifications. The channel and username are optional. </li> + </ol>' + end + + def fields + default_fields + build_event_channels + end + + def default_fields + [ + { type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' }, + { type: 'text', name: 'username', placeholder: 'username' }, + { type: 'checkbox', name: 'notify_only_broken_builds' }, + { type: 'checkbox', name: 'notify_only_broken_pipelines' }, + ] + end + + def default_channel + "#town-square" + end +end diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 33431f41dc2..3993dfbda17 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -1,8 +1,18 @@ -class MattermostSlashCommandsService < ChatService +class MattermostSlashCommandsService < Service include TriggersHelper prop_accessor :token + def valid_token?(token) + self.respond_to?(:token) && + self.token.present? && + ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) + end + + def supported_events + [] + end + def can_test? false end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index e1b937817f4..0df1743c4ba 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -1,25 +1,10 @@ -class SlackService < Service - prop_accessor :webhook, :username, :channel - boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines - validates :webhook, presence: true, url: true, if: :activated? - - def initialize_properties - # Custom serialized properties initialization - self.supported_events.each { |event| self.class.prop_accessor(event_channel_name(event)) } - - if properties.nil? - self.properties = {} - self.notify_only_broken_builds = true - self.notify_only_broken_pipelines = true - end - end - +class SlackService < ChatService def title - 'Slack' + 'Slack notifications' end def description - 'A team communication tool for the 21st century' + 'Receive event notifications in Slack' end def to_param @@ -27,150 +12,29 @@ class SlackService < Service end def help - 'This service sends notifications to your Slack channel.<br/> - To setup this Service you need to create a new <b>"Incoming webhook"</b> in your Slack integration panel, - and enter the Webhook URL below.' + 'This service sends notifications about projects events to Slack channels.<br /> + To setup this service: + <ol> + <li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event. </li> + <li>Paste the <strong>Webhook URL</strong> into the field below. </li> + <li>Select events below to enable notifications. The channel and username are optional. </li> + </ol>' end def fields - default_fields = - [ - { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' }, - { type: 'text', name: 'username', placeholder: 'username' }, - { type: 'text', name: 'channel', placeholder: "#general" }, - { type: 'checkbox', name: 'notify_only_broken_builds' }, - { type: 'checkbox', name: 'notify_only_broken_pipelines' }, - ] - default_fields + build_event_channels end - def supported_events - %w[push issue confidential_issue merge_request note tag_push - build pipeline wiki_page] - end - - def execute(data) - return unless supported_events.include?(data[:object_kind]) - return unless webhook.present? - - object_kind = data[:object_kind] - - data = data.merge( - project_url: project_url, - project_name: project_name - ) - - # WebHook events often have an 'update' event that follows a 'open' or - # 'close' action. Ignore update events for now to prevent duplicate - # messages from arriving. - - message = get_message(object_kind, data) - - if message - opt = {} - - event_channel = get_channel_field(object_kind) || channel - - opt[:channel] = event_channel if event_channel - opt[:username] = username if username - - notifier = Slack::Notifier.new(webhook, opt) - notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback) - - true - else - false - end - end - - def event_channel_names - supported_events.map { |event| event_channel_name(event) } - end - - def event_field(event) - fields.find { |field| field[:name] == event_channel_name(event) } + def default_fields + [ + { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' }, + { type: 'text', name: 'username', placeholder: 'username' }, + { type: 'checkbox', name: 'notify_only_broken_builds' }, + { type: 'checkbox', name: 'notify_only_broken_pipelines' }, + ] end - def global_fields - fields.reject { |field| field[:name].end_with?('channel') } - end - - private - - def get_message(object_kind, data) - case object_kind - when "push", "tag_push" - PushMessage.new(data) - when "issue" - IssueMessage.new(data) unless is_update?(data) - when "merge_request" - MergeMessage.new(data) unless is_update?(data) - when "note" - NoteMessage.new(data) - when "build" - BuildMessage.new(data) if should_build_be_notified?(data) - when "pipeline" - PipelineMessage.new(data) if should_pipeline_be_notified?(data) - when "wiki_page" - WikiPageMessage.new(data) - end - end - - def get_channel_field(event) - field_name = event_channel_name(event) - self.public_send(field_name) - end - - def build_event_channels - supported_events.reduce([]) do |channels, event| - channels << { type: 'text', name: event_channel_name(event), placeholder: "#general" } - end - end - - def event_channel_name(event) - "#{event}_channel" - end - - def project_name - project.name_with_namespace.gsub(/\s/, '') - end - - def project_url - project.web_url - end - - def is_update?(data) - data[:object_attributes][:action] == 'update' - end - - def should_build_be_notified?(data) - case data[:commit][:status] - when 'success' - !notify_only_broken_builds? - when 'failed' - true - else - false - end - end - - def should_pipeline_be_notified?(data) - case data[:object_attributes][:status] - when 'success' - !notify_only_broken_pipelines? - when 'failed' - true - else - false - end + def default_channel + "#general" end end - -require "slack_service/issue_message" -require "slack_service/push_message" -require "slack_service/merge_message" -require "slack_service/note_message" -require "slack_service/build_message" -require "slack_service/pipeline_message" -require "slack_service/wiki_page_message" diff --git a/app/models/project_services/slack_service/base_message.rb b/app/models/project_services/slack_service/base_message.rb deleted file mode 100644 index f1182824687..00000000000 --- a/app/models/project_services/slack_service/base_message.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'slack-notifier' - -class SlackService - class BaseMessage - def initialize(params) - raise NotImplementedError - end - - def pretext - format(message) - end - - def fallback - end - - def attachments - raise NotImplementedError - end - - private - - def message - raise NotImplementedError - end - - def format(string) - Slack::Notifier::LinkFormatter.format(string) - end - - def attachment_color - '#345' - end - end -end diff --git a/app/models/project_services/slack_service/build_message.rb b/app/models/project_services/slack_service/build_message.rb deleted file mode 100644 index 0fca4267bad..00000000000 --- a/app/models/project_services/slack_service/build_message.rb +++ /dev/null @@ -1,82 +0,0 @@ -class SlackService - class BuildMessage < BaseMessage - attr_reader :sha - attr_reader :ref_type - attr_reader :ref - attr_reader :status - attr_reader :project_name - attr_reader :project_url - attr_reader :user_name - attr_reader :duration - - def initialize(params) - @sha = params[:sha] - @ref_type = params[:tag] ? 'tag' : 'branch' - @ref = params[:ref] - @project_name = params[:project_name] - @project_url = params[:project_url] - @status = params[:commit][:status] - @user_name = params[:commit][:author_name] - @duration = params[:commit][:duration] - end - - def pretext - '' - end - - def fallback - format(message) - end - - def attachments - [{ text: format(message), color: attachment_color }] - end - - private - - def message - "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}" - end - - def format(string) - Slack::Notifier::LinkFormatter.format(string) - end - - def humanized_status - case status - when 'success' - 'passed' - else - status - end - end - - def attachment_color - if status == 'success' - 'good' - else - 'danger' - end - end - - def branch_url - "#{project_url}/commits/#{ref}" - end - - def branch_link - "[#{ref}](#{branch_url})" - end - - def project_link - "[#{project_name}](#{project_url})" - end - - def commit_url - "#{project_url}/commit/#{sha}/builds" - end - - def commit_link - "[#{Commit.truncate_sha(sha)}](#{commit_url})" - end - end -end diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/slack_service/issue_message.rb deleted file mode 100644 index cd87a79d0c6..00000000000 --- a/app/models/project_services/slack_service/issue_message.rb +++ /dev/null @@ -1,69 +0,0 @@ -class SlackService - class IssueMessage < BaseMessage - attr_reader :user_name - attr_reader :title - attr_reader :project_name - attr_reader :project_url - attr_reader :issue_iid - attr_reader :issue_url - attr_reader :action - attr_reader :state - attr_reader :description - - def initialize(params) - @user_name = params[:user][:username] - @project_name = params[:project_name] - @project_url = params[:project_url] - - obj_attr = params[:object_attributes] - obj_attr = HashWithIndifferentAccess.new(obj_attr) - @title = obj_attr[:title] - @issue_iid = obj_attr[:iid] - @issue_url = obj_attr[:url] - @action = obj_attr[:action] - @state = obj_attr[:state] - @description = obj_attr[:description] || '' - end - - def attachments - return [] unless opened_issue? - - description_message - end - - private - - def message - case state - when "opened" - "[#{project_link}] Issue #{state} by #{user_name}" - else - "[#{project_link}] Issue #{issue_link} #{state} by #{user_name}" - end - end - - def opened_issue? - action == "open" - end - - def description_message - [{ - title: issue_title, - title_link: issue_url, - text: format(description), - color: "#C95823" }] - end - - def project_link - "[#{project_name}](#{project_url})" - end - - def issue_link - "[#{issue_title}](#{issue_url})" - end - - def issue_title - "##{issue_iid} #{title}" - end - end -end diff --git a/app/models/project_services/slack_service/merge_message.rb b/app/models/project_services/slack_service/merge_message.rb deleted file mode 100644 index b7615c96068..00000000000 --- a/app/models/project_services/slack_service/merge_message.rb +++ /dev/null @@ -1,60 +0,0 @@ -class SlackService - class MergeMessage < BaseMessage - attr_reader :user_name - attr_reader :project_name - attr_reader :project_url - attr_reader :merge_request_id - attr_reader :source_branch - attr_reader :target_branch - attr_reader :state - attr_reader :title - - def initialize(params) - @user_name = params[:user][:username] - @project_name = params[:project_name] - @project_url = params[:project_url] - - obj_attr = params[: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] - @title = format_title(obj_attr[:title]) - end - - def pretext - format(message) - end - - def attachments - [] - end - - private - - def format_title(title) - '*' + title.lines.first.chomp + '*' - end - - def message - merge_request_message - end - - def project_link - "[#{project_name}](#{project_url})" - end - - def merge_request_message - "#{user_name} #{state} #{merge_request_link} in #{project_link}: #{title}" - end - - def merge_request_link - "[merge request !#{merge_request_id}](#{merge_request_url})" - end - - def merge_request_url - "#{project_url}/merge_requests/#{merge_request_id}" - end - end -end diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/slack_service/note_message.rb deleted file mode 100644 index 797c5937f09..00000000000 --- a/app/models/project_services/slack_service/note_message.rb +++ /dev/null @@ -1,83 +0,0 @@ -class SlackService - class NoteMessage < BaseMessage - attr_reader :message - attr_reader :user_name - attr_reader :project_name - attr_reader :project_link - attr_reader :note - attr_reader :note_url - attr_reader :title - - def initialize(params) - params = HashWithIndifferentAccess.new(params) - @user_name = params[:user][:username] - @project_name = params[:project_name] - @project_url = params[:project_url] - - obj_attr = params[:object_attributes] - obj_attr = HashWithIndifferentAccess.new(obj_attr) - @note = obj_attr[:note] - @note_url = obj_attr[:url] - noteable_type = obj_attr[:noteable_type] - - case noteable_type - when "Commit" - create_commit_note(HashWithIndifferentAccess.new(params[:commit])) - when "Issue" - create_issue_note(HashWithIndifferentAccess.new(params[:issue])) - when "MergeRequest" - create_merge_note(HashWithIndifferentAccess.new(params[:merge_request])) - when "Snippet" - create_snippet_note(HashWithIndifferentAccess.new(params[:snippet])) - end - end - - def attachments - description_message - end - - private - - def format_title(title) - title.lines.first.chomp - end - - def create_commit_note(commit) - commit_sha = commit[:id] - commit_sha = Commit.truncate_sha(commit_sha) - commented_on_message( - "commit #{commit_sha}", - format_title(commit[:message])) - end - - def create_issue_note(issue) - commented_on_message( - "issue ##{issue[:iid]}", - format_title(issue[:title])) - end - - def create_merge_note(merge_request) - commented_on_message( - "merge request !#{merge_request[:iid]}", - format_title(merge_request[:title])) - end - - def create_snippet_note(snippet) - commented_on_message( - "snippet ##{snippet[:id]}", - format_title(snippet[:title])) - end - - def description_message - [{ text: format(@note), color: attachment_color }] - end - - def project_link - "[#{@project_name}](#{@project_url})" - end - - def commented_on_message(target, title) - @message = "#{@user_name} [commented on #{target}](#{@note_url}) in #{project_link}: *#{title}*" - end - end -end diff --git a/app/models/project_services/slack_service/pipeline_message.rb b/app/models/project_services/slack_service/pipeline_message.rb deleted file mode 100644 index b6355fc4171..00000000000 --- a/app/models/project_services/slack_service/pipeline_message.rb +++ /dev/null @@ -1,78 +0,0 @@ -class SlackService - class PipelineMessage < BaseMessage - attr_reader :ref_type, :ref, :status, :project_name, :project_url, - :user_name, :duration, :pipeline_id - - def initialize(data) - pipeline_attributes = data[:object_attributes] - @ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch' - @ref = pipeline_attributes[:ref] - @status = pipeline_attributes[:status] - @duration = pipeline_attributes[:duration] - @pipeline_id = pipeline_attributes[:id] - - @project_name = data[:project][:path_with_namespace] - @project_url = data[:project][:web_url] - @user_name = (data[:user] && data[:user][:name]) || 'API' - end - - def pretext - '' - end - - def fallback - format(message) - end - - def attachments - [{ text: format(message), color: attachment_color }] - end - - private - - def message - "#{project_link}: Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}" - end - - def format(string) - Slack::Notifier::LinkFormatter.format(string) - end - - def humanized_status - case status - when 'success' - 'passed' - else - status - end - end - - def attachment_color - if status == 'success' - 'good' - else - 'danger' - end - end - - def branch_url - "#{project_url}/commits/#{ref}" - end - - def branch_link - "[#{ref}](#{branch_url})" - end - - def project_link - "[#{project_name}](#{project_url})" - end - - def pipeline_url - "#{project_url}/pipelines/#{pipeline_id}" - end - - def pipeline_link - "[##{pipeline_id}](#{pipeline_url})" - end - end -end diff --git a/app/models/project_services/slack_service/push_message.rb b/app/models/project_services/slack_service/push_message.rb deleted file mode 100644 index b26f3e9ddce..00000000000 --- a/app/models/project_services/slack_service/push_message.rb +++ /dev/null @@ -1,110 +0,0 @@ -class SlackService - class PushMessage < BaseMessage - attr_reader :after - attr_reader :before - attr_reader :commits - attr_reader :project_name - attr_reader :project_url - attr_reader :ref - attr_reader :ref_type - attr_reader :user_name - - def initialize(params) - @after = params[:after] - @before = params[:before] - @commits = params.fetch(:commits, []) - @project_name = params[:project_name] - @project_url = params[:project_url] - @ref_type = Gitlab::Git.tag_ref?(params[:ref]) ? 'tag' : 'branch' - @ref = Gitlab::Git.ref_name(params[:ref]) - @user_name = params[:user_name] - end - - def pretext - format(message) - end - - def attachments - return [] if new_branch? || removed_branch? - - commit_message_attachments - end - - private - - def message - if new_branch? - new_branch_message - elsif removed_branch? - removed_branch_message - else - push_message - end - end - - def format(string) - Slack::Notifier::LinkFormatter.format(string) - end - - def new_branch_message - "#{user_name} pushed new #{ref_type} #{branch_link} to #{project_link}" - end - - def removed_branch_message - "#{user_name} removed #{ref_type} #{ref} from #{project_link}" - end - - def push_message - "#{user_name} pushed to #{ref_type} #{branch_link} of #{project_link} (#{compare_link})" - end - - def commit_messages - commits.map { |commit| compose_commit_message(commit) }.join("\n") - end - - def commit_message_attachments - [{ text: format(commit_messages), color: attachment_color }] - end - - def compose_commit_message(commit) - author = commit[:author][:name] - id = Commit.truncate_sha(commit[:id]) - message = commit[:message] - url = commit[:url] - - "[#{id}](#{url}): #{message} - #{author}" - end - - def new_branch? - Gitlab::Git.blank_ref?(before) - end - - def removed_branch? - Gitlab::Git.blank_ref?(after) - end - - def branch_url - "#{project_url}/commits/#{ref}" - end - - def compare_url - "#{project_url}/compare/#{before}...#{after}" - end - - def branch_link - "[#{ref}](#{branch_url})" - end - - def project_link - "[#{project_name}](#{project_url})" - end - - def compare_link - "[Compare changes](#{compare_url})" - end - - def attachment_color - '#345' - end - end -end diff --git a/app/models/project_services/slack_service/wiki_page_message.rb b/app/models/project_services/slack_service/wiki_page_message.rb deleted file mode 100644 index 160ca3ac115..00000000000 --- a/app/models/project_services/slack_service/wiki_page_message.rb +++ /dev/null @@ -1,53 +0,0 @@ -class SlackService - class WikiPageMessage < BaseMessage - attr_reader :user_name - attr_reader :title - attr_reader :project_name - attr_reader :project_url - attr_reader :wiki_page_url - attr_reader :action - attr_reader :description - - def initialize(params) - @user_name = params[:user][:username] - @project_name = params[:project_name] - @project_url = params[:project_url] - - obj_attr = params[:object_attributes] - obj_attr = HashWithIndifferentAccess.new(obj_attr) - @title = obj_attr[:title] - @wiki_page_url = obj_attr[:url] - @description = obj_attr[:content] - - @action = - case obj_attr[:action] - when "create" - "created" - when "update" - "edited" - end - end - - def attachments - description_message - end - - private - - def message - "#{user_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*" - end - - def description_message - [{ text: format(@description), color: attachment_color }] - end - - def project_link - "[#{project_name}](#{project_url})" - end - - def wiki_page_link - "[wiki page](#{wiki_page_url})" - end - end -end diff --git a/app/models/service.rb b/app/models/service.rb index e49a8fa2904..8e58f2a1925 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -220,6 +220,7 @@ class Service < ActiveRecord::Base pivotaltracker pushover redmine + mattermost slack teamcity ] diff --git a/changelogs/unreleased/issue_22269.yml b/changelogs/unreleased/issue_22269.yml new file mode 100644 index 00000000000..6b7164aff77 --- /dev/null +++ b/changelogs/unreleased/issue_22269.yml @@ -0,0 +1,4 @@ +--- +title: Create mattermost service +merge_request: +author: diff --git a/doc/api/services.md b/doc/api/services.md index 3dad953cd1e..1466b8189b0 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -703,9 +703,9 @@ Get Redmine service settings for a project. GET /projects/:id/services/redmine ``` -## Slack +## Slack notifications -A team communication tool for the 21st century +Receive event notifications in Slack ### Create/Edit Slack service @@ -737,6 +737,40 @@ Get Slack service settings for a project. GET /projects/:id/services/slack ``` +## Mattermost notifications + +Receive event notifications in Mattermost + +### Create/Edit Mattermost notifications service + +Set Mattermost service for a project. + +``` +PUT /projects/:id/services/mattermost +``` + +Parameters: + +- `webhook` (**required**) - https://mattermost.example/hooks/1298aff... +- `username` (optional) - username +- `channel` (optional) - #channel + +### Delete Mattermost notifications service + +Delete Mattermost Notifications service for a project. + +``` +DELETE /projects/:id/services/mattermost +``` + +### Get Mattermost notifications service settings + +Get Mattermost notifications service settings for a project. + +``` +GET /projects/:id/services/mattermost +``` + ## JetBrains TeamCity CI A continuous integration and build server diff --git a/doc/project_services/img/mattermost_configuration.png b/doc/project_services/img/mattermost_configuration.png new file mode 100644 index 00000000000..3c5ff5ee317 Binary files /dev/null and b/doc/project_services/img/mattermost_configuration.png differ diff --git a/doc/project_services/mattermost.md b/doc/project_services/mattermost.md new file mode 100644 index 00000000000..fbc7dfeee6d --- /dev/null +++ b/doc/project_services/mattermost.md @@ -0,0 +1,45 @@ +# Mattermost Notifications Service + +## On Mattermost + +To enable Mattermost integration you must create an incoming webhook integration: + +1. Sign in to your Mattermost instance +1. Visit incoming webhooks, that will be something like: https://mattermost.example/your_team_name/integrations/incoming_webhooks/add +1. Choose a display name, description and channel, those can be overridden on GitLab +1. Save it, copy the **Webhook URL**, we'll need this later for GitLab. + +There might be some cases that Incoming Webhooks are blocked by admin, ask your mattermost admin to enable +it on https://mattermost.example/admin_console/integrations/custom. + +Display name override is not enabled by default, you need to ask your admin to enable it on that same section. + +## On GitLab + +After you set up Mattermost, it's time to set up GitLab. + +Go to your project's **Settings > Services > Mattermost Notifications** and you will see a +checkbox with the following events that can be triggered: + +- Push +- Issue +- Merge request +- Note +- Tag push +- Build +- Wiki page + +Bellow each of these event checkboxes, you will have an input field to insert +which Mattermost channel you want to send that event message, with `#town-square` +being the default. The hash sign is optional. + +At the end, fill in your Mattermost details: + +| Field | Description | +| ----- | ----------- | +| **Webhook** | The incoming webhooks which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo... | +| **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. | +| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. | + + +![Mattermost configuration](img/mattermost_configuration.png) diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index a7bcd186a8c..0f398874b8f 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -44,10 +44,11 @@ further configuration instructions and details. Contributions are welcome. | JetBrains TeamCity CI | A continuous integration and build server | | [Kubernetes](kubernetes.md) | A containerized deployment service | | [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands | +| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost | +| [Slack Notifications](slack.md) | Receive event notifications in Slack | | PivotalTracker | Project Management Software (Source Commits Endpoint) | | Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop | | [Redmine](redmine.md) | Redmine issue tracker | -| [Slack](slack.md) | A team communication tool for the 21st century | ## Services Templates diff --git a/doc/project_services/slack.md b/doc/project_services/slack.md index 3cfe77c9f85..0b682b43810 100644 --- a/doc/project_services/slack.md +++ b/doc/project_services/slack.md @@ -1,4 +1,4 @@ -# Slack Service +# Slack Notifications Service ## On Slack @@ -15,7 +15,7 @@ Slack: After you set up Slack, it's time to set up GitLab. -Go to your project's **Settings > Services > Slack** and you will see a +Go to your project's **Settings > Services > Slack Notifications** and you will see a checkbox with the following events that can be triggered: - Push diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index c4ee838b7c9..068137f6255 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -137,6 +137,7 @@ project: - asana_service - gemnasium_service - slack_service +- mattermost_service - buildkite_service - bamboo_service - teamcity_service diff --git a/spec/models/project_services/chat_message/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb new file mode 100644 index 00000000000..b71d153f814 --- /dev/null +++ b/spec/models/project_services/chat_message/build_message_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe ChatMessage::BuildMessage do + subject { described_class.new(args) } + + let(:args) do + { + sha: '97de212e80737a608d939f648d959671fb0a0142', + ref: 'develop', + tag: false, + + project_name: 'project_name', + project_url: 'example.gitlab.com', + + commit: { + status: status, + author_name: 'hacker', + duration: duration, + }, + } + end + + let(:message) { build_message } + + context 'build succeeded' do + let(:status) { 'success' } + let(:color) { 'good' } + let(:duration) { 10 } + let(:message) { build_message('passed') } + + it 'returns a message with information about succeeded build' do + expect(subject.pretext).to be_empty + expect(subject.fallback).to eq(message) + expect(subject.attachments).to eq([text: message, color: color]) + end + end + + context 'build failed' do + let(:status) { 'failed' } + let(:color) { 'danger' } + let(:duration) { 10 } + + it 'returns a message with information about failed build' do + expect(subject.pretext).to be_empty + expect(subject.fallback).to eq(message) + expect(subject.attachments).to eq([text: message, color: color]) + end + end + + def build_message(status_text = status) + "<example.gitlab.com|project_name>:" \ + " Commit <example.gitlab.com/commit/" \ + "97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \ + " of <example.gitlab.com/commits/develop|develop> branch" \ + " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}" + end +end diff --git a/spec/models/project_services/chat_message/issue_message_spec.rb b/spec/models/project_services/chat_message/issue_message_spec.rb new file mode 100644 index 00000000000..ebe0ead4408 --- /dev/null +++ b/spec/models/project_services/chat_message/issue_message_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe ChatMessage::IssueMessage, models: true do + subject { described_class.new(args) } + + let(:args) do + { + user: { + name: 'Test User', + username: 'test.user' + }, + project_name: 'project_name', + project_url: 'somewhere.com', + + object_attributes: { + title: 'Issue title', + id: 10, + iid: 100, + assignee_id: 1, + url: 'url', + action: 'open', + state: 'opened', + description: 'issue description' + } + } + end + + let(:color) { '#C95823' } + + context '#initialize' do + before do + args[:object_attributes][:description] = nil + end + + it 'returns a non-null description' do + expect(subject.description).to eq('') + end + end + + context 'open' do + it 'returns a message regarding opening of issues' do + expect(subject.pretext).to eq( + '<somewhere.com|[project_name>] Issue opened by test.user') + expect(subject.attachments).to eq([ + { + title: "#100 Issue title", + title_link: "url", + text: "issue description", + color: color, + } + ]) + end + end + + context 'close' do + before do + args[:object_attributes][:action] = 'close' + args[:object_attributes][:state] = 'closed' + end + + it 'returns a message regarding closing of issues' do + expect(subject.pretext). to eq( + '<somewhere.com|[project_name>] Issue <url|#100 Issue title> closed by test.user') + expect(subject.attachments).to be_empty + end + end +end diff --git a/spec/models/project_services/chat_message/merge_message_spec.rb b/spec/models/project_services/chat_message/merge_message_spec.rb new file mode 100644 index 00000000000..07c414c6ca4 --- /dev/null +++ b/spec/models/project_services/chat_message/merge_message_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe ChatMessage::MergeMessage, models: true do + subject { described_class.new(args) } + + let(:args) do + { + user: { + name: 'Test User', + username: 'test.user' + }, + project_name: 'project_name', + project_url: 'somewhere.com', + + object_attributes: { + title: "Issue title\nSecond line", + id: 10, + iid: 100, + assignee_id: 1, + url: 'url', + state: 'opened', + description: 'issue description', + source_branch: 'source_branch', + target_branch: 'target_branch', + } + } + end + + let(:color) { '#345' } + + context 'open' do + it 'returns a message regarding opening of merge requests' do + expect(subject.pretext).to eq( + 'test.user opened <somewhere.com/merge_requests/100|merge request !100> '\ + 'in <somewhere.com|project_name>: *Issue title*') + expect(subject.attachments).to be_empty + end + end + + context 'close' do + before do + args[:object_attributes][:state] = 'closed' + end + it 'returns a message regarding closing of merge requests' do + expect(subject.pretext).to eq( + 'test.user closed <somewhere.com/merge_requests/100|merge request !100> '\ + 'in <somewhere.com|project_name>: *Issue title*') + expect(subject.attachments).to be_empty + end + end +end diff --git a/spec/models/project_services/chat_message/note_message_spec.rb b/spec/models/project_services/chat_message/note_message_spec.rb new file mode 100644 index 00000000000..31936da40a2 --- /dev/null +++ b/spec/models/project_services/chat_message/note_message_spec.rb @@ -0,0 +1,130 @@ +require 'spec_helper' + +describe ChatMessage::NoteMessage, models: true do + let(:color) { '#345' } + + before do + @args = { + user: { + name: 'Test User', + username: 'test.user', + avatar_url: 'http://fakeavatar' + }, + project_name: 'project_name', + project_url: 'somewhere.com', + repository: { + name: 'project_name', + url: 'somewhere.com', + }, + object_attributes: { + id: 10, + note: 'comment on a commit', + url: 'url', + noteable_type: 'Commit' + } + } + end + + context 'commit notes' do + before do + @args[:object_attributes][:note] = 'comment on a commit' + @args[:object_attributes][:noteable_type] = 'Commit' + @args[:commit] = { + id: '5f163b2b95e6f53cbd428f5f0b103702a52b9a23', + message: "Added a commit message\ndetails\n123\n" + } + end + + it 'returns a message regarding notes on commits' do + message = described_class.new(@args) + expect(message.pretext).to eq("test.user <url|commented on " \ + "commit 5f163b2b> in <somewhere.com|project_name>: " \ + "*Added a commit message*") + expected_attachments = [ + { + text: "comment on a commit", + color: color, + } + ] + expect(message.attachments).to eq(expected_attachments) + end + end + + context 'merge request notes' do + before do + @args[:object_attributes][:note] = 'comment on a merge request' + @args[:object_attributes][:noteable_type] = 'MergeRequest' + @args[:merge_request] = { + id: 1, + iid: 30, + title: "merge request title\ndetails\n" + } + end + + it 'returns a message regarding notes on a merge request' do + message = described_class.new(@args) + expect(message.pretext).to eq("test.user <url|commented on " \ + "merge request !30> in <somewhere.com|project_name>: " \ + "*merge request title*") + expected_attachments = [ + { + text: "comment on a merge request", + color: color, + } + ] + expect(message.attachments).to eq(expected_attachments) + end + end + + context 'issue notes' do + before do + @args[:object_attributes][:note] = 'comment on an issue' + @args[:object_attributes][:noteable_type] = 'Issue' + @args[:issue] = { + id: 1, + iid: 20, + title: "issue title\ndetails\n" + } + end + + it 'returns a message regarding notes on an issue' do + message = described_class.new(@args) + expect(message.pretext).to eq( + "test.user <url|commented on " \ + "issue #20> in <somewhere.com|project_name>: " \ + "*issue title*") + expected_attachments = [ + { + text: "comment on an issue", + color: color, + } + ] + expect(message.attachments).to eq(expected_attachments) + end + end + + context 'project snippet notes' do + before do + @args[:object_attributes][:note] = 'comment on a snippet' + @args[:object_attributes][:noteable_type] = 'Snippet' + @args[:snippet] = { + id: 5, + title: "snippet title\ndetails\n" + } + end + + it 'returns a message regarding notes on a project snippet' do + message = described_class.new(@args) + expect(message.pretext).to eq("test.user <url|commented on " \ + "snippet #5> in <somewhere.com|project_name>: " \ + "*snippet title*") + expected_attachments = [ + { + text: "comment on a snippet", + color: color, + } + ] + expect(message.attachments).to eq(expected_attachments) + end + end +end diff --git a/spec/models/project_services/chat_message/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb new file mode 100644 index 00000000000..eca71db07b6 --- /dev/null +++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe ChatMessage::PipelineMessage do + subject { described_class.new(args) } + let(:user) { { name: 'hacker' } } + + let(:args) do + { + object_attributes: { + id: 123, + sha: '97de212e80737a608d939f648d959671fb0a0142', + tag: false, + ref: 'develop', + status: status, + duration: duration + }, + project: { path_with_namespace: 'project_name', + web_url: 'example.gitlab.com' }, + user: user + } + end + + let(:message) { build_message } + + context 'pipeline succeeded' do + let(:status) { 'success' } + let(:color) { 'good' } + let(:duration) { 10 } + let(:message) { build_message('passed') } + + it 'returns a message with information about succeeded build' do + verify_message + end + end + + context 'pipeline failed' do + let(:status) { 'failed' } + let(:color) { 'danger' } + let(:duration) { 10 } + + it 'returns a message with information about failed build' do + verify_message + end + + context 'when triggered by API therefore lacking user' do + let(:user) { nil } + let(:message) { build_message(status, 'API') } + + it 'returns a message stating it is by API' do + verify_message + end + end + end + + def verify_message + expect(subject.pretext).to be_empty + expect(subject.fallback).to eq(message) + expect(subject.attachments).to eq([text: message, color: color]) + end + + def build_message(status_text = status, name = user[:name]) + "<example.gitlab.com|project_name>:" \ + " Pipeline <example.gitlab.com/pipelines/123|#123>" \ + " of <example.gitlab.com/commits/develop|develop> branch" \ + " by #{name} #{status_text} in #{duration} #{'second'.pluralize(duration)}" + end +end diff --git a/spec/models/project_services/chat_message/push_message_spec.rb b/spec/models/project_services/chat_message/push_message_spec.rb new file mode 100644 index 00000000000..b781c4505db --- /dev/null +++ b/spec/models/project_services/chat_message/push_message_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe ChatMessage::PushMessage, models: true do + subject { described_class.new(args) } + + let(:args) do + { + after: 'after', + before: 'before', + project_name: 'project_name', + ref: 'refs/heads/master', + user_name: 'test.user', + project_url: 'url' + } + end + + let(:color) { '#345' } + + context 'push' do + before do + args[:commits] = [ + { message: 'message1', url: 'url1', id: 'abcdefghijkl', author: { name: 'author1' } }, + { message: 'message2', url: 'url2', id: '123456789012', author: { name: 'author2' } }, + ] + end + + it 'returns a message regarding pushes' do + expect(subject.pretext).to eq( + 'test.user pushed to branch <url/commits/master|master> of '\ + '<url|project_name> (<url/compare/before...after|Compare changes>)' + ) + expect(subject.attachments).to eq([ + { + text: "<url1|abcdefgh>: message1 - author1\n"\ + "<url2|12345678>: message2 - author2", + color: color, + } + ]) + end + end + + context 'tag push' do + let(:args) do + { + after: 'after', + before: Gitlab::Git::BLANK_SHA, + project_name: 'project_name', + ref: 'refs/tags/new_tag', + user_name: 'test.user', + project_url: 'url' + } + end + + it 'returns a message regarding pushes' do + expect(subject.pretext).to eq('test.user pushed new tag ' \ + '<url/commits/new_tag|new_tag> to ' \ + '<url|project_name>') + expect(subject.attachments).to be_empty + end + end + + context 'new branch' do + before do + args[:before] = Gitlab::Git::BLANK_SHA + end + + it 'returns a message regarding a new branch' do + expect(subject.pretext).to eq( + 'test.user pushed new branch <url/commits/master|master> to '\ + '<url|project_name>' + ) + expect(subject.attachments).to be_empty + end + end + + context 'removed branch' do + before do + args[:after] = Gitlab::Git::BLANK_SHA + end + + it 'returns a message regarding a removed branch' do + expect(subject.pretext).to eq( + 'test.user removed branch master from <url|project_name>' + ) + expect(subject.attachments).to be_empty + end + end +end diff --git a/spec/models/project_services/chat_message/wiki_page_message_spec.rb b/spec/models/project_services/chat_message/wiki_page_message_spec.rb new file mode 100644 index 00000000000..94c04dc0865 --- /dev/null +++ b/spec/models/project_services/chat_message/wiki_page_message_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' + +describe ChatMessage::WikiPageMessage, models: true do + subject { described_class.new(args) } + + let(:args) do + { + user: { + name: 'Test User', + username: 'test.user' + }, + project_name: 'project_name', + project_url: 'somewhere.com', + object_attributes: { + title: 'Wiki page title', + url: 'url', + content: 'Wiki page description' + } + } + end + + describe '#pretext' do + context 'when :action == "create"' do + before { args[:object_attributes][:action] = 'create' } + + it 'returns a message that a new wiki page was created' do + expect(subject.pretext).to eq( + 'test.user created <url|wiki page> in <somewhere.com|project_name>: '\ + '*Wiki page title*') + end + end + + context 'when :action == "update"' do + before { args[:object_attributes][:action] = 'update' } + + it 'returns a message that a wiki page was updated' do + expect(subject.pretext).to eq( + 'test.user edited <url|wiki page> in <somewhere.com|project_name>: '\ + '*Wiki page title*') + end + end + end + + describe '#attachments' do + let(:color) { '#345' } + + context 'when :action == "create"' do + before { args[:object_attributes][:action] = 'create' } + + it 'returns the attachment for a new wiki page' do + expect(subject.attachments).to eq([ + { + text: "Wiki page description", + color: color, + } + ]) + end + end + + context 'when :action == "update"' do + before { args[:object_attributes][:action] = 'update' } + + it 'returns the attachment for an updated wiki page' do + expect(subject.attachments).to eq([ + { + text: "Wiki page description", + color: color, + } + ]) + end + end + end +end diff --git a/spec/models/project_services/chat_service_spec.rb b/spec/models/project_services/chat_service_spec.rb index c6a45a3e1be..e6314a43501 100644 --- a/spec/models/project_services/chat_service_spec.rb +++ b/spec/models/project_services/chat_service_spec.rb @@ -2,14 +2,7 @@ require 'spec_helper' describe ChatService, models: true do describe "Associations" do - it { is_expected.to have_many :chat_names } - end - - describe '#valid_token?' do - subject { described_class.new } - - it 'is false as it has no token' do - expect(subject.valid_token?('wer')).to be_falsey - end + before { allow(subject).to receive(:activated?).and_return(true) } + it { is_expected.to validate_presence_of :webhook } end end diff --git a/spec/models/project_services/mattermost_service_spec.rb b/spec/models/project_services/mattermost_service_spec.rb new file mode 100644 index 00000000000..1e5b4c715c3 --- /dev/null +++ b/spec/models/project_services/mattermost_service_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe MattermostService, models: true do + it_behaves_like "slack or mattermost" +end diff --git a/spec/models/project_services/slack_service/build_message_spec.rb b/spec/models/project_services/slack_service/build_message_spec.rb deleted file mode 100644 index 452f4e2782c..00000000000 --- a/spec/models/project_services/slack_service/build_message_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -require 'spec_helper' - -describe SlackService::BuildMessage do - subject { SlackService::BuildMessage.new(args) } - - let(:args) do - { - sha: '97de212e80737a608d939f648d959671fb0a0142', - ref: 'develop', - tag: false, - - project_name: 'project_name', - project_url: 'example.gitlab.com', - - commit: { - status: status, - author_name: 'hacker', - duration: duration, - }, - } - end - - let(:message) { build_message } - - context 'build succeeded' do - let(:status) { 'success' } - let(:color) { 'good' } - let(:duration) { 10 } - let(:message) { build_message('passed') } - - it 'returns a message with information about succeeded build' do - expect(subject.pretext).to be_empty - expect(subject.fallback).to eq(message) - expect(subject.attachments).to eq([text: message, color: color]) - end - end - - context 'build failed' do - let(:status) { 'failed' } - let(:color) { 'danger' } - let(:duration) { 10 } - - it 'returns a message with information about failed build' do - expect(subject.pretext).to be_empty - expect(subject.fallback).to eq(message) - expect(subject.attachments).to eq([text: message, color: color]) - end - end - - def build_message(status_text = status) - "<example.gitlab.com|project_name>:" \ - " Commit <example.gitlab.com/commit/" \ - "97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \ - " of <example.gitlab.com/commits/develop|develop> branch" \ - " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}" - end -end diff --git a/spec/models/project_services/slack_service/issue_message_spec.rb b/spec/models/project_services/slack_service/issue_message_spec.rb deleted file mode 100644 index 98c36ec088d..00000000000 --- a/spec/models/project_services/slack_service/issue_message_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'spec_helper' - -describe SlackService::IssueMessage, models: true do - subject { SlackService::IssueMessage.new(args) } - - let(:args) do - { - user: { - name: 'Test User', - username: 'test.user' - }, - project_name: 'project_name', - project_url: 'somewhere.com', - - object_attributes: { - title: 'Issue title', - id: 10, - iid: 100, - assignee_id: 1, - url: 'url', - action: 'open', - state: 'opened', - description: 'issue description' - } - } - end - - let(:color) { '#C95823' } - - context '#initialize' do - before do - args[:object_attributes][:description] = nil - end - - it 'returns a non-null description' do - expect(subject.description).to eq('') - end - end - - context 'open' do - it 'returns a message regarding opening of issues' do - expect(subject.pretext).to eq( - '<somewhere.com|[project_name>] Issue opened by test.user') - expect(subject.attachments).to eq([ - { - title: "#100 Issue title", - title_link: "url", - text: "issue description", - color: color, - } - ]) - end - end - - context 'close' do - before do - args[:object_attributes][:action] = 'close' - args[:object_attributes][:state] = 'closed' - end - - it 'returns a message regarding closing of issues' do - expect(subject.pretext). to eq( - '<somewhere.com|[project_name>] Issue <url|#100 Issue title> closed by test.user') - expect(subject.attachments).to be_empty - end - end -end diff --git a/spec/models/project_services/slack_service/merge_message_spec.rb b/spec/models/project_services/slack_service/merge_message_spec.rb deleted file mode 100644 index c5c052d9af1..00000000000 --- a/spec/models/project_services/slack_service/merge_message_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'spec_helper' - -describe SlackService::MergeMessage, models: true do - subject { SlackService::MergeMessage.new(args) } - - let(:args) do - { - user: { - name: 'Test User', - username: 'test.user' - }, - project_name: 'project_name', - project_url: 'somewhere.com', - - object_attributes: { - title: "Issue title\nSecond line", - id: 10, - iid: 100, - assignee_id: 1, - url: 'url', - state: 'opened', - description: 'issue description', - source_branch: 'source_branch', - target_branch: 'target_branch', - } - } - end - - let(:color) { '#345' } - - context 'open' do - it 'returns a message regarding opening of merge requests' do - expect(subject.pretext).to eq( - 'test.user opened <somewhere.com/merge_requests/100|merge request !100> '\ - 'in <somewhere.com|project_name>: *Issue title*') - expect(subject.attachments).to be_empty - end - end - - context 'close' do - before do - args[:object_attributes][:state] = 'closed' - end - it 'returns a message regarding closing of merge requests' do - expect(subject.pretext).to eq( - 'test.user closed <somewhere.com/merge_requests/100|merge request !100> '\ - 'in <somewhere.com|project_name>: *Issue title*') - expect(subject.attachments).to be_empty - end - end -end diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb deleted file mode 100644 index 97f818125d3..00000000000 --- a/spec/models/project_services/slack_service/note_message_spec.rb +++ /dev/null @@ -1,130 +0,0 @@ -require 'spec_helper' - -describe SlackService::NoteMessage, models: true do - let(:color) { '#345' } - - before do - @args = { - user: { - name: 'Test User', - username: 'test.user', - avatar_url: 'http://fakeavatar' - }, - project_name: 'project_name', - project_url: 'somewhere.com', - repository: { - name: 'project_name', - url: 'somewhere.com', - }, - object_attributes: { - id: 10, - note: 'comment on a commit', - url: 'url', - noteable_type: 'Commit' - } - } - end - - context 'commit notes' do - before do - @args[:object_attributes][:note] = 'comment on a commit' - @args[:object_attributes][:noteable_type] = 'Commit' - @args[:commit] = { - id: '5f163b2b95e6f53cbd428f5f0b103702a52b9a23', - message: "Added a commit message\ndetails\n123\n" - } - end - - it 'returns a message regarding notes on commits' do - message = SlackService::NoteMessage.new(@args) - expect(message.pretext).to eq("test.user <url|commented on " \ - "commit 5f163b2b> in <somewhere.com|project_name>: " \ - "*Added a commit message*") - expected_attachments = [ - { - text: "comment on a commit", - color: color, - } - ] - expect(message.attachments).to eq(expected_attachments) - end - end - - context 'merge request notes' do - before do - @args[:object_attributes][:note] = 'comment on a merge request' - @args[:object_attributes][:noteable_type] = 'MergeRequest' - @args[:merge_request] = { - id: 1, - iid: 30, - title: "merge request title\ndetails\n" - } - end - - it 'returns a message regarding notes on a merge request' do - message = SlackService::NoteMessage.new(@args) - expect(message.pretext).to eq("test.user <url|commented on " \ - "merge request !30> in <somewhere.com|project_name>: " \ - "*merge request title*") - expected_attachments = [ - { - text: "comment on a merge request", - color: color, - } - ] - expect(message.attachments).to eq(expected_attachments) - end - end - - context 'issue notes' do - before do - @args[:object_attributes][:note] = 'comment on an issue' - @args[:object_attributes][:noteable_type] = 'Issue' - @args[:issue] = { - id: 1, - iid: 20, - title: "issue title\ndetails\n" - } - end - - it 'returns a message regarding notes on an issue' do - message = SlackService::NoteMessage.new(@args) - expect(message.pretext).to eq( - "test.user <url|commented on " \ - "issue #20> in <somewhere.com|project_name>: " \ - "*issue title*") - expected_attachments = [ - { - text: "comment on an issue", - color: color, - } - ] - expect(message.attachments).to eq(expected_attachments) - end - end - - context 'project snippet notes' do - before do - @args[:object_attributes][:note] = 'comment on a snippet' - @args[:object_attributes][:noteable_type] = 'Snippet' - @args[:snippet] = { - id: 5, - title: "snippet title\ndetails\n" - } - end - - it 'returns a message regarding notes on a project snippet' do - message = SlackService::NoteMessage.new(@args) - expect(message.pretext).to eq("test.user <url|commented on " \ - "snippet #5> in <somewhere.com|project_name>: " \ - "*snippet title*") - expected_attachments = [ - { - text: "comment on a snippet", - color: color, - } - ] - expect(message.attachments).to eq(expected_attachments) - end - end -end diff --git a/spec/models/project_services/slack_service/pipeline_message_spec.rb b/spec/models/project_services/slack_service/pipeline_message_spec.rb deleted file mode 100644 index 4098500122f..00000000000 --- a/spec/models/project_services/slack_service/pipeline_message_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'spec_helper' - -describe SlackService::PipelineMessage do - subject { SlackService::PipelineMessage.new(args) } - let(:user) { { name: 'hacker' } } - - let(:args) do - { - object_attributes: { - id: 123, - sha: '97de212e80737a608d939f648d959671fb0a0142', - tag: false, - ref: 'develop', - status: status, - duration: duration - }, - project: { path_with_namespace: 'project_name', - web_url: 'example.gitlab.com' }, - user: user - } - end - - let(:message) { build_message } - - context 'pipeline succeeded' do - let(:status) { 'success' } - let(:color) { 'good' } - let(:duration) { 10 } - let(:message) { build_message('passed') } - - it 'returns a message with information about succeeded build' do - verify_message - end - end - - context 'pipeline failed' do - let(:status) { 'failed' } - let(:color) { 'danger' } - let(:duration) { 10 } - - it 'returns a message with information about failed build' do - verify_message - end - - context 'when triggered by API therefore lacking user' do - let(:user) { nil } - let(:message) { build_message(status, 'API') } - - it 'returns a message stating it is by API' do - verify_message - end - end - end - - def verify_message - expect(subject.pretext).to be_empty - expect(subject.fallback).to eq(message) - expect(subject.attachments).to eq([text: message, color: color]) - end - - def build_message(status_text = status, name = user[:name]) - "<example.gitlab.com|project_name>:" \ - " Pipeline <example.gitlab.com/pipelines/123|#123>" \ - " of <example.gitlab.com/commits/develop|develop> branch" \ - " by #{name} #{status_text} in #{duration} #{'second'.pluralize(duration)}" - end -end diff --git a/spec/models/project_services/slack_service/push_message_spec.rb b/spec/models/project_services/slack_service/push_message_spec.rb deleted file mode 100644 index 17cd05e24f1..00000000000 --- a/spec/models/project_services/slack_service/push_message_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -require 'spec_helper' - -describe SlackService::PushMessage, models: true do - subject { SlackService::PushMessage.new(args) } - - let(:args) do - { - after: 'after', - before: 'before', - project_name: 'project_name', - ref: 'refs/heads/master', - user_name: 'test.user', - project_url: 'url' - } - end - - let(:color) { '#345' } - - context 'push' do - before do - args[:commits] = [ - { message: 'message1', url: 'url1', id: 'abcdefghijkl', author: { name: 'author1' } }, - { message: 'message2', url: 'url2', id: '123456789012', author: { name: 'author2' } }, - ] - end - - it 'returns a message regarding pushes' do - expect(subject.pretext).to eq( - 'test.user pushed to branch <url/commits/master|master> of '\ - '<url|project_name> (<url/compare/before...after|Compare changes>)' - ) - expect(subject.attachments).to eq([ - { - text: "<url1|abcdefgh>: message1 - author1\n"\ - "<url2|12345678>: message2 - author2", - color: color, - } - ]) - end - end - - context 'tag push' do - let(:args) do - { - after: 'after', - before: Gitlab::Git::BLANK_SHA, - project_name: 'project_name', - ref: 'refs/tags/new_tag', - user_name: 'test.user', - project_url: 'url' - } - end - - it 'returns a message regarding pushes' do - expect(subject.pretext).to eq('test.user pushed new tag ' \ - '<url/commits/new_tag|new_tag> to ' \ - '<url|project_name>') - expect(subject.attachments).to be_empty - end - end - - context 'new branch' do - before do - args[:before] = Gitlab::Git::BLANK_SHA - end - - it 'returns a message regarding a new branch' do - expect(subject.pretext).to eq( - 'test.user pushed new branch <url/commits/master|master> to '\ - '<url|project_name>' - ) - expect(subject.attachments).to be_empty - end - end - - context 'removed branch' do - before do - args[:after] = Gitlab::Git::BLANK_SHA - end - - it 'returns a message regarding a removed branch' do - expect(subject.pretext).to eq( - 'test.user removed branch master from <url|project_name>' - ) - expect(subject.attachments).to be_empty - end - end -end diff --git a/spec/models/project_services/slack_service/wiki_page_message_spec.rb b/spec/models/project_services/slack_service/wiki_page_message_spec.rb deleted file mode 100644 index 093911598b0..00000000000 --- a/spec/models/project_services/slack_service/wiki_page_message_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -require 'spec_helper' - -describe SlackService::WikiPageMessage, models: true do - subject { described_class.new(args) } - - let(:args) do - { - user: { - name: 'Test User', - username: 'test.user' - }, - project_name: 'project_name', - project_url: 'somewhere.com', - object_attributes: { - title: 'Wiki page title', - url: 'url', - content: 'Wiki page description' - } - } - end - - describe '#pretext' do - context 'when :action == "create"' do - before { args[:object_attributes][:action] = 'create' } - - it 'returns a message that a new wiki page was created' do - expect(subject.pretext).to eq( - 'test.user created <url|wiki page> in <somewhere.com|project_name>: '\ - '*Wiki page title*') - end - end - - context 'when :action == "update"' do - before { args[:object_attributes][:action] = 'update' } - - it 'returns a message that a wiki page was updated' do - expect(subject.pretext).to eq( - 'test.user edited <url|wiki page> in <somewhere.com|project_name>: '\ - '*Wiki page title*') - end - end - end - - describe '#attachments' do - let(:color) { '#345' } - - context 'when :action == "create"' do - before { args[:object_attributes][:action] = 'create' } - - it 'returns the attachment for a new wiki page' do - expect(subject.attachments).to eq([ - { - text: "Wiki page description", - color: color, - } - ]) - end - end - - context 'when :action == "update"' do - before { args[:object_attributes][:action] = 'update' } - - it 'returns the attachment for an updated wiki page' do - expect(subject.attachments).to eq([ - { - text: "Wiki page description", - color: color, - } - ]) - end - end - end -end diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb index c07a70a8069..4928391fd7e 100644 --- a/spec/models/project_services/slack_service_spec.rb +++ b/spec/models/project_services/slack_service_spec.rb @@ -1,327 +1,5 @@ require 'spec_helper' describe SlackService, models: true do - let(:slack) { SlackService.new } - let(:webhook_url) { 'https://example.gitlab.com/' } - - describe "Associations" do - it { is_expected.to belong_to :project } - it { is_expected.to have_one :service_hook } - end - - describe 'Validations' do - context 'when service is active' do - before { subject.active = true } - - it { is_expected.to validate_presence_of(:webhook) } - it_behaves_like 'issue tracker service URL attribute', :webhook - end - - context 'when service is inactive' do - before { subject.active = false } - - it { is_expected.not_to validate_presence_of(:webhook) } - end - end - - describe "Execute" do - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:username) { 'slack_username' } - let(:channel) { 'slack_channel' } - - let(:push_sample_data) do - Gitlab::DataBuilder::Push.build_sample(project, user) - end - - before do - allow(slack).to receive_messages( - project: project, - project_id: project.id, - service_hook: true, - webhook: webhook_url - ) - - WebMock.stub_request(:post, webhook_url) - - opts = { - title: 'Awesome issue', - description: 'please fix' - } - - issue_service = Issues::CreateService.new(project, user, opts) - @issue = issue_service.execute - @issues_sample_data = issue_service.hook_data(@issue, 'open') - - opts = { - title: 'Awesome merge_request', - description: 'please fix', - source_branch: 'feature', - target_branch: 'master' - } - merge_service = MergeRequests::CreateService.new(project, - user, opts) - @merge_request = merge_service.execute - @merge_sample_data = merge_service.hook_data(@merge_request, - 'open') - - opts = { - title: "Awesome wiki_page", - content: "Some text describing some thing or another", - format: "md", - message: "user created page: Awesome wiki_page" - } - - wiki_page_service = WikiPages::CreateService.new(project, user, opts) - @wiki_page = wiki_page_service.execute - @wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create') - end - - it "calls Slack API for push events" do - slack.execute(push_sample_data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - - it "calls Slack API for issue events" do - slack.execute(@issues_sample_data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - - it "calls Slack API for merge requests events" do - slack.execute(@merge_sample_data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - - it "calls Slack API for wiki page events" do - slack.execute(@wiki_page_sample_data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - - it 'uses the username as an option for slack when configured' do - allow(slack).to receive(:username).and_return(username) - expect(Slack::Notifier).to receive(:new). - with(webhook_url, username: username). - and_return( - double(:slack_service).as_null_object - ) - - slack.execute(push_sample_data) - end - - it 'uses the channel as an option when it is configured' do - allow(slack).to receive(:channel).and_return(channel) - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: channel). - and_return( - double(:slack_service).as_null_object - ) - slack.execute(push_sample_data) - end - - context "event channels" do - it "uses the right channel for push event" do - slack.update_attributes(push_channel: "random") - - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( - double(:slack_service).as_null_object - ) - - slack.execute(push_sample_data) - end - - it "uses the right channel for merge request event" do - slack.update_attributes(merge_request_channel: "random") - - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( - double(:slack_service).as_null_object - ) - - slack.execute(@merge_sample_data) - end - - it "uses the right channel for issue event" do - slack.update_attributes(issue_channel: "random") - - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( - double(:slack_service).as_null_object - ) - - slack.execute(@issues_sample_data) - end - - it "uses the right channel for wiki event" do - slack.update_attributes(wiki_page_channel: "random") - - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( - double(:slack_service).as_null_object - ) - - slack.execute(@wiki_page_sample_data) - end - - context "note event" do - let(:issue_note) do - create(:note_on_issue, project: project, note: "issue note") - end - - it "uses the right channel" do - slack.update_attributes(note_channel: "random") - - note_data = Gitlab::DataBuilder::Note.build(issue_note, user) - - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( - double(:slack_service).as_null_object - ) - - slack.execute(note_data) - end - end - end - end - - describe "Note events" do - let(:user) { create(:user) } - let(:project) { create(:project, creator_id: user.id) } - - before do - allow(slack).to receive_messages( - project: project, - project_id: project.id, - service_hook: true, - webhook: webhook_url - ) - - WebMock.stub_request(:post, webhook_url) - end - - context 'when commit comment event executed' do - let(:commit_note) do - create(:note_on_commit, author: user, - project: project, - commit_id: project.repository.commit.id, - note: 'a comment on a commit') - end - - it "calls Slack API for commit comment events" do - data = Gitlab::DataBuilder::Note.build(commit_note, user) - slack.execute(data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - end - - context 'when merge request comment event executed' do - let(:merge_request_note) do - create(:note_on_merge_request, project: project, - note: "merge request note") - end - - it "calls Slack API for merge request comment events" do - data = Gitlab::DataBuilder::Note.build(merge_request_note, user) - slack.execute(data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - end - - context 'when issue comment event executed' do - let(:issue_note) do - create(:note_on_issue, project: project, note: "issue note") - end - - it "calls Slack API for issue comment events" do - data = Gitlab::DataBuilder::Note.build(issue_note, user) - slack.execute(data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - end - - context 'when snippet comment event executed' do - let(:snippet_note) do - create(:note_on_project_snippet, project: project, - note: "snippet note") - end - - it "calls Slack API for snippet comment events" do - data = Gitlab::DataBuilder::Note.build(snippet_note, user) - slack.execute(data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - end - end - - describe 'Pipeline events' do - let(:user) { create(:user) } - let(:project) { create(:project) } - - let(:pipeline) do - create(:ci_pipeline, - project: project, status: status, - sha: project.commit.sha, ref: project.default_branch) - end - - before do - allow(slack).to receive_messages( - project: project, - service_hook: true, - webhook: webhook_url - ) - end - - shared_examples 'call Slack API' do - before do - WebMock.stub_request(:post, webhook_url) - end - - it 'calls Slack API for pipeline events' do - data = Gitlab::DataBuilder::Pipeline.build(pipeline) - slack.execute(data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - end - - context 'with failed pipeline' do - let(:status) { 'failed' } - - it_behaves_like 'call Slack API' - end - - context 'with succeeded pipeline' do - let(:status) { 'success' } - - context 'with default to notify_only_broken_pipelines' do - it 'does not call Slack API for pipeline events' do - data = Gitlab::DataBuilder::Pipeline.build(pipeline) - result = slack.execute(data) - - expect(result).to be_falsy - end - end - - context 'with setting notify_only_broken_pipelines to false' do - before do - slack.notify_only_broken_pipelines = false - end - - it_behaves_like 'call Slack API' - end - end - end + it_behaves_like "slack or mattermost" end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 21ff238841e..1d8e42202ea 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -23,6 +23,7 @@ describe Project, models: true do it { is_expected.to have_many(:chat_services) } it { is_expected.to have_one(:forked_project_link).dependent(:destroy) } it { is_expected.to have_one(:slack_service).dependent(:destroy) } + it { is_expected.to have_one(:mattermost_service).dependent(:destroy) } it { is_expected.to have_one(:pushover_service).dependent(:destroy) } it { is_expected.to have_one(:asana_service).dependent(:destroy) } it { is_expected.to have_many(:boards).dependent(:destroy) } diff --git a/spec/support/slack_mattermost_shared_examples.rb b/spec/support/slack_mattermost_shared_examples.rb new file mode 100644 index 00000000000..56d4965f74d --- /dev/null +++ b/spec/support/slack_mattermost_shared_examples.rb @@ -0,0 +1,328 @@ +Dir[Rails.root.join("app/models/project_services/chat_message/*.rb")].each { |f| require f } + +RSpec.shared_examples 'slack or mattermost' do + let(:chat_service) { described_class.new } + let(:webhook_url) { 'https://example.gitlab.com/' } + + describe "Associations" do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe 'Validations' do + context 'when service is active' do + before { subject.active = true } + + it { is_expected.to validate_presence_of(:webhook) } + it_behaves_like 'issue tracker service URL attribute', :webhook + end + + context 'when service is inactive' do + before { subject.active = false } + + it { is_expected.not_to validate_presence_of(:webhook) } + end + end + + describe "#execute" do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:username) { 'slack_username' } + let(:channel) { 'slack_channel' } + + let(:push_sample_data) do + Gitlab::DataBuilder::Push.build_sample(project, user) + end + + before do + allow(chat_service).to receive_messages( + project: project, + project_id: project.id, + service_hook: true, + webhook: webhook_url + ) + + WebMock.stub_request(:post, webhook_url) + + opts = { + title: 'Awesome issue', + description: 'please fix' + } + + issue_service = Issues::CreateService.new(project, user, opts) + @issue = issue_service.execute + @issues_sample_data = issue_service.hook_data(@issue, 'open') + + opts = { + title: 'Awesome merge_request', + description: 'please fix', + source_branch: 'feature', + target_branch: 'master' + } + merge_service = MergeRequests::CreateService.new(project, + user, opts) + @merge_request = merge_service.execute + @merge_sample_data = merge_service.hook_data(@merge_request, + 'open') + + opts = { + title: "Awesome wiki_page", + content: "Some text describing some thing or another", + format: "md", + message: "user created page: Awesome wiki_page" + } + + wiki_page_service = WikiPages::CreateService.new(project, user, opts) + @wiki_page = wiki_page_service.execute + @wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create') + end + + it "calls Slack/Mattermost API for push events" do + chat_service.execute(push_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it "calls Slack/Mattermost API for issue events" do + chat_service.execute(@issues_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it "calls Slack/Mattermost API for merge requests events" do + chat_service.execute(@merge_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it "calls Slack/Mattermost API for wiki page events" do + chat_service.execute(@wiki_page_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it 'uses the username as an option for slack when configured' do + allow(chat_service).to receive(:username).and_return(username) + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, username: username, channel: chat_service.default_channel). + and_return( + double(:slack_service).as_null_object + ) + + chat_service.execute(push_sample_data) + end + + it 'uses the channel as an option when it is configured' do + allow(chat_service).to receive(:channel).and_return(channel) + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: channel). + and_return( + double(:slack_service).as_null_object + ) + chat_service.execute(push_sample_data) + end + + context "event channels" do + it "uses the right channel for push event" do + chat_service.update_attributes(push_channel: "random") + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + chat_service.execute(push_sample_data) + end + + it "uses the right channel for merge request event" do + chat_service.update_attributes(merge_request_channel: "random") + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + chat_service.execute(@merge_sample_data) + end + + it "uses the right channel for issue event" do + chat_service.update_attributes(issue_channel: "random") + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + chat_service.execute(@issues_sample_data) + end + + it "uses the right channel for wiki event" do + chat_service.update_attributes(wiki_page_channel: "random") + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + chat_service.execute(@wiki_page_sample_data) + end + + context "note event" do + let(:issue_note) do + create(:note_on_issue, project: project, note: "issue note") + end + + it "uses the right channel" do + chat_service.update_attributes(note_channel: "random") + + note_data = Gitlab::DataBuilder::Note.build(issue_note, user) + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + chat_service.execute(note_data) + end + end + end + end + + describe "Note events" do + let(:user) { create(:user) } + let(:project) { create(:project, creator_id: user.id) } + + before do + allow(chat_service).to receive_messages( + project: project, + project_id: project.id, + service_hook: true, + webhook: webhook_url + ) + + WebMock.stub_request(:post, webhook_url) + end + + context 'when commit comment event executed' do + let(:commit_note) do + create(:note_on_commit, author: user, + project: project, + commit_id: project.repository.commit.id, + note: 'a comment on a commit') + end + + it "calls Slack/Mattermost API for commit comment events" do + data = Gitlab::DataBuilder::Note.build(commit_note, user) + chat_service.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + end + + context 'when merge request comment event executed' do + let(:merge_request_note) do + create(:note_on_merge_request, project: project, + note: "merge request note") + end + + it "calls Slack API for merge request comment events" do + data = Gitlab::DataBuilder::Note.build(merge_request_note, user) + chat_service.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + end + + context 'when issue comment event executed' do + let(:issue_note) do + create(:note_on_issue, project: project, note: "issue note") + end + + it "calls Slack API for issue comment events" do + data = Gitlab::DataBuilder::Note.build(issue_note, user) + chat_service.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + end + + context 'when snippet comment event executed' do + let(:snippet_note) do + create(:note_on_project_snippet, project: project, + note: "snippet note") + end + + it "calls Slack API for snippet comment events" do + data = Gitlab::DataBuilder::Note.build(snippet_note, user) + chat_service.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + end + end + + describe 'Pipeline events' do + let(:user) { create(:user) } + let(:project) { create(:project) } + + let(:pipeline) do + create(:ci_pipeline, + project: project, status: status, + sha: project.commit.sha, ref: project.default_branch) + end + + before do + allow(chat_service).to receive_messages( + project: project, + service_hook: true, + webhook: webhook_url + ) + end + + shared_examples 'call Slack/Mattermost API' do + before do + WebMock.stub_request(:post, webhook_url) + end + + it 'calls Slack/Mattermost API for pipeline events' do + data = Gitlab::DataBuilder::Pipeline.build(pipeline) + chat_service.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + end + + context 'with failed pipeline' do + let(:status) { 'failed' } + + it_behaves_like 'call Slack/Mattermost API' + end + + context 'with succeeded pipeline' do + let(:status) { 'success' } + + context 'with default to notify_only_broken_pipelines' do + it 'does not call Slack/Mattermost API for pipeline events' do + data = Gitlab::DataBuilder::Pipeline.build(pipeline) + result = chat_service.execute(data) + + expect(result).to be_falsy + end + end + + context 'with setting notify_only_broken_pipelines to false' do + before do + chat_service.notify_only_broken_pipelines = false + end + + it_behaves_like 'call Slack/Mattermost API' + end + end + end +end -- cgit v1.2.1 From 6342ca367a32e55ec6b5ef8b3e8f9e13387b6615 Mon Sep 17 00:00:00 2001 From: Pedro Moreira da Silva <pedro@gitlab.com> Date: Wed, 30 Nov 2016 16:04:32 +0000 Subject: =?UTF-8?q?Improve=20`issue=20create=20=E2=80=A6`=20slash=20comman?= =?UTF-8?q?d=20with=20user=20input=20keys=20to=20create=20a=20newline=20in?= =?UTF-8?q?=20chat=20clients.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/gitlab/chat_commands/issue_create.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb index 1dba85c1b51..e6e8ce85e98 100644 --- a/lib/gitlab/chat_commands/issue_create.rb +++ b/lib/gitlab/chat_commands/issue_create.rb @@ -8,7 +8,7 @@ module Gitlab end def self.help_message - 'issue new <title>\n<description>' + 'issue create <title>` *`Shift`* + *`Enter`* `<description>' end def self.allowed?(project, user) -- cgit v1.2.1 From ef7fd97901c3f3820f08f93f97dd8fdbf0616648 Mon Sep 17 00:00:00 2001 From: Pedro Moreira da Silva <pedro@gitlab.com> Date: Wed, 30 Nov 2016 16:30:03 +0000 Subject: Update Mattermost slash commands docs to explain how to create a newline and use <kbd> for user input. See HTML5 spec: https://www.w3.org/TR/html5/text-level-semantics.html#the-kbd-element --- doc/project_services/mattermost_slash_commands.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/doc/project_services/mattermost_slash_commands.md b/doc/project_services/mattermost_slash_commands.md index 6fcbf6f1642..a4d2618e567 100644 --- a/doc/project_services/mattermost_slash_commands.md +++ b/doc/project_services/mattermost_slash_commands.md @@ -65,7 +65,7 @@ the administrator console. ### Step 3. Create a new custom slash command in Mattermost -Now that you have enabled the custom slash commands in Mattermost and opened +Now that you have enabled custom slash commands in Mattermost and opened the Mattermost slash commands service in GitLab, it's time to copy these values in a new slash command. @@ -128,20 +128,16 @@ GitLab using the Mattermost commands. ## Available slash commands -The available slash commands so far are: +The available slash commands are: | Command | Description | Example | | ------- | ----------- | ------- | -| `/<trigger> issue create <title>\n<description>` | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | `/trigger issue create We need to change the homepage` | -| `/<trigger> issue show <issue-number>` | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | `/trigger issue show 42` | -| `/<trigger> deploy <environment> to <environment>` | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | `/trigger deploy staging to production` | +| <kbd>/<trigger> issue create <title> **Shift** + **Enter** <description></kbd> | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | <samp>/gitlab issue create We need to change the homepage</samp> | +| <kbd>/<trigger> issue show <issue-number></kbd> | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | <samp>/gitlab issue show 42</samp> | +| <kbd>/<trigger> deploy <environment> to <environment></kbd> | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | <samp>/gitlab deploy staging to production</samp> | -To see a list of available commands that can interact with GitLab, type the -trigger word followed by `help`: - -``` -/my-project help -``` +To see a list of available commands to interact with GitLab, type the +trigger word followed by <kbd>help</kbd>. Example: <samp>/gitlab help</samp> ![Mattermost bot available commands](img/mattermost_bot_available_commands.png) -- cgit v1.2.1 From 6e8af6362ab7028e21a3073b221bf9a2703a1dda Mon Sep 17 00:00:00 2001 From: Pedro Moreira da Silva <pedro@gitlab.com> Date: Wed, 7 Dec 2016 12:15:41 +0000 Subject: Add changelog for !7850. https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7850 --- ...most-slash-command-for-issue-create-needs-better-documentation.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml diff --git a/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml b/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml new file mode 100644 index 00000000000..531b0f83099 --- /dev/null +++ b/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml @@ -0,0 +1,4 @@ +--- +title: Improve help message for issue create slash command +merge_request: 7850 +author: -- cgit v1.2.1 From 70e72e8938b169f88e5b014500a9afeb6ff66a2b Mon Sep 17 00:00:00 2001 From: Pedro Moreira da Silva <pedro@gitlab.com> Date: Thu, 15 Dec 2016 13:11:43 +0000 Subject: Rename `issue create` slash command to `issue new` --- doc/project_services/mattermost_slash_commands.md | 2 +- lib/gitlab/chat_commands/issue_create.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/project_services/mattermost_slash_commands.md b/doc/project_services/mattermost_slash_commands.md index a4d2618e567..1a7c13a29b4 100644 --- a/doc/project_services/mattermost_slash_commands.md +++ b/doc/project_services/mattermost_slash_commands.md @@ -132,7 +132,7 @@ The available slash commands are: | Command | Description | Example | | ------- | ----------- | ------- | -| <kbd>/<trigger> issue create <title> **Shift** + **Enter** <description></kbd> | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | <samp>/gitlab issue create We need to change the homepage</samp> | +| <kbd>/<trigger> issue new <title> <kbd>⇧ Shift</kbd>+<kbd>↵ Enter</kbd> <description></kbd> | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | <samp>/gitlab issue new We need to change the homepage</samp> | | <kbd>/<trigger> issue show <issue-number></kbd> | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | <samp>/gitlab issue show 42</samp> | | <kbd>/<trigger> deploy <environment> to <environment></kbd> | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | <samp>/gitlab deploy staging to production</samp> | diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb index e6e8ce85e98..cefb6775db8 100644 --- a/lib/gitlab/chat_commands/issue_create.rb +++ b/lib/gitlab/chat_commands/issue_create.rb @@ -8,7 +8,7 @@ module Gitlab end def self.help_message - 'issue create <title>` *`Shift`* + *`Enter`* `<description>' + 'issue new <title> *`⇧ Shift`*+*`↵ Enter`* <description>' end def self.allowed?(project, user) -- cgit v1.2.1 From 35a3e9183052bab847c30203f27fea9cf77901a4 Mon Sep 17 00:00:00 2001 From: Nick Thomas <nick@gitlab.com> Date: Wed, 14 Dec 2016 22:58:44 +0000 Subject: Make the index on environment name and project id unique, fixing up any duplicates --- ...1207231620_fixup_environment_name_uniqueness.rb | 53 ++++++++++++++++++++++ ...7231621_create_environment_name_unique_index.rb | 18 ++++++++ db/schema.rb | 6 +-- 3 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20161207231620_fixup_environment_name_uniqueness.rb create mode 100644 db/migrate/20161207231621_create_environment_name_unique_index.rb diff --git a/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb new file mode 100644 index 00000000000..b74552e762d --- /dev/null +++ b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb @@ -0,0 +1,53 @@ +class FixupEnvironmentNameUniqueness < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = 'Renaming non-unique environments' + + def up + environments = Arel::Table.new(:environments) + + # Get all [project_id, name] pairs that occur more than once + finder_sql = environments. + group(environments[:project_id], environments[:name]). + having(Arel.sql("COUNT(1)").gt(1)). + project(environments[:project_id], environments[:name]). + to_sql + + conflicting = connection.exec_query(finder_sql) + + conflicting.rows.each do |project_id, name| + fix_duplicates(project_id, name) + end + end + + def down + # Nothing to do + end + + # Rename conflicting environments by appending "-#{id}" to all but the first + def fix_duplicates(project_id, name) + environments = Arel::Table.new(:environments) + finder_sql = environments. + where(environments[:project_id].eq(project_id)). + where(environments[:name].eq(name)). + order(environments[:id].asc). + project(environments[:id], environments[:name]). + to_sql + + # Now we have the data for all the conflicting rows + conflicts = connection.exec_query(finder_sql).rows + conflicts.shift # Leave the first row alone + + conflicts.each do |id, name| + update_sql = + Arel::UpdateManager.new(ActiveRecord::Base). + table(environments). + set(environments[:name] => name + "-" + id.to_s). + where(environments[:id].eq(id)). + to_sql + + connection.exec_update(update_sql, self.class.name, []) + end + end +end diff --git a/db/migrate/20161207231621_create_environment_name_unique_index.rb b/db/migrate/20161207231621_create_environment_name_unique_index.rb new file mode 100644 index 00000000000..ac680c8d10f --- /dev/null +++ b/db/migrate/20161207231621_create_environment_name_unique_index.rb @@ -0,0 +1,18 @@ +class CreateEnvironmentNameUniqueIndex < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = true + DOWNTIME_REASON = 'Making a non-unique index into a unique index' + + def up + remove_index :environments, [:project_id, :name] + add_concurrent_index :environments, [:project_id, :name], unique: true + end + + def down + remove_index :environments, [:project_id, :name], unique: true + add_concurrent_index :environments, [:project_id, :name] + end +end diff --git a/db/schema.rb b/db/schema.rb index 4711b7873af..83c8ad48537 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: 20161212142807) do +ActiveRecord::Schema.define(version: 20161207231621) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -430,7 +430,7 @@ ActiveRecord::Schema.define(version: 20161212142807) do t.string "state", default: "available", null: false end - add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree + add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", unique: true, using: :btree create_table "events", force: :cascade do |t| t.string "target_type" @@ -1290,4 +1290,4 @@ ActiveRecord::Schema.define(version: 20161212142807) do add_foreign_key "subscriptions", "projects", on_delete: :cascade add_foreign_key "trending_projects", "projects", on_delete: :cascade add_foreign_key "u2f_registrations", "users" -end \ No newline at end of file +end -- cgit v1.2.1 From 93a03cd92f6418fbeaf126c30c161ab40d377e94 Mon Sep 17 00:00:00 2001 From: Nick Thomas <nick@gitlab.com> Date: Thu, 8 Dec 2016 01:09:18 +0000 Subject: Add an environment slug --- app/models/environment.rb | 50 ++++++++++++++++++ .../unreleased/22864-add-environment-slug.yml | 4 ++ db/migrate/20161207231626_add_environment_slug.rb | 60 ++++++++++++++++++++++ ...153400_add_unique_index_for_environment_slug.rb | 15 ++++++ db/schema.rb | 14 ++--- doc/api/enviroments.md | 8 ++- lib/api/entities.rb | 2 +- lib/api/environments.rb | 3 ++ lib/api/helpers/custom_validators.rb | 14 +++++ lib/gitlab/regex.rb | 9 ++++ spec/lib/gitlab/regex_spec.rb | 16 ++++++ spec/models/environment_spec.rb | 48 +++++++++++++---- spec/requests/api/environments_spec.rb | 17 ++++++ 13 files changed, 242 insertions(+), 18 deletions(-) create mode 100644 changelogs/unreleased/22864-add-environment-slug.yml create mode 100644 db/migrate/20161207231626_add_environment_slug.rb create mode 100644 db/migrate/20161209153400_add_unique_index_for_environment_slug.rb create mode 100644 lib/api/helpers/custom_validators.rb diff --git a/app/models/environment.rb b/app/models/environment.rb index 96700143ddd..0abbf674b9d 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -1,9 +1,15 @@ class Environment < ActiveRecord::Base + # Used to generate random suffixes for the slug + NUMBERS = '0'..'9' + SUFFIX_CHARS = ('a'..'z').to_a + NUMBERS.to_a + belongs_to :project, required: true, validate: true has_many :deployments before_validation :nullify_external_url + before_validation :generate_slug, if: ->(env) { env.slug.blank? } + before_save :set_environment_type validates :name, @@ -13,6 +19,13 @@ class Environment < ActiveRecord::Base format: { with: Gitlab::Regex.environment_name_regex, message: Gitlab::Regex.environment_name_regex_message } + validates :slug, + presence: true, + uniqueness: { scope: :project_id }, + length: { maximum: 24 }, + format: { with: Gitlab::Regex.environment_slug_regex, + message: Gitlab::Regex.environment_slug_regex_message } + validates :external_url, uniqueness: { scope: :project_id }, length: { maximum: 255 }, @@ -107,4 +120,41 @@ class Environment < ActiveRecord::Base action.expanded_environment_name == environment end end + + # An environment name is not necessarily suitable for use in URLs, DNS + # or other third-party contexts, so provide a slugified version. A slug has + # the following properties: + # * contains only lowercase letters (a-z), numbers (0-9), and '-' + # * begins with a letter + # * has a maximum length of 24 bytes (OpenShift limitation) + # * cannot end with `-` + def generate_slug + # Lowercase letters and numbers only + slugified = name.to_s.downcase.gsub(/[^a-z0-9]/, '-') + + # Must start with a letter + slugified = "env-" + slugified if NUMBERS.cover?(slugified[0]) + + # Maximum length: 24 characters (OpenShift limitation) + slugified = slugified[0..23] + + # Cannot end with a "-" character (Kubernetes label limitation) + slugified = slugified[0..-2] if slugified[-1] == "-" + + # Add a random suffix, shortening the current string if necessary, if it + # has been slugified. This ensures uniqueness. + slugified = slugified[0..16] + "-" + random_suffix if slugified != name + + self.slug = slugified + end + + private + + # Slugifying a name may remove the uniqueness guarantee afforded by it being + # based on name (which must be unique). To compensate, we add a random + # 6-byte suffix in those circumstances. This is not *guaranteed* uniqueness, + # but the chance of collisions is vanishingly small + def random_suffix + (0..5).map { SUFFIX_CHARS.sample }.join + end end diff --git a/changelogs/unreleased/22864-add-environment-slug.yml b/changelogs/unreleased/22864-add-environment-slug.yml new file mode 100644 index 00000000000..f90f79337d5 --- /dev/null +++ b/changelogs/unreleased/22864-add-environment-slug.yml @@ -0,0 +1,4 @@ +--- +title: Add a slug to environments +merge_request: 7983 +author: diff --git a/db/migrate/20161207231626_add_environment_slug.rb b/db/migrate/20161207231626_add_environment_slug.rb new file mode 100644 index 00000000000..7153e6a32b1 --- /dev/null +++ b/db/migrate/20161207231626_add_environment_slug.rb @@ -0,0 +1,60 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddEnvironmentSlug < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = 'Adding NOT NULL column environments.slug with dependent data' + + # Used to generate random suffixes for the slug + NUMBERS = '0'..'9' + SUFFIX_CHARS = ('a'..'z').to_a + NUMBERS.to_a + + def up + environments = Arel::Table.new(:environments) + + add_column :environments, :slug, :string + finder = environments.project(:id, :name) + + connection.exec_query(finder.to_sql).rows.each do |id, name| + updater = Arel::UpdateManager.new(ActiveRecord::Base). + table(environments). + set(environments[:slug] => generate_slug(name)). + where(environments[:id].eq(id)) + + connection.exec_update(updater.to_sql, self.class.name, []) + end + + change_column_null :environments, :slug, false + end + + def down + remove_column :environments, :slug + end + + # Copy of the Environment#generate_slug implementation + def generate_slug(name) + # Lowercase letters and numbers only + slugified = name.to_s.downcase.gsub(/[^a-z0-9]/, '-') + + # Must start with a letter + slugified = "env-" + slugified if NUMBERS.cover?(slugified[0]) + + # Maximum length: 24 characters (OpenShift limitation) + slugified = slugified[0..23] + + # Cannot end with a "-" character (Kubernetes label limitation) + slugified = slugified[0..-2] if slugified[-1] == "-" + + # Add a random suffix, shortening the current string if necessary, if it + # has been slugified. This ensures uniqueness. + slugified = slugified[0..16] + "-" + random_suffix if slugified != name + + slugified + end + + def random_suffix + (0..5).map { SUFFIX_CHARS.sample }.join + end +end diff --git a/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb new file mode 100644 index 00000000000..e9fcef1cd45 --- /dev/null +++ b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb @@ -0,0 +1,15 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddUniqueIndexForEnvironmentSlug < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = 'Adding a *unique* index to environments.slug' + + disable_ddl_transaction! + + def change + add_concurrent_index :environments, [:project_id, :slug], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 83c8ad48537..67ff83d96d9 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: 20161207231621) do +ActiveRecord::Schema.define(version: 20161212142807) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -98,14 +98,14 @@ ActiveRecord::Schema.define(version: 20161207231621) do t.text "help_page_text_html" t.text "shared_runners_text_html" t.text "after_sign_up_text_html" + t.boolean "sidekiq_throttling_enabled", default: false + t.string "sidekiq_throttling_queues" + t.decimal "sidekiq_throttling_factor" t.boolean "housekeeping_enabled", default: true, null: false t.boolean "housekeeping_bitmaps_enabled", default: true, null: false t.integer "housekeeping_incremental_repack_period", default: 10, null: false t.integer "housekeeping_full_repack_period", default: 50, null: false t.integer "housekeeping_gc_period", default: 200, null: false - t.boolean "sidekiq_throttling_enabled", default: false - t.string "sidekiq_throttling_queues" - t.decimal "sidekiq_throttling_factor" t.boolean "html_emails_enabled", default: true end @@ -428,9 +428,11 @@ ActiveRecord::Schema.define(version: 20161207231621) do t.string "external_url" t.string "environment_type" t.string "state", default: "available", null: false + t.string "slug", null: false end add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", unique: true, using: :btree + add_index "environments", ["project_id", "slug"], name: "index_environments_on_project_id_and_slug", unique: true, using: :btree create_table "events", force: :cascade do |t| t.string "target_type" @@ -737,8 +739,8 @@ ActiveRecord::Schema.define(version: 20161207231621) do t.integer "visibility_level", default: 20, null: false t.boolean "request_access_enabled", default: false, null: false t.datetime "deleted_at" - t.text "description_html" t.boolean "lfs_enabled" + t.text "description_html" t.integer "parent_id" end @@ -1219,8 +1221,8 @@ ActiveRecord::Schema.define(version: 20161207231621) do t.datetime "otp_grace_period_started_at" t.boolean "ldap_email", default: false, null: false t.boolean "external", default: false - t.string "incoming_email_token" t.string "organization" + t.string "incoming_email_token" t.boolean "authorized_projects_populated" end diff --git a/doc/api/enviroments.md b/doc/api/enviroments.md index 87a5fa67124..1299aca8c45 100644 --- a/doc/api/enviroments.md +++ b/doc/api/enviroments.md @@ -22,8 +22,9 @@ Example response: [ { "id": 1, - "name": "Env1", - "external_url": "https://env1.example.gitlab.com" + "name": "review/fix-foo", + "slug": "review-fix-foo-dfjre3", + "external_url": "https://review-fix-foo-dfjre3.example.gitlab.com" } ] ``` @@ -54,6 +55,7 @@ Example response: { "id": 1, "name": "deploy", + "slug": "deploy", "external_url": "https://deploy.example.gitlab.com" } ``` @@ -85,6 +87,7 @@ Example response: { "id": 1, "name": "staging", + "slug": "staging", "external_url": "https://staging.example.gitlab.com" } ``` @@ -112,6 +115,7 @@ Example response: { "id": 1, "name": "deploy", + "slug": "deploy", "external_url": "https://deploy.example.gitlab.com" } ``` diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 01c0f5072ba..dfbb3ab86dd 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -629,7 +629,7 @@ module API end class EnvironmentBasic < Grape::Entity - expose :id, :name, :external_url + expose :id, :name, :slug, :external_url end class Environment < EnvironmentBasic diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 80bbd9bb6e4..1a7e68f0528 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -1,6 +1,7 @@ module API # Environments RESTfull API endpoints class Environments < Grape::API + include ::API::Helpers::CustomValidators include PaginationParams before { authenticate! } @@ -29,6 +30,7 @@ module API params do requires :name, type: String, desc: 'The name of the environment to be created' optional :external_url, type: String, desc: 'URL on which this deployment is viewable' + optional :slug, absence: { message: "is automatically generated and cannot be changed" } end post ':id/environments' do authorize! :create_environment, user_project @@ -50,6 +52,7 @@ module API requires :environment_id, type: Integer, desc: 'The environment ID' optional :name, type: String, desc: 'The new environment name' optional :external_url, type: String, desc: 'The new URL on which this deployment is viewable' + optional :slug, absence: { message: "is automatically generated and cannot be changed" } end put ':id/environments/:environment_id' do authorize! :update_environment, user_project diff --git a/lib/api/helpers/custom_validators.rb b/lib/api/helpers/custom_validators.rb new file mode 100644 index 00000000000..0a8f3073a50 --- /dev/null +++ b/lib/api/helpers/custom_validators.rb @@ -0,0 +1,14 @@ +module API + module Helpers + module CustomValidators + class Absence < Grape::Validations::Base + def validate_param!(attr_name, params) + return if params.respond_to?(:key?) && !params.key?(attr_name) + raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:absence) + end + end + end + end +end + +Grape::Validations.register_validator(:absence, ::API::Helpers::CustomValidators::Absence) diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 7c711d581e8..9e0b0e5ea98 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -131,5 +131,14 @@ module Gitlab def kubernetes_namespace_regex_message "can contain only letters, digits or '-', and cannot start or end with '-'" end + + def environment_slug_regex + @environment_slug_regex ||= /\A[a-z]([a-z0-9-]*[a-z0-9])?\z/.freeze + end + + def environment_slug_regex_message + "can contain only lowercase letters, digits, and '-'. " \ + "Must start with a letter, and cannot end with '-'" + end end end diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index c51b10bdc69..c78cd30157e 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -29,4 +29,20 @@ describe Gitlab::Regex, lib: true do describe 'file path regex' do it { expect('foo@/bar').to match(Gitlab::Regex.file_path_regex) } end + + describe 'environment slug regex' do + def be_matched + match(Gitlab::Regex.environment_slug_regex) + end + + it { expect('foo').to be_matched } + it { expect('foo-1').to be_matched } + + it { expect('FOO').not_to be_matched } + it { expect('foo/1').not_to be_matched } + it { expect('foo.1').not_to be_matched } + it { expect('foo*1').not_to be_matched } + it { expect('9foo').not_to be_matched } + it { expect('foo-').not_to be_matched } + end end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index c8170164898..706f1a5cd1c 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Environment, models: true do - let(:environment) { create(:environment) } + subject(:environment) { create(:environment) } it { is_expected.to belong_to(:project) } it { is_expected.to have_many(:deployments) } @@ -15,15 +15,11 @@ describe Environment, models: true do it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) } it { is_expected.to validate_length_of(:name).is_at_most(255) } - it { is_expected.to validate_length_of(:external_url).is_at_most(255) } - - # To circumvent a not null violation of the name column: - # https://github.com/thoughtbot/shoulda-matchers/issues/336 - it 'validates uniqueness of :external_url' do - create(:environment) + it { is_expected.to validate_uniqueness_of(:slug).scoped_to(:project_id) } + it { is_expected.to validate_length_of(:slug).is_at_most(24) } - is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) - end + it { is_expected.to validate_length_of(:external_url).is_at_most(255) } + it { is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) } describe '#nullify_external_url' do it 'replaces a blank url with nil' do @@ -199,4 +195,38 @@ describe Environment, models: true do expect(environment.actions_for('review/master')).to contain_exactly(review_action) end end + + describe '#slug' do + it "is automatically generated" do + expect(environment.slug).not_to be_nil + end + + it "is not regenerated if name changes" do + original_slug = environment.slug + environment.update_attributes!(name: environment.name.reverse) + + expect(environment.slug).to eq(original_slug) + end + end + + describe '#generate_slug' do + SUFFIX = "-[a-z0-9]{6}" + { + "staging-12345678901234567" => "staging-123456789" + SUFFIX, + "9-staging-123456789012345" => "env-9-staging-123" + SUFFIX, + "staging-1234567890123456" => "staging-1234567890123456", + "production" => "production", + "PRODUCTION" => "production" + SUFFIX, + "review/1-foo" => "review-1-foo" + SUFFIX, + "1-foo" => "env-1-foo" + SUFFIX, + "1/foo" => "env-1-foo" + SUFFIX, + "foo-" => "foo" + SUFFIX, + }.each do |name, matcher| + it "returns a slug matching #{matcher}, given #{name}" do + slug = described_class.new(name: name).generate_clean_name + + expect(slug).to match(/\A#{matcher}\z/) + end + end + end end diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 126496c43a5..b9d535bc314 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -46,6 +46,7 @@ describe API::Environments, api: true do expect(response).to have_http_status(201) expect(json_response['name']).to eq('mepmep') + expect(json_response['slug']).to eq('mepmep') expect(json_response['external']).to be nil end @@ -60,6 +61,13 @@ describe API::Environments, api: true do expect(response).to have_http_status(400) end + + it 'returns a 400 if slug is specified' do + post api("/projects/#{project.id}/environments", user), name: "foo", slug: "foo" + + expect(response).to have_http_status(400) + expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed") + end end context 'a non member' do @@ -86,6 +94,15 @@ describe API::Environments, api: true do expect(json_response['external_url']).to eq(url) end + it "won't allow slug to be changed" do + slug = environment.slug + api_url = api("/projects/#{project.id}/environments/#{environment.id}", user) + put api_url, slug: slug + "-foo" + + expect(response).to have_http_status(400) + expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed") + end + it "won't update the external_url if only the name is passed" do url = environment.external_url put api("/projects/#{project.id}/environments/#{environment.id}", user), -- cgit v1.2.1 From 58486918fc12bbcc5139b6ca32461ad5e037497b Mon Sep 17 00:00:00 2001 From: Nick Thomas <nick@gitlab.com> Date: Thu, 8 Dec 2016 15:37:41 +0000 Subject: Create environments when the build referencing them is created --- app/services/ci/create_pipeline_builds_service.rb | 15 +++++++++++++-- spec/models/environment_spec.rb | 2 +- spec/services/ci/create_pipeline_service_spec.rb | 17 +++++++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/app/services/ci/create_pipeline_builds_service.rb b/app/services/ci/create_pipeline_builds_service.rb index 005014fa1de..b7da3f8e7eb 100644 --- a/app/services/ci/create_pipeline_builds_service.rb +++ b/app/services/ci/create_pipeline_builds_service.rb @@ -10,18 +10,29 @@ module Ci end end + def project + pipeline.project + end + private def create_build(build_attributes) build_attributes = build_attributes.merge( pipeline: pipeline, - project: pipeline.project, + project: project, ref: pipeline.ref, tag: pipeline.tag, user: current_user, trigger_request: trigger_request ) - pipeline.builds.create(build_attributes) + build = pipeline.builds.create(build_attributes) + + # Create the environment before the build starts. This sets its slug and + # makes it available as an environment variable + project.environments.find_or_create_by(name: build.expanded_environment_name) if + build.has_environment? + + build end def new_builds diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 706f1a5cd1c..97cbb093ed2 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -223,7 +223,7 @@ describe Environment, models: true do "foo-" => "foo" + SUFFIX, }.each do |name, matcher| it "returns a slug matching #{matcher}, given #{name}" do - slug = described_class.new(name: name).generate_clean_name + slug = described_class.new(name: name).generate_slug expect(slug).to match(/\A#{matcher}\z/) end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 4aadd009f3e..ceaca96e25b 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -210,5 +210,22 @@ describe Ci::CreatePipelineService, services: true do expect(result.manual_actions).not_to be_empty end end + + context 'with environment' do + before do + config = YAML.dump(deploy: { environment: { name: "review/$CI_BUILD_REF_NAME" }, script: 'ls' }) + stub_ci_pipeline_yaml_file(config) + end + + it 'creates the environment' do + result = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: [{ message: 'some msg' }]) + + expect(result).to be_persisted + expect(Environment.find_by(name: "review/master")).not_to be_nil + end + end end end -- cgit v1.2.1 From ffaf25b76a167cd9e0260175181314db2d735ce0 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Sat, 3 Dec 2016 11:28:19 +0100 Subject: Add Okta authentication documentation --- doc/administration/auth/README.md | 3 +- doc/administration/auth/img/okta_admin_panel.png | Bin 0 -> 26164 bytes doc/administration/auth/img/okta_saml_settings.png | Bin 0 -> 25470 bytes doc/administration/auth/okta.md | 160 +++++++++++++++++++++ 4 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 doc/administration/auth/img/okta_admin_panel.png create mode 100644 doc/administration/auth/img/okta_saml_settings.png create mode 100644 doc/administration/auth/okta.md diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md index 07e548aaabe..2fc5d0355b5 100644 --- a/doc/administration/auth/README.md +++ b/doc/administration/auth/README.md @@ -7,5 +7,6 @@ providers. and 389 Server - [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, Crowd and Azure -- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider - [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS +- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider +- [Okta](okta.md) Configure GitLab to sign in using Okta diff --git a/doc/administration/auth/img/okta_admin_panel.png b/doc/administration/auth/img/okta_admin_panel.png new file mode 100644 index 00000000000..12e21956715 Binary files /dev/null and b/doc/administration/auth/img/okta_admin_panel.png differ diff --git a/doc/administration/auth/img/okta_saml_settings.png b/doc/administration/auth/img/okta_saml_settings.png new file mode 100644 index 00000000000..ee275ece369 Binary files /dev/null and b/doc/administration/auth/img/okta_saml_settings.png differ diff --git a/doc/administration/auth/okta.md b/doc/administration/auth/okta.md new file mode 100644 index 00000000000..cb42b7743c5 --- /dev/null +++ b/doc/administration/auth/okta.md @@ -0,0 +1,160 @@ +# Okta SSO provider + +Okta is a [Single Sign-on provider][okta-sso] that can be used to authenticate +with GitLab. + +The following documentation enables Okta as a SAML provider. + +## Configure the Okta application + +1. On Okta go to the admin section and choose to **Add an App**. +1. When the app screen comes up you see another button to **Create an App** and + choose SAML 2.0 on the next screen. +1. Now, very important, add a logo + (you can choose it from https://about.gitlab.com/press/). You'll have to + crop and resize it. +1. Next, you'll need the to fill in the SAML general config. Here's an example + image. + + ![Okta admin panel view](img/okta_admin_panel.png) + +1. The last part of the configuration is the feedback section where you can + just say you're a customer and creating an app for internal use. +1. When you have your app you'll have a few tabs on the top of the app's + profile. Click on the SAML 2.0 config instructions button which should + look like the following: + + ![Okta SAML settings](img/okta_saml_settings.png) + +1. On the screen that comes up take note of the + **Identity Provider Single Sign-On URL** which you'll use for the + `idp_sso_target_url` on your GitLab config file. + +1. **Before you leave Okta make sure you add your user and groups if any.** + +--- + +Now that the Okta app is configured, it's time to enable it in GitLab. + +## Configure GitLab + +1. On your GitLab server, open the configuration file: + + **For Omnibus GitLab installations** + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + **For installations from source** + + ```sh + cd /home/git/gitlab + sudo -u git -H editor config/gitlab.yml + ``` + +1. See [Initial OmniAuth Configuration](../../integration/omniauth.md#initial-omniauth-configuration) + for initial settings. + +1. To allow your users to use Okta to sign up without having to manually create + an account first, don't forget to add the following values to your + configuration: + + **For Omnibus GitLab installations** + + ```ruby + gitlab_rails['omniauth_allow_single_sign_on'] = ['saml'] + gitlab_rails['omniauth_block_auto_created_users'] = false + ``` + + **For installations from source** + + ```yaml + allow_single_sign_on: ["saml"] + block_auto_created_users: false + ``` + +1. You can also automatically link Okta users with existing GitLab users if + their email addresses match by adding the following setting: + + **For Omnibus GitLab installations** + + ```ruby + gitlab_rails['omniauth_auto_link_saml_user'] = true + ``` + + **For installations from source** + + ```yaml + auto_link_saml_user: true + ``` + +1. Add the provider configuration. + + >**Notes:** + >- Change the value for `assertion_consumer_service_url` to match the HTTPS endpoint + of GitLab (append `users/auth/saml/callback` to the HTTPS URL of your GitLab + installation to generate the correct value). + >- To get the `idp_cert_fingerprint` fingerprint, first download the + certificate from the Okta app you registered and then run: + `openssl x509 -in okta.cert -noout -fingerprint`. Substitute `okta.cert` + with the location of your certificate. + >- Change the value of `idp_sso_target_url`, with the value of the + **Identity Provider Single Sign-On URL** from the step when you + configured the Okta app. + >- Change the value of `issuer` to a unique name, which will identify the application + to the IdP. + >- Leave `name_identifier_format` as-is. + + **For Omnibus GitLab installations** + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + name: 'saml', + args: { + assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', + idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', + idp_sso_target_url: 'https://gitlab.oktapreview.com/app/gitlabdev773716_gitlabsaml_1/exk8odl81tBrjpD4B0h7/sso/saml', + issuer: 'https://gitlab.example.com', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + }, + label: 'Okta' # optional label for SAML login button, defaults to "Saml" + } + ] + ``` + + **For installations from source** + + ```yaml + - { + name: 'saml', + args: { + assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', + idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', + idp_sso_target_url: 'https://gitlab.oktapreview.com/app/gitlabdev773716_gitlabsaml_1/exk8odl81tBrjpD4B0h7/sso/saml', + issuer: 'https://gitlab.example.com', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + }, + label: 'Okta' # optional label for SAML login button, defaults to "Saml" + } + ``` + + +1. [Reconfigure][reconf] or [restart] GitLab for Omnibus and installations + from source respectively for the changes to take effect. + +You might want to try this out on a incognito browser window. + +## Configuring groups + +>**Note:** +Make sure the groups exist and are assigned to the Okta app. + +You can take a look of the [SAML documentation][saml] on external groups since +it works the same. + +[okta-sso]: https://www.okta.com/products/single-sign-on/ +[saml]: ../../integration/saml.md#external-groups +[reconf]: ../restart_gitlab.md#omnibus-gitlab-reconfigure +[restart]: ../restart_gitlab.md#installations-from-source -- cgit v1.2.1 From 84c319b62cffb46a160ffab7b83aec3ee28430e9 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Thu, 15 Dec 2016 15:14:44 +0100 Subject: Document `repocheck.log` in logs docs [ci skip] --- doc/administration/logs.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/doc/administration/logs.md b/doc/administration/logs.md index d757a3c2a66..facd0569107 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -136,3 +136,13 @@ I, [2015-02-13T07:16:01.530733 #9047] INFO -- : reaped #<Process::Status: pid 9 I, [2015-02-13T07:16:01.534501 #13379] INFO -- : worker=1 spawned pid=13379 I, [2015-02-13T07:16:01.534848 #13379] INFO -- : worker=1 ready ``` + +## `repocheck.log` + +This file lives in `/var/log/gitlab/gitlab-rails/repocheck.log` for +omnibus package or in `/home/git/gitlab/log/repocheck.log` for +installations from source. + +It logs information whenever a [repository check is run][repocheck] on a project. + +[repocheck]: repository_checks.md -- cgit v1.2.1 From 80513a129592583ed100e7a90fc9ea144eb62ea9 Mon Sep 17 00:00:00 2001 From: Nick Thomas <nick@gitlab.com> Date: Thu, 8 Dec 2016 16:21:16 +0000 Subject: Add $CI_ENVIRONMENT_NAME and $CI_ENVIRONMENT_SLUG --- app/models/ci/build.rb | 20 ++++++++++++++++++-- app/models/environment.rb | 7 +++++++ doc/ci/environments.md | 34 +++++++++++++++++++++++----------- doc/ci/variables/README.md | 2 ++ doc/ci/yaml/README.md | 22 ++++++++++------------ spec/models/build_spec.rb | 36 ++++++++++++++++++++++++++++++++++++ 6 files changed, 96 insertions(+), 25 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 45a416a4c41..fdbf28a1d68 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -9,6 +9,14 @@ module Ci has_many :deployments, as: :deployable + # The "environment" field for builds is a String, and is the unexpanded name + def persisted_environment + @persisted_environment ||= Environment.find_by( + name: expanded_environment_name, + project_id: gl_project_id + ) + end + serialize :options serialize :yaml_variables @@ -143,7 +151,7 @@ module Ci end def expanded_environment_name - ExpandVariables.expand(environment, variables) if environment + ExpandVariables.expand(environment, simple_variables) if environment end def has_environment? @@ -206,7 +214,8 @@ module Ci slugified.gsub(/[^a-z0-9]/, '-')[0..62] end - def variables + # Variables whose value does not depend on other variables + def simple_variables variables = predefined_variables variables += project.predefined_variables variables += pipeline.predefined_variables @@ -219,6 +228,13 @@ module Ci variables end + # All variables, including those dependent on other variables + def variables + variables = simple_variables + variables += persisted_environment.predefined_variables if persisted_environment.present? + variables + end + def merge_request merge_requests = MergeRequest.includes(:merge_request_diff) .where(source_branch: ref, source_project_id: pipeline.gl_project_id) diff --git a/app/models/environment.rb b/app/models/environment.rb index 0abbf674b9d..8ef1c841ea3 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -50,6 +50,13 @@ class Environment < ActiveRecord::Base state :stopped end + def predefined_variables + [ + { key: 'CI_ENVIRONMENT_NAME', value: name, public: true }, + { key: 'CI_ENVIRONMENT_SLUG', value: slug, public: true }, + ] + end + def recently_updated_on_branch?(ref) ref.to_s == last_deployment.try(:ref) end diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 705bca6cc1f..bad0233a9ce 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -86,6 +86,13 @@ will later see, is exposed in various places within GitLab. Each time a job that has an environment specified and succeeds, a deployment is recorded, remembering the Git SHA and environment name. +>**Note:** +Starting with GitLab 8.15, the environment name is exposed to the Runner in +two forms: `$CI_ENVIRONMENT_NAME`, and `$CI_ENVIRONMENT_SLUG`. The first is +the name given in `.gitlab-ci.yml` (with any variables expanded), while the +second is a "cleaned-up" version of the name, suitable for use in URLs, DNS, +etc. + To sum up, with the above `.gitlab-ci.yml` we have achieved that: - All branches will run the `test` and `build` jobs. @@ -157,7 +164,7 @@ that can be found in the deployments page job with the commit associated with it. >**Note:** -Bare in mind that your mileage will vary and it's entirely up to how you define +Bear in mind that your mileage will vary and it's entirely up to how you define the deployment process in the job's `script` whether the rollback succeeds or not. GitLab CI is just following orders. @@ -248,7 +255,7 @@ deploy_review: - echo "Deploy a review app" environment: name: review/$CI_BUILD_REF_NAME - url: https://$CI_BUILD_REF_SLUG.example.com + url: https://$CI_BUILD_REF_SLUG.review.example.com only: - branches except: @@ -266,9 +273,18 @@ ones. So, the first part is `review`, followed by a `/` and then `$CI_BUILD_REF_NAME` which takes the value of the branch name. Since `$CI_BUILD_REF_NAME` itself may also contain `/`, or other characters that would be invalid in a domain name or -URL, we use `$CI_BUILD_REF_SLUG` in the `environment:url` so that the environment -can get a specific and distinct URL for each branch. Again, the way you set up -the webserver to serve these requests is based on your setup. +URL, we use `$CI_ENVIRONMENT_SLUG` in the `environment:url` so that the +environment can get a specific and distinct URL for each branch. In this case, +given a `$CI_BUILD_REF_NAME` of `100-Do-The-Thing`, the URL will be something +like `https://review-100-do-the-4f99a2.example.com`. Again, the way you set up +the web server to serve these requests is based on your setup. + +You could also use `$CI_BUILD_REF_SLUG` in `environment:url`, e.g.: +`https://$CI_BUILD_REF_SLUG.review.example.com`. We use `$CI_ENVIRONMENT_SLUG` +here because it is guaranteed to be unique, but if you're using a workflow like +[GitLab Flow][gitlab-flow], collisions are very unlikely, and you may prefer +environment names to be more closely based on the branch name - the example +above would give you an URL like `https://100-do-the-thing.review.example.com` Last but not least, we tell the job to run [`only`][only] on branches [`except`][only] master. @@ -300,7 +316,7 @@ deploy_review: - echo "Deploy a review app" environment: name: review/$CI_BUILD_REF_NAME - url: https://$CI_BUILD_REF_SLUG.example.com + url: https://$CI_ENVIRONMENT_SLUG.example.com only: - branches except: @@ -419,7 +435,7 @@ deploy_review: - echo "Deploy a review app" environment: name: review/$CI_BUILD_REF_NAME - url: https://$CI_BUILD_REF_SLUG.example.com + url: https://$CI_ENVIRONMENT_SLUG.example.com on_stop: stop_review only: - branches @@ -493,10 +509,6 @@ fetch = +refs/environments/*:refs/remotes/origin/environments/* ## Limitations -1. `$CI_BUILD_REF_SLUG` is not *guaranteed* to be unique, so there is a small - chance of collisions between similarly-named branches (`fix-foo` would - conflict with `fix/foo`, for instance). Following a well-defined workflow - such as [GitLab Flow][gitlab-flow] can keep this from being a problem. 1. You are limited to use only the [CI predefined variables][variables] in the `environment: name`. If you try to re-use variables defined inside `script` as part of the environment name, it will not work. diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index e0ff9756868..eb540a50606 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -52,6 +52,8 @@ version of Runner required. | **CI_PROJECT_PATH** | 8.10 | 0.5 | The namespace with project name | | **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project | | **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the build is run | +| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this build | +| **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. | | **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry | | **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project | | **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used | diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 5e8d888e555..a62193700fc 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -690,7 +690,7 @@ The `stop_review_app` job is **required** to have the following keywords defined #### dynamic environments > [Introduced][ce-6323] in GitLab 8.12 and GitLab Runner 1.6. - `$CI_BUILD_REF_SLUG` was [introduced][ce-8072] in GitLab 8.15. + `$CI_ENVIRONMENT_SLUG` was [introduced][ce-7983] in GitLab 8.15 `environment` can also represent a configuration hash with `name` and `url`. These parameters can use any of the defined [CI variables](#variables) @@ -703,15 +703,17 @@ deploy as review app: stage: deploy script: make deploy environment: - name: review-apps/$CI_BUILD_REF_NAME - url: https://$CI_BUILD_REF_SLUG.review.example.com/ + name: review/$CI_BUILD_REF_NAME + url: https://$CI_ENVIRONMENT_SLUG.example.com/ ``` The `deploy as review app` job will be marked as deployment to dynamically -create the `review-apps/$CI_BUILD_REF_NAME` environment, which `$CI_BUILD_REF_NAME` -is an [environment variable][variables] set by the Runner. If for example the -`deploy as review app` job was run in a branch named `pow`, this environment -should be accessible under `https://pow.review.example.com/`. +create the `review/$CI_BUILD_REF_NAME` environment, where `$CI_BUILD_REF_NAME` +is an [environment variable][variables] set by the Runner. The +`$CI_ENVIRONMENT_SLUG` variable is based on the environment name, but suitable +for inclusion in URLs. In this case, if the `deploy as review app` job was run +in a branch named `pow`, this environment would be accessible with an URL like +`https://review-pow-aaaaaa.example.com/`. This of course implies that the underlying server which hosts the application is properly configured. @@ -720,10 +722,6 @@ The common use case is to create dynamic environments for branches and use them as Review Apps. You can see a simple example using Review Apps at https://gitlab.com/gitlab-examples/review-apps-nginx/. -`$CI_BUILD_REF_SLUG` is another environment variable set by the runner, based on -`$CI_BUILD_REF_NAME` but lower-cased, and with some characters replaced with -`-`, making it suitable for use in URLs and domain names. - ### artifacts >**Notes:** @@ -1243,5 +1241,5 @@ CI with various languages. [ce-6323]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6323 [environment]: ../environments.md [ce-6669]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6669 -[ce-8072]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/xxxx [variables]: ../variables/README.md +[ce-7983]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7983 diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index ea60c17a58a..d5f2ffcff59 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -87,6 +87,26 @@ describe Ci::Build, models: true do end end + describe '#persisted_environment' do + before do + @environment = create(:environment, project: project, name: "foo-#{project.default_branch}") + end + + subject { build.persisted_environment } + + context 'referenced literally' do + let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}") } + + it { is_expected.to eq(@environment) } + end + + context 'referenced with a variable' do + let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_BUILD_REF_NAME") } + + it { is_expected.to eq(@environment) } + end + end + describe '#trace' do it { expect(build.trace).to be_nil } @@ -328,6 +348,22 @@ describe Ci::Build, models: true do it { user_variables.each { |v| is_expected.to include(v) } } end + context 'when build has an environment' do + before do + build.update(environment: 'production') + create(:environment, project: build.project, name: 'production', slug: 'prod-slug') + end + + let(:environment_variables) do + [ + { key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true }, + { key: 'CI_ENVIRONMENT_SLUG', value: 'prod-slug', public: true } + ] + end + + it { environment_variables.each { |v| is_expected.to include(v) } } + end + context 'when build started manually' do before do build.update_attributes(when: :manual) -- cgit v1.2.1 From a074ad5e7781b38c44f05ff6cb10bc581c47b605 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Thu, 15 Dec 2016 15:18:06 +0100 Subject: Fix headings in administration/logs.md [ci skip] --- doc/administration/logs.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/doc/administration/logs.md b/doc/administration/logs.md index facd0569107..4b8d5c5cc87 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -1,4 +1,4 @@ -## Log system +# Log system GitLab has an advanced log system where everything is logged so that you can analyze your instance using various system log files. In addition to @@ -9,10 +9,10 @@ documentation](http://docs.gitlab.com/ee/administration/audit_events.html) System log files are typically plain text in a standard log file format. This guide talks about how to read and use these system log files. -### production.log +## `production.log` This file lives in `/var/log/gitlab/gitlab-rails/production.log` for -omnibus package or in `/home/git/gitlab/log/production.log` for +Omnibus GitLab packages or in `/home/git/gitlab/log/production.log` for installations from source. (When Gitlab is running in an environment other than production, the corresponding logfile is shown here.) @@ -46,10 +46,10 @@ In this example we can see that server processed an HTTP request with URL 19:34:53 +0200. Also we can see that request was processed by `Projects::TreeController`. -### application.log +## `application.log` This file lives in `/var/log/gitlab/gitlab-rails/application.log` for -omnibus package or in `/home/git/gitlab/log/application.log` for +Omnibus GitLab packages or in `/home/git/gitlab/log/application.log` for installations from source. It helps you discover events happening in your instance such as user creation, @@ -63,10 +63,10 @@ October 07, 2014 11:25: User "Claudie Hodkiewicz" (nasir_stehr@olson.co.uk) was October 07, 2014 11:25: Project "project133" was removed ``` -### githost.log +## `githost.log` This file lives in `/var/log/gitlab/gitlab-rails/githost.log` for -omnibus package or in `/home/git/gitlab/log/githost.log` for +Omnibus GitLab packages or in `/home/git/gitlab/log/githost.log` for installations from source. GitLab has to interact with Git repositories but in some rare cases @@ -81,10 +81,10 @@ December 03, 2014 13:20 -> ERROR -> Command failed [1]: /usr/bin/git --git-dir=/ error: failed to push some refs to '/Users/vsizov/gitlab-development-kit/repositories/gitlabhq/gitlab_git.git' ``` -### sidekiq.log +## `sidekiq.log` This file lives in `/var/log/gitlab/gitlab-rails/sidekiq.log` for -omnibus package or in `/home/git/gitlab/log/sidekiq.log` for +Omnibus GitLab packages or in `/home/git/gitlab/log/sidekiq.log` for installations from source. GitLab uses background jobs for processing tasks which can take a long @@ -96,10 +96,10 @@ this file. For example: 2014-06-10T18:18:26Z 14299 TID-55uqo INFO: Booting Sidekiq 3.0.0 with redis options {:url=>"redis://localhost:6379/0", :namespace=>"sidekiq"} ``` -### gitlab-shell.log +## `gitlab-shell.log` This file lives in `/var/log/gitlab/gitlab-shell/gitlab-shell.log` for -omnibus package or in `/home/git/gitlab-shell/gitlab-shell.log` for +Omnibus GitLab packages or in `/home/git/gitlab-shell/gitlab-shell.log` for installations from source. GitLab shell is used by Gitlab for executing Git commands and provide @@ -110,10 +110,10 @@ I, [2015-02-13T06:17:00.671315 #9291] INFO -- : Adding project root/example.git I, [2015-02-13T06:17:00.679433 #9291] INFO -- : Moving existing hooks directory and symlinking global hooks directory for /var/opt/gitlab/git-data/repositories/root/example.git. ``` -### unicorn\_stderr.log +## `unicorn\_stderr.log` This file lives in `/var/log/gitlab/unicorn/unicorn_stderr.log` for -omnibus package or in `/home/git/gitlab/log/unicorn_stderr.log` for +Omnibus GitLab packages or in `/home/git/gitlab/log/unicorn_stderr.log` for installations from source. Unicorn is a high-performance forking Web server which is used for @@ -140,7 +140,7 @@ I, [2015-02-13T07:16:01.534848 #13379] INFO -- : worker=1 ready ## `repocheck.log` This file lives in `/var/log/gitlab/gitlab-rails/repocheck.log` for -omnibus package or in `/home/git/gitlab/log/repocheck.log` for +Omnibus GitLab packages or in `/home/git/gitlab/log/repocheck.log` for installations from source. It logs information whenever a [repository check is run][repocheck] on a project. -- cgit v1.2.1 From 12db4cc0e70d3e249f3bf9fde85e336839422319 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@gitlab.com> Date: Fri, 9 Dec 2016 01:56:31 +0000 Subject: Merge branch 'jej-note-search-uses-finder' into 'security' Fix missing Note access checks in by moving Note#search to updated NoteFinder Split from !2024 to partially solve https://gitlab.com/gitlab-org/gitlab-ce/issues/23867 ## Which fixes are in this MR? :warning: - Potentially untested :bomb: - No test coverage :traffic_light: - Test coverage of some sort exists (a test failed when error raised) :vertical_traffic_light: - Test coverage of return value (a test failed when nil used) :white_check_mark: - Permissions check tested ### Note lookup without access check - [x] :white_check_mark: app/finders/notes_finder.rb:13 :download_code check - [x] :white_check_mark: app/finders/notes_finder.rb:19 `SnippetsFinder` - [x] :white_check_mark: app/models/note.rb:121 [`Issue#visible_to_user`] - [x] :white_check_mark: lib/gitlab/project_search_results.rb:113 - This is the only use of `app/models/note.rb:121` above, but importantly has no access checks at all. This means it leaks MR comments and snippets when those features are `team-only` in addition to the issue comments which would be fixed by `app/models/note.rb:121`. - It is only called from SearchController where `can?(current_user, :download_code, @project)` is checked, so commit comments are not leaked. ### Previous discussions - [x] https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/2024/diffs#b915c5267a63628b0bafd23d37792ae73ceae272_13_13 `: download_code` check on commit - [x] https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/2024/diffs#b915c5267a63628b0bafd23d37792ae73ceae272_19_19 `SnippetsFinder` should be used - `SnippetsFinder` should check if the snippets feature is enabled -> https://gitlab.com/gitlab-org/gitlab-ce/issues/25223 ### Acceptance criteria met? - [x] Tests added for new code - [x] TODO comments removed - [x] Squashed and removed skipped tests - [x] Changelog entry - [ ] State Gitlab versions affected and issue severity in description - [ ] Create technical debt issue for NotesFinder. - Either split into `NotesFinder::ForTarget` and `NotesFinder::Search` or consider object per notable type such as `NotesFinder::OnIssue`. For the first option could create `NotesFinder::Base` which is either inherited from or which can be included in the other two. - Avoid case statement anti-pattern in this finder with use of `NotesFinder::OnCommit` etc. Consider something on the finder for this? `Model.finder(user, project)` - Move `inc_author` to the controller, and implement `related_notes` to replace `non_diff_notes`/`mr_and_commit_notes` See merge request !2035 --- app/controllers/projects/notes_controller.rb | 2 +- app/finders/notes_finder.rb | 113 ++++++++++-- app/models/merge_request.rb | 4 +- app/models/note.rb | 17 -- .../merge_requests/_merge_request.html.haml | 2 +- app/views/projects/merge_requests/_show.html.haml | 2 +- .../unreleased/jej-note-search-uses-finder.yml | 4 + lib/gitlab/project_search_results.rb | 2 +- lib/gitlab/sql/union.rb | 4 +- spec/controllers/search_controller_spec.rb | 61 +++++++ spec/factories/notes.rb | 2 +- spec/finders/notes_finder_spec.rb | 200 ++++++++++++++++++--- spec/lib/gitlab/project_search_results_spec.rb | 29 +++ spec/lib/gitlab/sql/union_spec.rb | 22 ++- spec/models/merge_request_spec.rb | 6 +- spec/models/note_spec.rb | 38 ---- 16 files changed, 387 insertions(+), 121 deletions(-) create mode 100644 changelogs/unreleased/jej-note-search-uses-finder.yml create mode 100644 spec/controllers/search_controller_spec.rb diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 15ca080c696..b71509f2c9b 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -217,6 +217,6 @@ class Projects::NotesController < Projects::ApplicationController end def find_current_user_notes - @notes = NotesFinder.new.execute(project, current_user, params) + @notes = NotesFinder.new(project, current_user, params).execute.inc_author end end diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index 2484339e3a4..4bd8c83081a 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -1,27 +1,102 @@ class NotesFinder FETCH_OVERLAP = 5.seconds - def execute(project, current_user, params) - target_type = params[:target_type] - target_id = params[:target_id] - # Default to 0 to remain compatible with old clients - last_fetched_at = Time.at(params.fetch(:last_fetched_at, 0).to_i) - - notes = - case target_type - when "commit" - project.notes.for_commit_id(target_id).non_diff_notes - when "issue" - IssuesFinder.new(current_user, project_id: project.id).find(target_id).notes.inc_author - when "merge_request" - MergeRequestsFinder.new(current_user, project_id: project.id).find(target_id).mr_and_commit_notes.inc_author - when "snippet", "project_snippet" - project.snippets.find(target_id).notes + # Used to filter Notes + # When used with target_type and target_id this returns notes specifically for the controller + # + # Arguments: + # current_user - which user check authorizations with + # project - which project to look for notes on + # params: + # target_type: string + # target_id: integer + # last_fetched_at: time + # search: string + # + def initialize(project, current_user, params = {}) + @project = project + @current_user = current_user + @params = params + init_collection + end + + def execute + @notes = since_fetch_at(@params[:last_fetched_at]) if @params[:last_fetched_at] + @notes + end + + private + + def init_collection + if @params[:target_id] + @notes = on_target(@params[:target_type], @params[:target_id]) + else + @notes = notes_of_any_type + end + end + + def notes_of_any_type + types = %w(commit issue merge_request snippet) + note_relations = types.map { |t| notes_for_type(t) } + note_relations.map!{ |notes| search(@params[:search], notes) } if @params[:search] + UnionFinder.new.find_union(note_relations, Note) + end + + def noteables_for_type(noteable_type) + case noteable_type + when "issue" + IssuesFinder.new(@current_user, project_id: @project.id).execute + when "merge_request" + MergeRequestsFinder.new(@current_user, project_id: @project.id).execute + when "snippet", "project_snippet" + SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project) + else + raise 'invalid target_type' + end + end + + def notes_for_type(noteable_type) + if noteable_type == "commit" + if Ability.allowed?(@current_user, :download_code, @project) + @project.notes.where(noteable_type: 'Commit') + else + Note.none + end + else + finder = noteables_for_type(noteable_type) + @project.notes.where(noteable_type: finder.base_class.name, noteable_id: finder.reorder(nil)) + end + end + + def on_target(target_type, target_id) + if target_type == "commit" + notes_for_type('commit').for_commit_id(target_id) + else + target = noteables_for_type(target_type).find(target_id) + + if target.respond_to?(:related_notes) + target.related_notes else - raise 'invalid target_type' + target.notes end + end + end + + # Searches for notes matching the given query. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + def search(query, notes_relation = @notes) + pattern = "%#{query}%" + notes_relation.where(Note.arel_table[:note].matches(pattern)) + end + + # Notes changed since last fetch + # Uses overlapping intervals to avoid worrying about race conditions + def since_fetch_at(fetch_time) + # Default to 0 to remain compatible with old clients + last_fetched_at = Time.at(@params.fetch(:last_fetched_at, 0).to_i) - # Use overlapping intervals to avoid worrying about race conditions - notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh + @notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index ea3cf1cdaac..b73d7acefea 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -452,7 +452,7 @@ class MergeRequest < ActiveRecord::Base should_remove_source_branch? || force_remove_source_branch? end - def mr_and_commit_notes + def related_notes # Fetch comments only from last 100 commits commits_for_notes_limit = 100 commit_ids = commits.last(commits_for_notes_limit).map(&:id) @@ -468,7 +468,7 @@ class MergeRequest < ActiveRecord::Base end def discussions - @discussions ||= self.mr_and_commit_notes. + @discussions ||= self.related_notes. inc_relations_for_view. fresh. discussions diff --git a/app/models/note.rb b/app/models/note.rb index 08bd08743ef..0c1b05dabf2 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -107,23 +107,6 @@ class Note < ActiveRecord::Base Discussion.for_diff_notes(active_notes). map { |d| [d.line_code, d] }.to_h end - - # Searches for notes matching the given query. - # - # This method uses ILIKE on PostgreSQL and LIKE on MySQL. - # - # query - The search query as a String. - # as_user - Limit results to those viewable by a specific user - # - # Returns an ActiveRecord::Relation. - def search(query, as_user: nil) - table = arel_table - pattern = "%#{query}%" - - Note.joins('LEFT JOIN issues ON issues.id = noteable_id'). - where(table[:note].matches(pattern)). - merge(Issue.visible_to_user(as_user)) - end end def cross_reference? diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index fa189ae62d8..959c796ecec 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -39,7 +39,7 @@ = icon('thumbs-down') = downvotes - - note_count = merge_request.mr_and_commit_notes.user.count + - note_count = merge_request.related_notes.user.count %li = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do = icon('comments') diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 896f10104fa..5febe806bdd 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -53,7 +53,7 @@ %li.notes-tab = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do Discussion - %span.badge= @merge_request.mr_and_commit_notes.user.count + %span.badge= @merge_request.related_notes.user.count - if @merge_request.source_project %li.commits-tab = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do diff --git a/changelogs/unreleased/jej-note-search-uses-finder.yml b/changelogs/unreleased/jej-note-search-uses-finder.yml new file mode 100644 index 00000000000..1768bdfd487 --- /dev/null +++ b/changelogs/unreleased/jej-note-search-uses-finder.yml @@ -0,0 +1,4 @@ +--- +title: Fix missing Note access checks by moving Note#search to updated NoteFinder +merge_request: +author: diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 66e6b29e798..6bdf3db9cb8 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -110,7 +110,7 @@ module Gitlab end def notes - @notes ||= project.notes.user.search(query, as_user: @current_user).order('updated_at DESC') + @notes ||= NotesFinder.new(project, @current_user, search: query).execute.user.order('updated_at DESC') end def commits diff --git a/lib/gitlab/sql/union.rb b/lib/gitlab/sql/union.rb index 1cd89b3a9c4..222021e8802 100644 --- a/lib/gitlab/sql/union.rb +++ b/lib/gitlab/sql/union.rb @@ -22,9 +22,7 @@ module Gitlab # By using "unprepared_statements" we remove the usage of placeholders # (thus fixing this problem), at a slight performance cost. fragments = ActiveRecord::Base.connection.unprepared_statement do - @relations.map do |rel| - rel.reorder(nil).to_sql - end + @relations.map { |rel| rel.reorder(nil).to_sql }.reject(&:blank?) end fragments.join("\nUNION\n") diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb new file mode 100644 index 00000000000..b7bb9290712 --- /dev/null +++ b/spec/controllers/search_controller_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe SearchController do + let(:user) { create(:user) } + let(:project) { create(:empty_project, :public) } + + before do + sign_in(user) + end + + it 'finds issue comments' do + project = create(:empty_project, :public) + note = create(:note_on_issue, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].first).to eq note + end + + context 'on restricted projects' do + context 'when signed out' do + before { sign_out(user) } + + it "doesn't expose comments on issues" do + project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_issue, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].count).to eq(0) + end + end + + it "doesn't expose comments on issues" do + project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_issue, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].count).to eq(0) + end + + it "doesn't expose comments on merge_requests" do + project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_merge_request, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].count).to eq(0) + end + + it "doesn't expose comments on snippets" do + project = create(:empty_project, :public, snippets_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_project_snippet, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].count).to eq(0) + end + end +end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 6919002dedc..a10ba629760 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -67,7 +67,7 @@ FactoryGirl.define do end trait :on_project_snippet do - noteable { create(:snippet, project: project) } + noteable { create(:project_snippet, project: project) } end trait :system do diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index 7c6860372cc..4d21254323c 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -2,59 +2,203 @@ require 'spec_helper' describe NotesFinder do let(:user) { create :user } - let(:project) { create :project } - let(:note1) { create :note_on_commit, project: project } - let(:note2) { create :note_on_commit, project: project } - let(:commit) { note1.noteable } + let(:project) { create(:empty_project) } before do project.team << [user, :master] end describe '#execute' do - let(:params) { { target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } } + it 'finds notes on snippets when project is public and user isnt a member' - before do - note1 - note2 + it 'finds notes on merge requests' do + create(:note_on_merge_request, project: project) + + notes = described_class.new(project, user).execute + + expect(notes.count).to eq(1) end - it 'finds all notes' do - notes = NotesFinder.new.execute(project, user, params) - expect(notes.size).to eq(2) + it 'finds notes on snippets' do + create(:note_on_project_snippet, project: project) + + notes = described_class.new(project, user).execute + + expect(notes.count).to eq(1) end - it 'raises an exception for an invalid target_type' do - params.merge!(target_type: 'invalid') - expect { NotesFinder.new.execute(project, user, params) }.to raise_error('invalid target_type') + it "excludes notes on commits the author can't download" do + project = create(:project, :private) + note = create(:note_on_commit, project: project) + params = { target_type: 'commit', target_id: note.noteable.id } + + notes = described_class.new(project, create(:user), params).execute + + expect(notes.count).to eq(0) end - it 'filters out old notes' do - note2.update_attribute(:updated_at, 2.hours.ago) - notes = NotesFinder.new.execute(project, user, params) - expect(notes).to eq([note1]) + it 'succeeds when no notes found' do + notes = described_class.new(project, create(:user)).execute + + expect(notes.count).to eq(0) end - context 'confidential issue notes' do - let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) } - let!(:confidential_note) { create(:note, noteable: confidential_issue, project: confidential_issue.project) } + context 'on restricted projects' do + let(:project) do + create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE, + snippets_access_level: ProjectFeature::PRIVATE, + merge_requests_access_level: ProjectFeature::PRIVATE) + end + + it 'publicly excludes notes on merge requests' do + create(:note_on_merge_request, project: project) + + notes = described_class.new(project, create(:user)).execute + + expect(notes.count).to eq(0) + end + + it 'publicly excludes notes on issues' do + create(:note_on_issue, project: project) + + notes = described_class.new(project, create(:user)).execute + + expect(notes.count).to eq(0) + end + + it 'publicly excludes notes on snippets' do + create(:note_on_project_snippet, project: project) + + notes = described_class.new(project, create(:user)).execute + + expect(notes.count).to eq(0) + end + end + + context 'for target' do + let(:project) { create(:project) } + let(:note1) { create :note_on_commit, project: project } + let(:note2) { create :note_on_commit, project: project } + let(:commit) { note1.noteable } + let(:params) { { target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } } + + before do + note1 + note2 + end + + it 'finds all notes' do + notes = described_class.new(project, user, params).execute + expect(notes.size).to eq(2) + end + + it 'finds notes on merge requests' do + note = create(:note_on_merge_request, project: project) + params = { target_type: 'merge_request', target_id: note.noteable.id } + + notes = described_class.new(project, user, params).execute + + expect(notes).to include(note) + end + + it 'finds notes on snippets' do + note = create(:note_on_project_snippet, project: project) + params = { target_type: 'snippet', target_id: note.noteable.id } - let(:params) { { target_id: confidential_issue.id, target_type: 'issue', last_fetched_at: 1.hour.ago.to_i } } + notes = described_class.new(project, user, params).execute - it 'returns notes if user can see the issue' do - expect(NotesFinder.new.execute(project, user, params)).to eq([confidential_note]) + expect(notes.count).to eq(1) end - it 'raises an error if user can not see the issue' do + it 'raises an exception for an invalid target_type' do + params.merge!(target_type: 'invalid') + expect { described_class.new(project, user, params).execute }.to raise_error('invalid target_type') + end + + it 'filters out old notes' do + note2.update_attribute(:updated_at, 2.hours.ago) + notes = described_class.new(project, user, params).execute + expect(notes).to eq([note1]) + end + + context 'confidential issue notes' do + let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) } + let!(:confidential_note) { create(:note, noteable: confidential_issue, project: confidential_issue.project) } + + let(:params) { { target_id: confidential_issue.id, target_type: 'issue', last_fetched_at: 1.hour.ago.to_i } } + + it 'returns notes if user can see the issue' do + expect(described_class.new(project, user, params).execute).to eq([confidential_note]) + end + + it 'raises an error if user can not see the issue' do + user = create(:user) + expect { described_class.new(project, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises an error for project members with guest role' do + user = create(:user) + project.team << [user, :guest] + + expect { described_class.new(project, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end + end + + describe '.search' do + let(:project) { create(:empty_project, :public) } + let(:note) { create(:note_on_issue, note: 'WoW', project: project) } + + it 'returns notes with matching content' do + expect(described_class.new(note.project, nil, search: note.note).execute).to eq([note]) + end + + it 'returns notes with matching content regardless of the casing' do + expect(described_class.new(note.project, nil, search: 'WOW').execute).to eq([note]) + end + + it 'returns commit notes user can access' do + note = create(:note_on_commit, project: project) + + expect(described_class.new(note.project, create(:user), search: note.note).execute).to eq([note]) + end + + context "confidential issues" do + let(:user) { create(:user) } + let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) } + let(:confidential_note) { create(:note, note: "Random", noteable: confidential_issue, project: confidential_issue.project) } + + it "returns notes with matching content if user can see the issue" do + expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to eq([confidential_note]) + end + + it "does not return notes with matching content if user can not see the issue" do user = create(:user) - expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound) + expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to be_empty end - it 'raises an error for project members with guest role' do + it "does not return notes with matching content for project members with guest role" do user = create(:user) project.team << [user, :guest] + expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to be_empty + end + + it "does not return notes with matching content for unauthenticated users" do + expect(described_class.new(confidential_note.project, nil, search: confidential_note.note).execute).to be_empty + end + end + + context 'inlines SQL filters on subqueries for performance' do + let(:sql) { described_class.new(note.project, nil, search: note.note).execute.to_sql } + let(:number_of_noteable_types) { 4 } + + specify 'project_id check' do + expect(sql.scan(/project_id/).count).to be >= (number_of_noteable_types + 2) + end - expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound) + specify 'search filter' do + expect(sql.scan(/LIKE/).count).to be >= number_of_noteable_types end end end diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 3cd9863ec6a..14ee386dba6 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -149,4 +149,33 @@ describe Gitlab::ProjectSearchResults, lib: true do expect(results.issues_count).to eq 3 end end + + describe 'notes search' do + it 'lists notes' do + project = create(:empty_project, :public) + note = create(:note, project: project) + + results = described_class.new(user, project, note.note) + + expect(results.objects('notes')).to include note + end + + it "doesn't list issue notes when access is restricted" do + project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_issue, project: project) + + results = described_class.new(user, project, note.note) + + expect(results.objects('notes')).not_to include note + end + + it "doesn't list merge_request notes when access is restricted" do + project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_merge_request, project: project) + + results = described_class.new(user, project, note.note) + + expect(results.objects('notes')).not_to include note + end + end end diff --git a/spec/lib/gitlab/sql/union_spec.rb b/spec/lib/gitlab/sql/union_spec.rb index 0cdbab87544..849edb09476 100644 --- a/spec/lib/gitlab/sql/union_spec.rb +++ b/spec/lib/gitlab/sql/union_spec.rb @@ -1,16 +1,26 @@ require 'spec_helper' describe Gitlab::SQL::Union, lib: true do + let(:relation_1) { User.where(email: 'alice@example.com').select(:id) } + let(:relation_2) { User.where(email: 'bob@example.com').select(:id) } + + def to_sql(relation) + relation.reorder(nil).to_sql + end + describe '#to_sql' do it 'returns a String joining relations together using a UNION' do - rel1 = User.where(email: 'alice@example.com') - rel2 = User.where(email: 'bob@example.com') - union = described_class.new([rel1, rel2]) + union = described_class.new([relation_1, relation_2]) + + expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}") + end - sql1 = rel1.reorder(nil).to_sql - sql2 = rel2.reorder(nil).to_sql + it 'skips Model.none segements' do + empty_relation = User.none + union = described_class.new([empty_relation, relation_1, relation_2]) - expect(union.to_sql).to eq("#{sql1}\nUNION\n#{sql2}") + expect{User.where("users.id IN (#{union.to_sql})").to_a}.not_to raise_error + expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}") end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 8b730be91fd..1b71d00eb8f 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -205,7 +205,7 @@ describe MergeRequest, models: true do end end - describe "#mr_and_commit_notes" do + describe "#related_notes" do let!(:merge_request) { create(:merge_request) } before do @@ -217,7 +217,7 @@ describe MergeRequest, models: true do it "includes notes for commits" do expect(merge_request.commits).not_to be_empty - expect(merge_request.mr_and_commit_notes.count).to eq(2) + expect(merge_request.related_notes.count).to eq(2) end it "includes notes for commits from target project as well" do @@ -225,7 +225,7 @@ describe MergeRequest, models: true do project: merge_request.target_project) expect(merge_request.commits).not_to be_empty - expect(merge_request.mr_and_commit_notes.count).to eq(3) + expect(merge_request.related_notes.count).to eq(3) end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 17a15b12dcb..310fecd8a5c 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -162,44 +162,6 @@ describe Note, models: true do end end - describe '.search' do - let(:note) { create(:note_on_issue, note: 'WoW') } - - it 'returns notes with matching content' do - expect(described_class.search(note.note)).to eq([note]) - end - - it 'returns notes with matching content regardless of the casing' do - expect(described_class.search('WOW')).to eq([note]) - end - - context "confidential issues" do - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) } - let(:confidential_note) { create(:note, note: "Random", noteable: confidential_issue, project: confidential_issue.project) } - - it "returns notes with matching content if user can see the issue" do - expect(described_class.search(confidential_note.note, as_user: user)).to eq([confidential_note]) - end - - it "does not return notes with matching content if user can not see the issue" do - user = create(:user) - expect(described_class.search(confidential_note.note, as_user: user)).to be_empty - end - - it "does not return notes with matching content for project members with guest role" do - user = create(:user) - project.team << [user, :guest] - expect(described_class.search(confidential_note.note, as_user: user)).to be_empty - end - - it "does not return notes with matching content for unauthenticated users" do - expect(described_class.search(confidential_note.note)).to be_empty - end - end - end - describe "editable?" do it "returns true" do note = build(:note) -- cgit v1.2.1 From 4bf61b8bd4b04eace6d0f205573f15fc9d981682 Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Mon, 12 Dec 2016 08:43:56 +0000 Subject: Merge branch 'jej-24637-move-issue-visible_to_user-to-finder' into 'security' Issue#visible_to_user moved to IssuesFinder Fixes https://gitlab.com/gitlab-org/gitlab-ce/issues/24637. See merge request !2039 --- app/finders/issues_finder.rb | 18 +++++- app/models/concerns/milestoneish.rb | 2 +- app/models/issue.rb | 55 ----------------- ...-24637-move-issue-visible_to_user-to-finder.yml | 4 ++ features/steps/group/milestones.rb | 2 + features/steps/shared/authentication.rb | 2 +- spec/finders/issues_finder_spec.rb | 71 ++++++++++++++++++---- spec/models/issue_spec.rb | 20 ------ spec/models/milestone_spec.rb | 11 ++-- 9 files changed, 90 insertions(+), 95 deletions(-) create mode 100644 changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index be00a219205..707eddd4d29 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -23,10 +23,26 @@ class IssuesFinder < IssuableFinder private def init_collection - Issue.visible_to_user(current_user) + IssuesFinder.not_restricted_by_confidentiality(current_user) end def iid_pattern @iid_pattern ||= %r{\A#{Regexp.escape(Issue.reference_prefix)}(?<iid>\d+)\z} end + + def self.not_restricted_by_confidentiality(user) + return Issue.where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank? + + return Issue.all if user.admin? + + Issue.where(' + issues.confidential IS NULL + OR issues.confidential IS FALSE + OR (issues.confidential = TRUE + AND (issues.author_id = :user_id + OR issues.assignee_id = :user_id + OR issues.project_id IN(:project_ids)))', + user_id: user.id, + project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id)) + end end diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index 875e9834487..4359f1d7b06 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -30,7 +30,7 @@ module Milestoneish end def issues_visible_to_user(user) - issues.visible_to_user(user) + IssuesFinder.new(user).execute.where(id: issues) end def upcoming? diff --git a/app/models/issue.rb b/app/models/issue.rb index 7fe92051037..738c96e4db3 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -60,61 +60,6 @@ class Issue < ActiveRecord::Base attributes end - class << self - private - - # Returns the project that the current scope belongs to if any, nil otherwise. - # - # Examples: - # - my_project.issues.without_due_date.owner_project => my_project - # - Issue.all.owner_project => nil - def owner_project - # No owner if we're not being called from an association - return unless all.respond_to?(:proxy_association) - - owner = all.proxy_association.owner - - # Check if the association is or belongs to a project - if owner.is_a?(Project) - owner - else - begin - owner.association(:project).target - rescue ActiveRecord::AssociationNotFoundError - nil - end - end - end - end - - def self.visible_to_user(user) - return where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank? - return all if user.admin? - - # Check if we are scoped to a specific project's issues - if owner_project - if owner_project.team.member?(user, Gitlab::Access::REPORTER) - # If the project is authorized for the user, they can see all issues in the project - return all - else - # else only non confidential and authored/assigned to them - return where('issues.confidential IS NULL OR issues.confidential IS FALSE - OR issues.author_id = :user_id OR issues.assignee_id = :user_id', - user_id: user.id) - end - end - - where(' - issues.confidential IS NULL - OR issues.confidential IS FALSE - OR (issues.confidential = TRUE - AND (issues.author_id = :user_id - OR issues.assignee_id = :user_id - OR issues.project_id IN(:project_ids)))', - user_id: user.id, - project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id)) - end - def self.reference_prefix '#' end diff --git a/changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml b/changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml new file mode 100644 index 00000000000..db1389e2024 --- /dev/null +++ b/changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml @@ -0,0 +1,4 @@ +--- +title: Issue#visible_to_user moved to IssuesFinder to prevent accidental use +merge_request: +author: diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb index f5fddab357d..c1d1eca9116 100644 --- a/features/steps/group/milestones.rb +++ b/features/steps/group/milestones.rb @@ -131,5 +131,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps issue.labels << project.labels.find_by(title: 'bug') issue.labels << project.labels.find_by(title: 'feature') end + + current_user.refresh_authorized_projects end end diff --git a/features/steps/shared/authentication.rb b/features/steps/shared/authentication.rb index 735e0ef6108..5c3e724746b 100644 --- a/features/steps/shared/authentication.rb +++ b/features/steps/shared/authentication.rb @@ -33,6 +33,6 @@ module SharedAuthentication end def current_user - @user || User.first + @user || User.reorder(nil).first end end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 7f69e888f32..97737d7ddc7 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -10,24 +10,24 @@ describe IssuesFinder do let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone, title: 'gitlab') } let(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'gitlab') } let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2) } - let(:closed_issue) { create(:issue, author: user2, assignee: user2, project: project2, state: 'closed') } - let!(:label_link) { create(:label_link, label: label, target: issue2) } - - before do - project1.team << [user, :master] - project2.team << [user, :developer] - project2.team << [user2, :developer] - - issue1 - issue2 - issue3 - end describe '#execute' do + let(:closed_issue) { create(:issue, author: user2, assignee: user2, project: project2, state: 'closed') } + let!(:label_link) { create(:label_link, label: label, target: issue2) } let(:search_user) { user } let(:params) { {} } let(:issues) { IssuesFinder.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute } + before do + project1.team << [user, :master] + project2.team << [user, :developer] + project2.team << [user2, :developer] + + issue1 + issue2 + issue3 + end + context 'scope: all' do let(:scope) { 'all' } @@ -193,6 +193,15 @@ describe IssuesFinder do expect(issues).to contain_exactly(issue2, issue3) end end + + it 'finds issues user can access due to group' do + group = create(:group) + project = create(:empty_project, group: group) + issue = create(:issue, project: project) + group.add_user(user, :owner) + + expect(issues).to include(issue) + end end context 'personal scope' do @@ -210,5 +219,43 @@ describe IssuesFinder do end end end + + context 'when project restricts issues' do + let(:scope) { nil } + + it "doesn't return team-only issues to non team members" do + project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + issue = create(:issue, project: project) + + expect(issues).not_to include(issue) + end + + it "doesn't return issues if feature disabled" do + [project1, project2].each do |project| + project.project_feature.update!(issues_access_level: ProjectFeature::DISABLED) + end + + expect(issues.count).to eq 0 + end + end + end + + describe '.not_restricted_by_confidentiality' do + let(:authorized_user) { create(:user) } + let(:project) { create(:empty_project, namespace: authorized_user.namespace) } + let!(:public_issue) { create(:issue, project: project) } + let!(:confidential_issue) { create(:issue, project: project, confidential: true) } + + it 'returns non confidential issues for nil user' do + expect(IssuesFinder.send(:not_restricted_by_confidentiality, nil)).to include(public_issue) + end + + it 'returns non confidential issues for user not authorized for the issues projects' do + expect(IssuesFinder.send(:not_restricted_by_confidentiality, user)).to include(public_issue) + end + + it 'returns all issues for user authorized for the issues projects' do + expect(IssuesFinder.send(:not_restricted_by_confidentiality, authorized_user)).to include(public_issue, confidential_issue) + end end end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 24e216329a9..bb56e44db29 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -22,26 +22,6 @@ describe Issue, models: true do it { is_expected.to have_db_index(:deleted_at) } end - describe '.visible_to_user' do - let(:user) { create(:user) } - let(:authorized_user) { create(:user) } - let(:project) { create(:project, namespace: authorized_user.namespace) } - let!(:public_issue) { create(:issue, project: project) } - let!(:confidential_issue) { create(:issue, project: project, confidential: true) } - - it 'returns non confidential issues for nil user' do - expect(Issue.visible_to_user(nil).count).to be(1) - end - - it 'returns non confidential issues for user not authorized for the issues projects' do - expect(Issue.visible_to_user(user).count).to be(1) - end - - it 'returns all issues for user authorized for the issues projects' do - expect(Issue.visible_to_user(authorized_user).count).to be(2) - end - end - describe '#to_reference' do let(:project) { build(:empty_project, name: 'sample-project') } let(:issue) { build(:issue, iid: 1, project: project) } diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 0cc2efae5f9..064f29d2d66 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -24,8 +24,9 @@ describe Milestone, models: true do it { is_expected.to have_many(:issues) } end - let(:milestone) { create(:milestone) } - let(:issue) { create(:issue) } + let(:project) { create(:project, :public) } + let(:milestone) { create(:milestone, project: project) } + let(:issue) { create(:issue, project: project) } let(:user) { create(:user) } describe "#title" do @@ -110,8 +111,8 @@ describe Milestone, models: true do describe :items_count do before do - milestone.issues << create(:issue) - milestone.issues << create(:closed_issue) + milestone.issues << create(:issue, project: project) + milestone.issues << create(:closed_issue, project: project) milestone.merge_requests << create(:merge_request) end @@ -126,7 +127,7 @@ describe Milestone, models: true do describe '#total_items_count' do before do - create :closed_issue, milestone: milestone + create :closed_issue, milestone: milestone, project: project create :merge_request, milestone: milestone end -- cgit v1.2.1 From ada8b026ef55733a94821525249ed67a094d5521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@gitlab.com> Date: Fri, 9 Dec 2016 16:31:14 +0000 Subject: Merge branch 'rs-filter-params' into 'security' Filter `incoming_email_token` and `runners_token` parameters Closes https://dev.gitlab.org/gitlab/gitlabhq/issues/2676 See merge request !2045 --- config/application.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/application.rb b/config/application.rb index fbf50df2850..782a7a36895 100644 --- a/config/application.rb +++ b/config/application.rb @@ -45,7 +45,7 @@ module Gitlab # # Parameters filtered: # - Password (:password, :password_confirmation) - # - Private tokens (:private_token, :authentication_token) + # - Private tokens # - Two-factor tokens (:otp_attempt) # - Repo/Project Import URLs (:import_url) # - Build variables (:variables) @@ -60,11 +60,13 @@ module Gitlab encrypted_key hook import_url + incoming_email_token key otp_attempt password password_confirmation private_token + runners_token secret_token sentry_dsn variables -- cgit v1.2.1 From e63f796ae4c89f3e51eb0e9977add0fe408c1b9e Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Thu, 15 Dec 2016 08:57:48 -0600 Subject: Unify margin widths --- app/assets/stylesheets/pages/editor.scss | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index fea9a6b5009..38d5df9e106 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -134,15 +134,17 @@ .new-file-name { max-width: none; width: 100%; + margin-bottom: 3px; } .file-buttons { display: block; width: 100%; + margin-bottom: 10px; .soft-wrap-toggle { width: 100%; - margin: 7px 0; + margin: 3px 0; } .encoding-selector, @@ -150,15 +152,12 @@ .gitignore-selector, .gitlab-ci-yml-selector { display: block; - margin: 7px 0; + margin: 3px 0; button { width: 100%; } } - } - } - } -- cgit v1.2.1 From 1356e40f22c555f676777ed9385a12b09c19fdce Mon Sep 17 00:00:00 2001 From: Luke Bennett <lukeeeebennettplus@gmail.com> Date: Thu, 13 Oct 2016 04:33:09 +0100 Subject: Changed autocomplete_sources into an action that returns a single 'at' type of sources at a time Finished up autocomplete_sources action and added frontend to fetch data only when its needed Added wait_for_ajax to specs Fixed builds and improved the setup/destroy lifecycle Changed global namespace and DRYed up loading logic Added safety for accidentally loading data twice Removed destroy as its not necessary and is messing with click events from a blur race condition Created AutocompleteSourcesController and updated routes Fixed @undefined from tabbing before load ends Disable tabSelectsMatch until we have loaded data Review changes --- app/assets/javascripts/gfm_auto_complete.js.es6 | 233 ++++++++++----------- app/assets/javascripts/gl_form.js | 2 +- app/assets/javascripts/issuable_form.js | 2 +- .../projects/autocomplete_sources_controller.rb | 48 +++++ app/controllers/projects_controller.rb | 33 --- app/views/layouts/_init_auto_complete.html.haml | 14 +- .../18435-autocomplete-is-not-performant.yml | 4 + config/routes/project.rb | 13 +- spec/features/participants_autocomplete_spec.rb | 7 +- .../projects/gfm_autocomplete_load_spec.rb | 4 +- spec/routing/project_routing_spec.rb | 19 +- 11 files changed, 210 insertions(+), 169 deletions(-) create mode 100644 app/controllers/projects/autocomplete_sources_controller.rb create mode 100644 changelogs/unreleased/18435-autocomplete-is-not-performant.yml diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 245383438d1..dda061a556b 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -2,19 +2,28 @@ // Creates the variables for setting up GFM auto-completion (function() { - if (window.GitLab == null) { - window.GitLab = {}; + if (window.gl == null) { + window.gl = {}; } function sanitize(str) { return str.replace(/<(?:.|\n)*?>/gm, ''); } - window.GitLab.GfmAutoComplete = { - dataLoading: false, - dataLoaded: false, + window.gl.GfmAutoComplete = { + dataSources: {}, + defaultLoadingData: ['loading'], cachedData: {}, - dataSource: '', + isLoadingData: {}, + atTypeMap: { + ':': 'emojis', + '@': 'members', + '#': 'issues', + '!': 'mergeRequests', + '~': 'labels', + '%': 'milestones', + '/': 'commands' + }, // Emoji Emoji: { template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>' @@ -35,33 +44,31 @@ template: '<li>${title}</li>' }, Loading: { - template: '<li><i class="fa fa-refresh fa-spin"></i> Loading...</li>' + template: '<li style="pointer-events: none;"><i class="fa fa-refresh fa-spin"></i> Loading...</li>' }, DefaultOptions: { sorter: function(query, items, searchKey) { - // Highlight first item only if at least one char was typed - this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0; - if ((items[0].name != null) && items[0].name === 'loading') { + if (gl.GfmAutoComplete.isLoading(items)) { return items; } return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey); }, filter: function(query, data, searchKey) { - if (data[0] === 'loading') { + if (gl.GfmAutoComplete.isLoading(data)) { + gl.GfmAutoComplete.togglePreventSelection.call(this, true); + gl.GfmAutoComplete.fetchData(this.$inputor, this.at); return data; + } else { + gl.GfmAutoComplete.togglePreventSelection.call(this, false); + return $.fn.atwho["default"].callbacks.filter(query, data, searchKey); } - return $.fn.atwho["default"].callbacks.filter(query, data, searchKey); }, beforeInsert: function(value) { if (value && !this.setting.skipSpecialCharacterTest) { var withoutAt = value.substring(1); if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"'; } - if (!window.GitLab.GfmAutoComplete.dataLoaded) { - return this.at; - } else { - return value; - } + return value; }, matcher: function (flag, subtext) { // The below is taken from At.js source @@ -85,69 +92,46 @@ } } }, - setup: _.debounce(function(input) { + setup: function(input) { // Add GFM auto-completion to all input fields, that accept GFM input. this.input = input || $('.js-gfm-input'); - // destroy previous instances - this.destroyAtWho(); - // set up instances - this.setupAtWho(); - - if (this.dataSource && !this.dataLoading && !this.cachedData) { - this.dataLoading = true; - return this.fetchData(this.dataSource) - .done((data) => { - this.dataLoading = false; - this.loadData(data); - }); - }; - - if (this.cachedData != null) { - return this.loadData(this.cachedData); - } - }, 1000), - setupAtWho: function() { + this.setupLifecycle(); + }, + setupLifecycle() { + this.input.each((i, input) => { + const $input = $(input); + $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); + }); + }, + setupAtWho: function($input) { // Emoji - this.input.atwho({ + $input.atwho({ at: ':', - displayTpl: (function(_this) { - return function(value) { - if (value.path != null) { - return _this.Emoji.template; - } else { - return _this.Loading.template; - } - }; - })(this), + displayTpl: function(value) { + return value.path != null ? this.Emoji.template : this.Loading.template; + }.bind(this), insertTpl: ':${name}:', - data: ['loading'], startWithSpace: false, skipSpecialCharacterTest: true, + data: this.defaultLoadingData, callbacks: { sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, beforeInsert: this.DefaultOptions.beforeInsert, - matcher: this.DefaultOptions.matcher + filter: this.DefaultOptions.filter } }); // Team Members - this.input.atwho({ + $input.atwho({ at: '@', - displayTpl: (function(_this) { - return function(value) { - if (value.username != null) { - return _this.Members.template; - } else { - return _this.Loading.template; - } - }; - })(this), + displayTpl: function(value) { + return value.username != null ? this.Members.template : this.Loading.template; + }.bind(this), insertTpl: '${atwho-at}${username}', searchKey: 'search', - data: ['loading'], startWithSpace: false, alwaysHighlightFirst: true, skipSpecialCharacterTest: true, + data: this.defaultLoadingData, callbacks: { sorter: this.DefaultOptions.sorter, filter: this.DefaultOptions.filter, @@ -178,20 +162,14 @@ } } }); - this.input.atwho({ + $input.atwho({ at: '#', alias: 'issues', searchKey: 'search', - displayTpl: (function(_this) { - return function(value) { - if (value.title != null) { - return _this.Issues.template; - } else { - return _this.Loading.template; - } - }; - })(this), - data: ['loading'], + displayTpl: function(value) { + return value.title != null ? this.Issues.template : this.Loading.template; + }.bind(this), + data: this.defaultLoadingData, insertTpl: '${atwho-at}${id}', startWithSpace: false, callbacks: { @@ -213,26 +191,21 @@ } } }); - this.input.atwho({ + $input.atwho({ at: '%', alias: 'milestones', searchKey: 'search', - displayTpl: (function(_this) { - return function(value) { - if (value.title != null) { - return _this.Milestones.template; - } else { - return _this.Loading.template; - } - }; - })(this), insertTpl: '${atwho-at}${title}', - data: ['loading'], + displayTpl: function(value) { + return value.title != null ? this.Milestones.template : this.Loading.template; + }.bind(this), startWithSpace: false, + data: this.defaultLoadingData, callbacks: { matcher: this.DefaultOptions.matcher, sorter: this.DefaultOptions.sorter, beforeInsert: this.DefaultOptions.beforeInsert, + filter: this.DefaultOptions.filter, beforeSave: function(milestones) { return $.map(milestones, function(m) { if (m.title == null) { @@ -247,21 +220,15 @@ } } }); - this.input.atwho({ + $input.atwho({ at: '!', alias: 'mergerequests', searchKey: 'search', - displayTpl: (function(_this) { - return function(value) { - if (value.title != null) { - return _this.Issues.template; - } else { - return _this.Loading.template; - } - }; - })(this), - data: ['loading'], startWithSpace: false, + displayTpl: function(value) { + return value.title != null ? this.Issues.template : this.Loading.template; + }.bind(this), + data: this.defaultLoadingData, insertTpl: '${atwho-at}${id}', callbacks: { sorter: this.DefaultOptions.sorter, @@ -282,18 +249,31 @@ } } }); - this.input.atwho({ + $input.atwho({ at: '~', alias: 'labels', searchKey: 'search', - displayTpl: this.Labels.template, + data: this.defaultLoadingData, + displayTpl: function(value) { + return this.isLoading(value) ? this.Loading.template : this.Labels.template; + }.bind(this), insertTpl: '${atwho-at}${title}', startWithSpace: false, callbacks: { matcher: this.DefaultOptions.matcher, sorter: this.DefaultOptions.sorter, beforeInsert: this.DefaultOptions.beforeInsert, + filter: this.DefaultOptions.filter, beforeSave: function(merges) { + if (gl.GfmAutoComplete.isLoading(merges)) return merges; + var sanitizeLabelTitle; + sanitizeLabelTitle = function(title) { + if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) { + return "\"" + (sanitize(title)) + "\""; + } else { + return sanitize(title); + } + }; return $.map(merges, function(m) { return { title: sanitize(m.title), @@ -305,12 +285,14 @@ } }); // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms - this.input.filter('[data-supports-slash-commands="true"]').atwho({ + $input.filter('[data-supports-slash-commands="true"]').atwho({ at: '/', alias: 'commands', searchKey: 'search', skipSpecialCharacterTest: true, + data: this.defaultLoadingData, displayTpl: function(value) { + if (this.isLoading(value)) return this.Loading.template; var tpl = '<li>/${name}'; if (value.aliases.length > 0) { tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>'; @@ -323,7 +305,7 @@ } tpl += '</li>'; return _.template(tpl)(value); - }, + }.bind(this), insertTpl: function(value) { var tpl = "/${name} "; var reference_prefix = null; @@ -341,6 +323,7 @@ filter: this.DefaultOptions.filter, beforeInsert: this.DefaultOptions.beforeInsert, beforeSave: function(commands) { + if (gl.GfmAutoComplete.isLoading(commands)) return commands; return $.map(commands, function(c) { var search = c.name; if (c.aliases.length > 0) { @@ -368,32 +351,40 @@ }); return; }, - destroyAtWho: function() { - return this.input.atwho('destroy'); - }, - fetchData: function(dataSource) { - return $.getJSON(dataSource); + fetchData: function($input, at) { + if (this.isLoadingData[at]) return; + this.isLoadingData[at] = true; + if (this.cachedData[at]) { + this.loadData($input, at, this.cachedData[at]); + } else { + $.getJSON(this.dataSources[this.atTypeMap[at]], (data) => { + this.loadData($input, at, data); + }).fail(() => { this.isLoadingData[at] = false; }); + } }, - loadData: function(data) { - this.cachedData = data; - this.dataLoaded = true; - // load members - this.input.atwho('load', '@', data.members); - // load issues - this.input.atwho('load', 'issues', data.issues); - // load milestones - this.input.atwho('load', 'milestones', data.milestones); - // load merge requests - this.input.atwho('load', 'mergerequests', data.mergerequests); - // load emojis - this.input.atwho('load', ':', data.emojis); - // load labels - this.input.atwho('load', '~', data.labels); - // load commands - this.input.atwho('load', '/', data.commands); + loadData: function($input, at, data) { + this.isLoadingData[at] = false; + this.cachedData[at] = data; + $input.atwho('load', at, data); // This trigger at.js again // otherwise we would be stuck with loading until the user types - return $(':focus').trigger('keyup'); + return $input.trigger('keyup'); + }, + isLoading(data) { + if (!data) return false; + if (Array.isArray(data)) data = data[0]; + return data === this.defaultLoadingData[0] || data.name === this.defaultLoadingData[0]; + }, + togglePreventSelection(isPrevented = !!this.setting.tabSelectsMatch) { + this.setting.tabSelectsMatch = !isPrevented; + this.setting.spaceSelectsMatch = !isPrevented; + const eventListenerAction = `${isPrevented ? 'add' : 'remove'}EventListener`; + this.$inputor[0][eventListenerAction]('keydown', gl.GfmAutoComplete.preventSpaceTabEnter); + }, + preventSpaceTabEnter(e) { + const key = e.which || e.keyCode; + const preventables = [9, 13, 32]; + if (preventables.indexOf(key) > -1) e.preventDefault(); } }; diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index 56a33eeaad5..7dc2d13e5d8 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -30,7 +30,7 @@ this.form.addClass('gfm-form'); // remove notify commit author checkbox for non-commit notes gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button')); - GitLab.GfmAutoComplete.setup(this.form.find('.js-gfm-input')); + gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input')); new DropzoneInput(this.form); autosize(this.textarea); // form and textarea event listeners diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 2f3cad13cc0..1c4086517fe 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -19,7 +19,7 @@ this.renderWipExplanation = bind(this.renderWipExplanation, this); this.resetAutosave = bind(this.resetAutosave, this); this.handleSubmit = bind(this.handleSubmit, this); - GitLab.GfmAutoComplete.setup(); + gl.GfmAutoComplete.setup(); new UsersSelect(); new ZenMode(); this.titleField = this.form.find("input[name*='[title]']"); diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb new file mode 100644 index 00000000000..d9dfa534669 --- /dev/null +++ b/app/controllers/projects/autocomplete_sources_controller.rb @@ -0,0 +1,48 @@ +class Projects::AutocompleteSourcesController < Projects::ApplicationController + before_action :load_autocomplete_service, except: [:emojis, :members] + + def emojis + render json: Gitlab::AwardEmoji.urls + end + + def members + render json: ::Projects::ParticipantsService.new(@project, current_user).execute(noteable) + end + + def issues + render json: @autocomplete_service.issues + end + + def merge_requests + render json: @autocomplete_service.merge_requests + end + + def labels + render json: @autocomplete_service.labels + end + + def milestones + render json: @autocomplete_service.milestones + end + + def commands + render json: @autocomplete_service.commands(noteable, params[:type]) + end + + private + + def load_autocomplete_service + @autocomplete_service = ::Projects::AutocompleteService.new(@project, current_user) + end + + def noteable + case params[:type] + when 'Issue' + IssuesFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id]) + when 'MergeRequest' + MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id]) + when 'Commit' + @project.commit(params[:type_id]) + end + end +end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index a8a18b4fa16..d5ee503c44c 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -127,39 +127,6 @@ class ProjectsController < Projects::ApplicationController redirect_to edit_project_path(@project), alert: ex.message end - def autocomplete_sources - noteable = - case params[:type] - when 'Issue' - IssuesFinder.new(current_user, project_id: @project.id). - execute.find_by(iid: params[:type_id]) - when 'MergeRequest' - MergeRequestsFinder.new(current_user, project_id: @project.id). - execute.find_by(iid: params[:type_id]) - when 'Commit' - @project.commit(params[:type_id]) - else - nil - end - - autocomplete = ::Projects::AutocompleteService.new(@project, current_user) - participants = ::Projects::ParticipantsService.new(@project, current_user).execute(noteable) - - @suggestions = { - emojis: Gitlab::AwardEmoji.urls, - issues: autocomplete.issues, - milestones: autocomplete.milestones, - mergerequests: autocomplete.merge_requests, - labels: autocomplete.labels, - members: participants, - commands: autocomplete.commands(noteable, params[:type]) - } - - respond_to do |format| - format.json { render json: @suggestions } - end - end - def new_issue_address return render_404 unless Gitlab::IncomingEmail.supports_issue_creation? diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml index e138ebab018..3daa1e90a8c 100644 --- a/app/views/layouts/_init_auto_complete.html.haml +++ b/app/views/layouts/_init_auto_complete.html.haml @@ -3,6 +3,14 @@ - if project :javascript - GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: noteable_type, type_id: params[:id])}" - GitLab.GfmAutoComplete.cachedData = undefined; - GitLab.GfmAutoComplete.setup(); + gl.GfmAutoComplete.dataSources = { + emojis: "#{emojis_namespace_project_autocomplete_sources_path(project.namespace, project)}", + members: "#{members_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}", + issues: "#{issues_namespace_project_autocomplete_sources_path(project.namespace, project)}", + mergeRequests: "#{merge_requests_namespace_project_autocomplete_sources_path(project.namespace, project)}", + labels: "#{labels_namespace_project_autocomplete_sources_path(project.namespace, project)}", + milestones: "#{milestones_namespace_project_autocomplete_sources_path(project.namespace, project)}", + commands: "#{commands_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}" + }; + + gl.GfmAutoComplete.setup(); diff --git a/changelogs/unreleased/18435-autocomplete-is-not-performant.yml b/changelogs/unreleased/18435-autocomplete-is-not-performant.yml new file mode 100644 index 00000000000..019c55e27dc --- /dev/null +++ b/changelogs/unreleased/18435-autocomplete-is-not-performant.yml @@ -0,0 +1,4 @@ +--- +title: Made comment autocomplete more performant and removed some loading bugs +merge_request: 6856 +author: diff --git a/config/routes/project.rb b/config/routes/project.rb index 0754f0ec3b0..e17d6bae10c 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -11,6 +11,18 @@ constraints(ProjectUrlConstrainer.new) do module: :projects, as: :project) do + resources :autocomplete_sources, only: [] do + collection do + get 'emojis' + get 'members' + get 'issues' + get 'merge_requests' + get 'labels' + get 'milestones' + get 'commands' + end + end + # # Templates # @@ -316,7 +328,6 @@ constraints(ProjectUrlConstrainer.new) do post :remove_export post :generate_new_export get :download_export - get :autocomplete_sources get :activity get :refs put :new_issue_address diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb index a78a1c9c890..c2545b0c259 100644 --- a/spec/features/participants_autocomplete_spec.rb +++ b/spec/features/participants_autocomplete_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'Member autocomplete', feature: true do + include WaitForAjax + let(:project) { create(:project, :public) } let(:user) { create(:user) } let(:participant) { create(:user) } @@ -79,11 +81,10 @@ feature 'Member autocomplete', feature: true do end def open_member_suggestions - sleep 1 page.within('.new-note') do - sleep 1 - find('#note_note').native.send_keys('@') + find('#note_note').send_keys('@') end + wait_for_ajax end def visit_issue(project, issue) diff --git a/spec/features/projects/gfm_autocomplete_load_spec.rb b/spec/features/projects/gfm_autocomplete_load_spec.rb index 1921ea6d8ae..dd9622f16a0 100644 --- a/spec/features/projects/gfm_autocomplete_load_spec.rb +++ b/spec/features/projects/gfm_autocomplete_load_spec.rb @@ -10,12 +10,12 @@ describe 'GFM autocomplete loading', feature: true, js: true do end it 'does not load on project#show' do - expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).to eq('') + expect(evaluate_script('gl.GfmAutoComplete.dataSources')).to eq({}) end it 'loads on new issue page' do visit new_namespace_project_issue_path(project.namespace, project) - expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).not_to eq('') + expect(evaluate_script('gl.GfmAutoComplete.dataSources')).not_to eq({}) end end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index b6e7da841b1..77549db2927 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -80,10 +80,6 @@ describe 'project routing' do expect(get('/gitlab/gitlabhq/edit')).to route_to('projects#edit', namespace_id: 'gitlab', id: 'gitlabhq') end - it 'to #autocomplete_sources' do - expect(get('/gitlab/gitlabhq/autocomplete_sources')).to route_to('projects#autocomplete_sources', namespace_id: 'gitlab', id: 'gitlabhq') - end - describe 'to #show' do context 'regular name' do it { expect(get('/gitlab/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq') } @@ -117,6 +113,21 @@ describe 'project routing' do end end + # emojis_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/emojis(.:format) projects/autocomplete_sources#emojis + # members_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/members(.:format) projects/autocomplete_sources#members + # issues_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/issues(.:format) projects/autocomplete_sources#issues + # merge_requests_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/merge_requests(.:format) projects/autocomplete_sources#merge_requests + # labels_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/labels(.:format) projects/autocomplete_sources#labels + # milestones_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/milestones(.:format) projects/autocomplete_sources#milestones + # commands_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/commands(.:format) projects/autocomplete_sources#commands + describe Projects::AutocompleteSourcesController, 'routing' do + [:emojis, :members, :issues, :merge_requests, :labels, :milestones, :commands].each do |action| + it "to ##{action}" do + expect(get("/gitlab/gitlabhq/autocomplete_sources/#{action}")).to route_to("projects/autocomplete_sources##{action}", namespace_id: 'gitlab', project_id: 'gitlabhq') + end + end + end + # pages_project_wikis GET /:project_id/wikis/pages(.:format) projects/wikis#pages # history_project_wiki GET /:project_id/wikis/:id/history(.:format) projects/wikis#history # project_wikis POST /:project_id/wikis(.:format) projects/wikis#create -- cgit v1.2.1 From 0a5427d7c2679640c816cdfea1ffc30a3e7b4dd8 Mon Sep 17 00:00:00 2001 From: twonegatives <whitewhiteheaven@gmail.com> Date: Tue, 13 Dec 2016 01:13:14 +0300 Subject: Made Ci::Builds to have same ref as Ci::Pipeline in dev fixtures --- changelogs/unreleased/change_development_build_fixtures.yml | 4 ++++ db/fixtures/development/14_pipelines.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/change_development_build_fixtures.yml diff --git a/changelogs/unreleased/change_development_build_fixtures.yml b/changelogs/unreleased/change_development_build_fixtures.yml new file mode 100644 index 00000000000..b5dc3792745 --- /dev/null +++ b/changelogs/unreleased/change_development_build_fixtures.yml @@ -0,0 +1,4 @@ +--- +title: Ci::Builds have same ref as Ci::Pipeline in dev fixtures +merge_request: +author: twonegatives diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb index 08ad3097d34..19e001854d2 100644 --- a/db/fixtures/development/14_pipelines.rb +++ b/db/fixtures/development/14_pipelines.rb @@ -115,7 +115,7 @@ class Gitlab::Seeder::Pipelines def job_attributes(pipeline, opts) { name: 'test build', stage: 'test', stage_idx: stage_index(opts[:stage]), - ref: 'master', tag: false, user: build_user, project: @project, pipeline: pipeline, + ref: pipeline.ref, tag: false, user: build_user, project: @project, pipeline: pipeline, created_at: Time.now, updated_at: Time.now }.merge(opts) end -- cgit v1.2.1 From a7a2cc13555d63f17ee9a423ab8beda9c4d03fdc Mon Sep 17 00:00:00 2001 From: Drew Blessing <drew@gitlab.com> Date: Thu, 15 Dec 2016 10:39:26 -0600 Subject: Add `gitlab_rails['auto_migrate'] = false` to HA docs for Redis/PG [ci skip] In a high availability configuration, the Redis and PostgreSQL nodes should not attempt to run database migrations. In fact, trying will result in errors about not providing a database password. To prevent errors and confusion, add this configurtion to these nodes' `gitlab.rb` file. --- doc/administration/high_availability/database.md | 6 +++--- doc/administration/high_availability/redis.md | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md index b36cf18d459..e4f94eb7cb6 100644 --- a/doc/administration/high_availability/database.md +++ b/doc/administration/high_availability/database.md @@ -44,6 +44,9 @@ If you use a cloud-managed service, or provide your own PostgreSQL: gitlab_rails['db_password'] = 'DB password' postgresql['md5_auth_cidr_addresses'] = ['0.0.0.0/0'] postgresql['listen_address'] = '0.0.0.0' + + # Disable automatic database migrations + gitlab_rails['auto_migrate'] = false ``` 1. Run `sudo gitlab-ctl reconfigure` to install and configure PostgreSQL. @@ -102,9 +105,6 @@ If you use a cloud-managed service, or provide your own PostgreSQL: 1. Exit the database prompt by typing `\q` and Enter. 1. Exit the `gitlab-psql` user by running `exit` twice. 1. Run `sudo gitlab-ctl reconfigure` a final time. -1. Run `sudo touch /etc/gitlab/skip-auto-migrations` to prevent database migrations - from running on upgrade. Only the primary GitLab application server should - handle migrations. --- diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md index f532a106bc6..b4e7bf21e35 100644 --- a/doc/administration/high_availability/redis.md +++ b/doc/administration/high_availability/redis.md @@ -287,14 +287,14 @@ The prerequisites for a HA Redis setup are the following: redis['password'] = 'redis-password-goes-here' ``` -1. To prevent database migrations from running on upgrade, run: +1. Only the primary GitLab application server should handle migrations. To + prevent database migrations from running on upgrade, add the following + configuration to your `/etc/gitlab/gitlab.rb` file: ``` - sudo touch /etc/gitlab/skip-auto-migrations + gitlab_rails['auto_migrate'] = false ``` - Only the primary GitLab application server should handle migrations. - 1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect. ### Step 2. Configuring the slave Redis instances -- cgit v1.2.1 From a5ccaded656fb215f1f8d503b88c8f28bf90ce68 Mon Sep 17 00:00:00 2001 From: Felipe Artur <felipefac@gmail.com> Date: Tue, 6 Dec 2016 15:59:03 -0200 Subject: Change SlackService to SlackNotificationsService --- app/models/project.rb | 4 +- .../project_services/chat_notification_service.rb | 147 +++++++++++++++++++++ app/models/project_services/chat_service.rb | 143 ++------------------ .../mattermost_notification_service.rb | 41 ++++++ app/models/project_services/mattermost_service.rb | 41 ------ .../mattermost_slash_commands_service.rb | 12 +- .../project_services/slack_notification_service.rb | 40 ++++++ app/models/project_services/slack_service.rb | 40 ------ app/models/service.rb | 4 +- ...20141006143943_move_slack_service_to_webhook.rb | 6 +- ..._slack_service_to_slack_notification_service.rb | 14 ++ db/schema.rb | 2 +- lib/api/services.rb | 10 +- .../import_export/test_project_export.tar.gz | Bin 681774 -> 679415 bytes .../projects/services/slack_service_spec.rb | 4 +- spec/lib/gitlab/import_export/all_models.yml | 4 +- .../chat_notification_service_spec.rb | 12 ++ spec/models/project_services/chat_service_spec.rb | 11 +- .../mattermost_notification_service_spec.rb | 5 + .../project_services/mattermost_service_spec.rb | 5 - .../slack_notification_service_spec.rb | 5 + spec/models/project_services/slack_service_spec.rb | 5 - spec/models/project_spec.rb | 4 +- 23 files changed, 307 insertions(+), 252 deletions(-) create mode 100644 app/models/project_services/chat_notification_service.rb create mode 100644 app/models/project_services/mattermost_notification_service.rb delete mode 100644 app/models/project_services/mattermost_service.rb create mode 100644 app/models/project_services/slack_notification_service.rb delete mode 100644 app/models/project_services/slack_service.rb create mode 100644 db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb create mode 100644 spec/models/project_services/chat_notification_service_spec.rb create mode 100644 spec/models/project_services/mattermost_notification_service_spec.rb delete mode 100644 spec/models/project_services/mattermost_service_spec.rb create mode 100644 spec/models/project_services/slack_notification_service_spec.rb delete mode 100644 spec/models/project_services/slack_service_spec.rb diff --git a/app/models/project.rb b/app/models/project.rb index 19c2d24212d..5d092ca42c2 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -95,8 +95,8 @@ class Project < ActiveRecord::Base has_one :asana_service, dependent: :destroy has_one :gemnasium_service, dependent: :destroy has_one :mattermost_slash_commands_service, dependent: :destroy - has_one :mattermost_service, dependent: :destroy - has_one :slack_service, dependent: :destroy + has_one :mattermost_notification_service, dependent: :destroy + has_one :slack_notification_service, dependent: :destroy has_one :buildkite_service, dependent: :destroy has_one :bamboo_service, dependent: :destroy has_one :teamcity_service, dependent: :destroy diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb new file mode 100644 index 00000000000..b0556987721 --- /dev/null +++ b/app/models/project_services/chat_notification_service.rb @@ -0,0 +1,147 @@ +# Base class for Chat notifications services +# This class is not meant to be used directly, but only to inherit from. +class ChatNotificationService < Service + include ChatMessage + + default_value_for :category, 'chat' + + prop_accessor :webhook, :username, :channel + boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines + + validates :webhook, presence: true, url: true, if: :activated? + + def initialize_properties + # Custom serialized properties initialization + self.supported_events.each { |event| self.class.prop_accessor(event_channel_name(event)) } + + if properties.nil? + self.properties = {} + self.notify_only_broken_builds = true + self.notify_only_broken_pipelines = true + end + end + + def can_test? + valid? + end + + def supported_events + %w[push issue confidential_issue merge_request note tag_push + build pipeline wiki_page] + end + + def execute(data) + return unless supported_events.include?(data[:object_kind]) + return unless webhook.present? + + object_kind = data[:object_kind] + + data = data.merge( + project_url: project_url, + project_name: project_name + ) + + # WebHook events often have an 'update' event that follows a 'open' or + # 'close' action. Ignore update events for now to prevent duplicate + # messages from arriving. + + message = get_message(object_kind, data) + + return false unless message + + opt = {} + + opt[:channel] = get_channel_field(object_kind).presence || channel || default_channel + opt[:username] = username if username + notifier = Slack::Notifier.new(webhook, opt) + notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback) + + true + end + + def event_channel_names + supported_events.map { |event| event_channel_name(event) } + end + + def event_field(event) + fields.find { |field| field[:name] == event_channel_name(event) } + end + + def global_fields + fields.reject { |field| field[:name].end_with?('channel') } + end + + def default_channel + raise NotImplementedError + end + + private + + def get_message(object_kind, data) + case object_kind + when "push", "tag_push" + PushMessage.new(data) + when "issue" + IssueMessage.new(data) unless is_update?(data) + when "merge_request" + MergeMessage.new(data) unless is_update?(data) + when "note" + NoteMessage.new(data) + when "build" + BuildMessage.new(data) if should_build_be_notified?(data) + when "pipeline" + PipelineMessage.new(data) if should_pipeline_be_notified?(data) + when "wiki_page" + WikiPageMessage.new(data) + end + end + + def get_channel_field(event) + field_name = event_channel_name(event) + self.public_send(field_name) + end + + def build_event_channels + supported_events.reduce([]) do |channels, event| + channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel } + end + end + + def event_channel_name(event) + "#{event}_channel" + end + + def project_name + project.name_with_namespace.gsub(/\s/, '') + end + + def project_url + project.web_url + end + + def is_update?(data) + data[:object_attributes][:action] == 'update' + end + + def should_build_be_notified?(data) + case data[:commit][:status] + when 'success' + !notify_only_broken_builds? + when 'failed' + true + else + false + end + end + + def should_pipeline_be_notified?(data) + case data[:object_attributes][:status] + when 'success' + !notify_only_broken_pipelines? + when 'failed' + true + else + false + end + end +end diff --git a/app/models/project_services/chat_service.rb b/app/models/project_services/chat_service.rb index 8ac049ba939..574788462de 100644 --- a/app/models/project_services/chat_service.rb +++ b/app/models/project_services/chat_service.rb @@ -1,148 +1,21 @@ # Base class for Chat services -# This class is not meant to be used directly, but only to inherrit from. +# This class is not meant to be used directly, but only to inherit from. class ChatService < Service - include ChatMessage - default_value_for :category, 'chat' - prop_accessor :webhook, :username, :channel - boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines - - validates :webhook, presence: true, url: true, if: :activated? - - def initialize_properties - # Custom serialized properties initialization - self.supported_events.each { |event| self.class.prop_accessor(event_channel_name(event)) } + has_many :chat_names, foreign_key: :service_id - if properties.nil? - self.properties = {} - self.notify_only_broken_builds = true - self.notify_only_broken_pipelines = true - end - end - - def can_test? - valid? + def valid_token?(token) + self.respond_to?(:token) && + self.token.present? && + ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) end def supported_events - %w[push issue confidential_issue merge_request note tag_push - build pipeline wiki_page] + [] end - def execute(data) - return unless supported_events.include?(data[:object_kind]) - return unless webhook.present? - - object_kind = data[:object_kind] - - data = data.merge( - project_url: project_url, - project_name: project_name - ) - - # WebHook events often have an 'update' event that follows a 'open' or - # 'close' action. Ignore update events for now to prevent duplicate - # messages from arriving. - - message = get_message(object_kind, data) - - return false unless message - - opt = {} - - opt[:channel] = get_channel_field(object_kind).presence || channel || default_channel - opt[:username] = username if username - - notifier = Slack::Notifier.new(webhook, opt) - notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback) - - true - end - - def event_channel_names - supported_events.map { |event| event_channel_name(event) } - end - - def event_field(event) - fields.find { |field| field[:name] == event_channel_name(event) } - end - - def global_fields - fields.reject { |field| field[:name].end_with?('channel') } - end - - def default_channel + def trigger(params) raise NotImplementedError end - - private - - def get_message(object_kind, data) - case object_kind - when "push", "tag_push" - PushMessage.new(data) - when "issue" - IssueMessage.new(data) unless is_update?(data) - when "merge_request" - MergeMessage.new(data) unless is_update?(data) - when "note" - NoteMessage.new(data) - when "build" - BuildMessage.new(data) if should_build_be_notified?(data) - when "pipeline" - PipelineMessage.new(data) if should_pipeline_be_notified?(data) - when "wiki_page" - WikiPageMessage.new(data) - end - end - - def get_channel_field(event) - field_name = event_channel_name(event) - self.public_send(field_name) - end - - def build_event_channels - supported_events.reduce([]) do |channels, event| - channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel } - end - end - - def event_channel_name(event) - "#{event}_channel" - end - - def project_name - project.name_with_namespace.gsub(/\s/, '') - end - - def project_url - project.web_url - end - - def is_update?(data) - data[:object_attributes][:action] == 'update' - end - - def should_build_be_notified?(data) - case data[:commit][:status] - when 'success' - !notify_only_broken_builds? - when 'failed' - true - else - false - end - end - - def should_pipeline_be_notified?(data) - case data[:object_attributes][:status] - when 'success' - !notify_only_broken_pipelines? - when 'failed' - true - else - false - end - end end diff --git a/app/models/project_services/mattermost_notification_service.rb b/app/models/project_services/mattermost_notification_service.rb new file mode 100644 index 00000000000..de18c4b1f00 --- /dev/null +++ b/app/models/project_services/mattermost_notification_service.rb @@ -0,0 +1,41 @@ +class MattermostNotificationService < ChatNotificationService + def title + 'Mattermost notifications' + end + + def description + 'Receive event notifications in Mattermost' + end + + def to_param + 'mattermost_notification' + end + + def help + 'This service sends notifications about projects events to Mattermost channels.<br /> + To set up this service: + <ol> + <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation. </li> + <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event. </li> + <li>Paste the webhook <strong>URL</strong> into the field bellow. </li> + <li>Select events below to enable notifications. The channel and username are optional. </li> + </ol>' + end + + def fields + default_fields + build_event_channels + end + + def default_fields + [ + { type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' }, + { type: 'text', name: 'username', placeholder: 'username' }, + { type: 'checkbox', name: 'notify_only_broken_builds' }, + { type: 'checkbox', name: 'notify_only_broken_pipelines' }, + ] + end + + def default_channel + "#town-square" + end +end diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb deleted file mode 100644 index 9d61c251a32..00000000000 --- a/app/models/project_services/mattermost_service.rb +++ /dev/null @@ -1,41 +0,0 @@ -class MattermostService < ChatService - def title - 'Mattermost notifications' - end - - def description - 'Receive event notifications in Mattermost' - end - - def to_param - 'mattermost' - end - - def help - 'This service sends notifications about projects events to Mattermost channels.<br /> - To set up this service: - <ol> - <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation. </li> - <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event. </li> - <li>Paste the webhook <strong>URL</strong> into the field bellow. </li> - <li>Select events below to enable notifications. The channel and username are optional. </li> - </ol>' - end - - def fields - default_fields + build_event_channels - end - - def default_fields - [ - { type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' }, - { type: 'text', name: 'username', placeholder: 'username' }, - { type: 'checkbox', name: 'notify_only_broken_builds' }, - { type: 'checkbox', name: 'notify_only_broken_pipelines' }, - ] - end - - def default_channel - "#town-square" - end -end diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 3993dfbda17..33431f41dc2 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -1,18 +1,8 @@ -class MattermostSlashCommandsService < Service +class MattermostSlashCommandsService < ChatService include TriggersHelper prop_accessor :token - def valid_token?(token) - self.respond_to?(:token) && - self.token.present? && - ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) - end - - def supported_events - [] - end - def can_test? false end diff --git a/app/models/project_services/slack_notification_service.rb b/app/models/project_services/slack_notification_service.rb new file mode 100644 index 00000000000..3cbf89efba4 --- /dev/null +++ b/app/models/project_services/slack_notification_service.rb @@ -0,0 +1,40 @@ +class SlackNotificationService < ChatNotificationService + def title + 'Slack notifications' + end + + def description + 'Receive event notifications in Slack' + end + + def to_param + 'slack_notification' + end + + def help + 'This service sends notifications about projects events to Slack channels.<br /> + To setup this service: + <ol> + <li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event. </li> + <li>Paste the <strong>Webhook URL</strong> into the field below. </li> + <li>Select events below to enable notifications. The channel and username are optional. </li> + </ol>' + end + + def fields + default_fields + build_event_channels + end + + def default_fields + [ + { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' }, + { type: 'text', name: 'username', placeholder: 'username' }, + { type: 'checkbox', name: 'notify_only_broken_builds' }, + { type: 'checkbox', name: 'notify_only_broken_pipelines' }, + ] + end + + def default_channel + "#general" + end +end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb deleted file mode 100644 index 0df1743c4ba..00000000000 --- a/app/models/project_services/slack_service.rb +++ /dev/null @@ -1,40 +0,0 @@ -class SlackService < ChatService - def title - 'Slack notifications' - end - - def description - 'Receive event notifications in Slack' - end - - def to_param - 'slack' - end - - def help - 'This service sends notifications about projects events to Slack channels.<br /> - To setup this service: - <ol> - <li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event. </li> - <li>Paste the <strong>Webhook URL</strong> into the field below. </li> - <li>Select events below to enable notifications. The channel and username are optional. </li> - </ol>' - end - - def fields - default_fields + build_event_channels - end - - def default_fields - [ - { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' }, - { type: 'text', name: 'username', placeholder: 'username' }, - { type: 'checkbox', name: 'notify_only_broken_builds' }, - { type: 'checkbox', name: 'notify_only_broken_pipelines' }, - ] - end - - def default_channel - "#general" - end -end diff --git a/app/models/service.rb b/app/models/service.rb index 8e58f2a1925..0bbab078cf6 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -220,8 +220,8 @@ class Service < ActiveRecord::Base pivotaltracker pushover redmine - mattermost - slack + mattermost_notification + slack_notification teamcity ] end diff --git a/db/migrate/20141006143943_move_slack_service_to_webhook.rb b/db/migrate/20141006143943_move_slack_service_to_webhook.rb index 8cb120f7007..42e88d6d6e3 100644 --- a/db/migrate/20141006143943_move_slack_service_to_webhook.rb +++ b/db/migrate/20141006143943_move_slack_service_to_webhook.rb @@ -1,7 +1,11 @@ # rubocop:disable all class MoveSlackServiceToWebhook < ActiveRecord::Migration + + DOWNTIME = true + DOWNTIME_REASON = 'Move old fields "token" and "subdomain" to one single field "webhook"' + def change - SlackService.all.each do |slack_service| + SlackNotificationService.all.each do |slack_service| if ["token", "subdomain"].all? { |property| slack_service.properties.key? property } token = slack_service.properties['token'] subdomain = slack_service.properties['subdomain'] diff --git a/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb b/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb new file mode 100644 index 00000000000..a7278d7b5a6 --- /dev/null +++ b/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb @@ -0,0 +1,14 @@ +class ChangeSlackServiceToSlackNotificationService < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = 'Rename SlackService to SlackNotificationService' + + def up + execute("UPDATE services SET type = 'SlackNotificationService' WHERE type = 'SlackService'") + end + + def down + execute("UPDATE services SET type = 'SlackService' WHERE type = 'SlackNotificationService'") + end +end diff --git a/db/schema.rb b/db/schema.rb index 4711b7873af..5bd4ef1f21b 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: 20161212142807) do +ActiveRecord::Schema.define(version: 20161213172958) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/lib/api/services.rb b/lib/api/services.rb index b1e072b4f47..59232c84c24 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -473,7 +473,7 @@ module API desc: 'The description of the tracker' } ], - 'slack' => [ + 'slack-notification' => [ { required: true, name: :webhook, @@ -493,6 +493,14 @@ module API desc: 'The channel name' } ], + 'mattermost-notification' => [ + { + required: true, + name: :webhook, + type: String, + desc: 'The Mattermost webhook. e.g. http://mattermost_host/hooks/...' + } + ], 'teamcity' => [ { required: true, diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz index bfe59bdb90e..d3165d07d7b 100644 Binary files a/spec/features/projects/import_export/test_project_export.tar.gz and b/spec/features/projects/import_export/test_project_export.tar.gz differ diff --git a/spec/features/projects/services/slack_service_spec.rb b/spec/features/projects/services/slack_service_spec.rb index 16541f51d98..320ed13a01d 100644 --- a/spec/features/projects/services/slack_service_spec.rb +++ b/spec/features/projects/services/slack_service_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' feature 'Projects > Slack service > Setup events', feature: true do let(:user) { create(:user) } - let(:service) { SlackService.new } - let(:project) { create(:project, slack_service: service) } + let(:service) { SlackNotificationService.new } + let(:project) { create(:project, slack_notification_service: service) } background do service.fields diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 068137f6255..9b49d6837c3 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -136,8 +136,8 @@ project: - assembla_service - asana_service - gemnasium_service -- slack_service -- mattermost_service +- slack_notification_service +- mattermost_notification_service - buildkite_service - bamboo_service - teamcity_service diff --git a/spec/models/project_services/chat_notification_service_spec.rb b/spec/models/project_services/chat_notification_service_spec.rb new file mode 100644 index 00000000000..b4fb1cd9ed9 --- /dev/null +++ b/spec/models/project_services/chat_notification_service_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe ChatNotificationService, models: true do + describe "Associations" do + + before do + allow(subject).to receive(:activated?).and_return(true) + end + + it { is_expected.to validate_presence_of :webhook } + end +end diff --git a/spec/models/project_services/chat_service_spec.rb b/spec/models/project_services/chat_service_spec.rb index e6314a43501..c6a45a3e1be 100644 --- a/spec/models/project_services/chat_service_spec.rb +++ b/spec/models/project_services/chat_service_spec.rb @@ -2,7 +2,14 @@ require 'spec_helper' describe ChatService, models: true do describe "Associations" do - before { allow(subject).to receive(:activated?).and_return(true) } - it { is_expected.to validate_presence_of :webhook } + it { is_expected.to have_many :chat_names } + end + + describe '#valid_token?' do + subject { described_class.new } + + it 'is false as it has no token' do + expect(subject.valid_token?('wer')).to be_falsey + end end end diff --git a/spec/models/project_services/mattermost_notification_service_spec.rb b/spec/models/project_services/mattermost_notification_service_spec.rb new file mode 100644 index 00000000000..c01e64b4c8e --- /dev/null +++ b/spec/models/project_services/mattermost_notification_service_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe MattermostNotificationService, models: true do + it_behaves_like "slack or mattermost" +end diff --git a/spec/models/project_services/mattermost_service_spec.rb b/spec/models/project_services/mattermost_service_spec.rb deleted file mode 100644 index 1e5b4c715c3..00000000000 --- a/spec/models/project_services/mattermost_service_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'spec_helper' - -describe MattermostService, models: true do - it_behaves_like "slack or mattermost" -end diff --git a/spec/models/project_services/slack_notification_service_spec.rb b/spec/models/project_services/slack_notification_service_spec.rb new file mode 100644 index 00000000000..59ddddf7454 --- /dev/null +++ b/spec/models/project_services/slack_notification_service_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe SlackNotificationService, models: true do + it_behaves_like "slack or mattermost" +end diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb deleted file mode 100644 index 4928391fd7e..00000000000 --- a/spec/models/project_services/slack_service_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'spec_helper' - -describe SlackService, models: true do - it_behaves_like "slack or mattermost" -end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 1d8e42202ea..bab3c3dbb02 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -22,8 +22,8 @@ describe Project, models: true do it { is_expected.to have_many(:protected_branches).dependent(:destroy) } it { is_expected.to have_many(:chat_services) } it { is_expected.to have_one(:forked_project_link).dependent(:destroy) } - it { is_expected.to have_one(:slack_service).dependent(:destroy) } - it { is_expected.to have_one(:mattermost_service).dependent(:destroy) } + it { is_expected.to have_one(:slack_notification_service).dependent(:destroy) } + it { is_expected.to have_one(:mattermost_notification_service).dependent(:destroy) } it { is_expected.to have_one(:pushover_service).dependent(:destroy) } it { is_expected.to have_one(:asana_service).dependent(:destroy) } it { is_expected.to have_many(:boards).dependent(:destroy) } -- cgit v1.2.1 From de0dfb10d47da609b3cca1f0e5310c14e5b1572c Mon Sep 17 00:00:00 2001 From: Munken <mm.munk@gmail.com> Date: Thu, 15 Dec 2016 18:03:57 +0000 Subject: Don't open Asciidoc module twice --- lib/gitlab/asciidoc.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb index f77f412da56..fa234284361 100644 --- a/lib/gitlab/asciidoc.rb +++ b/lib/gitlab/asciidoc.rb @@ -35,11 +35,7 @@ module Gitlab html.html_safe end - end -end -module Gitlab - module Asciidoc class Html5Converter < Asciidoctor::Converter::Html5Converter extend Asciidoctor::Converter::Config -- cgit v1.2.1 From 82f9957d466810314b8f3af672bcdd706905007a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Tue, 13 Dec 2016 20:49:25 +0200 Subject: Refactor Namespace#parents method Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- app/models/namespace.rb | 12 +----------- spec/models/namespace_spec.rb | 8 +++++++- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/app/models/namespace.rb b/app/models/namespace.rb index b3cefc01b99..3830379be3e 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -172,17 +172,7 @@ class Namespace < ActiveRecord::Base end def parents - @parents ||= - begin - parents = [] - - if parent - parents << parent - parents += parent.parents - end - - parents - end + @parents ||= parent ? parent.parents + [parent] : [] end private diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 90e8f6b7227..1b10dd23c50 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -145,7 +145,13 @@ describe Namespace, models: true do let(:group) { create(:group) } let(:nested_group) { create(:group, parent: group) } let(:deep_nested_group) { create(:group, parent: nested_group) } + let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } - it { expect(deep_nested_group.parents).to eq([nested_group, group]) } + it 'returns the correct parents' do + expect(very_deep_nested_group.parents).to eq([group, nested_group, deep_nested_group]) + expect(deep_nested_group.parents).to eq([group, nested_group]) + expect(nested_group.parents).to eq([group]) + expect(group.parents).to eq([]) + end end end -- cgit v1.2.1 From 24d2e662d4251ed8c73a57319576af6de3d488a1 Mon Sep 17 00:00:00 2001 From: tauriedavis <taurie@gitlab.com> Date: Fri, 2 Dec 2016 12:38:04 -0800 Subject: Limit description container for mrs while viewing side by side diff --- app/assets/stylesheets/framework/variables.scss | 3 +- app/assets/stylesheets/pages/issuable.scss | 52 ++++++++++++++++++----- app/assets/stylesheets/pages/notes.scss | 4 -- app/views/projects/merge_requests/_show.html.haml | 2 +- 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 55d97b9219c..13abe6033b2 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -166,8 +166,7 @@ $row-hover-border: #b2d7ff; $progress-color: #c0392b; $header-height: 50px; $fixed-layout-width: 1280px; -$limited-layout-width: 958px; -$line-length-width: 700px; +$limited-layout-width: 990px; $gl-avatar-size: 40px; $error-exclamation-point: #e62958; $border-radius-default: 2px; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 2357dd2fe6f..0b15a72e6b4 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -1,15 +1,48 @@ -.container-limited.limit-container-width { - .issue-details { - .description, - .note-body { - p, - ul, - ol, - .code { - max-width: $line-length-width; +// Limit MR description for side-by-side diff view +.limit-container-width { + .detail-page-header { + max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2)); + margin-left: auto; + margin-right: auto; + } + + .issuable-details { + .detail-page-description, + .mr-source-target, + .mr-state-widget, + .merge-manually { + max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2)); + margin-left: auto; + margin-right: auto; + } + + .merge-request-tabs-holder { + &.affix { + border-bottom: 1px solid $border-color; + + .nav-links { + border: 0; + } + } + + .container-fluid { + padding-left: 0; + padding-right: 0; + max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2)); + margin-left: auto; + margin-right: auto; } } } + + .diffs { + .mr-version-controls, + .files-changed { + max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2)); + margin-left: auto; + margin-right: auto; + } + } } .issuable-details { @@ -23,7 +56,6 @@ .description img:not(.emoji) { border: 1px solid $table-border-gray; padding: 5px; - margin: 5px; max-height: calc(100vh - 100px); } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 10eb3d4203e..d697c8d25df 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -383,10 +383,6 @@ ul.notes { .note-action-button { margin-left: 10px; } - - @media (min-width: $screen-sm-min) { - position: relative; - } } .discussion-actions { diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 0db5548d36e..1187b04edb8 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -42,7 +42,7 @@ = render "projects/merge_requests/widget/show.html.haml" - if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user) - .light.prepend-top-default.append-bottom-default + .merge-manually.light.prepend-top-default.append-bottom-default You can also accept this merge request manually using the = succeed '.' do = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" -- cgit v1.2.1 From b6a1c0bf9bf9dd9c9a017d5867d856cd9086d8fd Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Thu, 15 Dec 2016 21:30:35 +0200 Subject: Add missing group policy spec Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- spec/policies/group_policy_spec.rb | 108 +++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 spec/policies/group_policy_spec.rb diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb new file mode 100644 index 00000000000..a20ac303a53 --- /dev/null +++ b/spec/policies/group_policy_spec.rb @@ -0,0 +1,108 @@ +require 'spec_helper' + +describe GroupPolicy, models: true do + let(:guest) { create(:user) } + let(:reporter) { create(:user) } + let(:developer) { create(:user) } + let(:master) { create(:user) } + let(:owner) { create(:user) } + let(:admin) { create(:admin) } + let(:group) { create(:group) } + + let(:master_permissions) do + [ + :create_projects, + :admin_milestones, + :admin_label + ] + end + + let(:owner_permissions) do + [ + :admin_group, + :admin_namespace, + :admin_group_member, + :change_visibility_level + ] + end + + before do + group.add_guest(guest) + group.add_reporter(reporter) + group.add_developer(developer) + group.add_master(master) + group.add_owner(owner) + end + + subject { described_class.abilities(current_user, group).to_set } + + context 'with no user' do + let(:current_user) { nil } + + it do + is_expected.to include(:read_group) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'guests' do + let(:current_user) { guest } + + it do + is_expected.to include(:read_group) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'reporter' do + let(:current_user) { reporter } + + it do + is_expected.to include(:read_group) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'developer' do + let(:current_user) { developer } + + it do + is_expected.to include(:read_group) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'master' do + let(:current_user) { master } + + it do + is_expected.to include(:read_group) + is_expected.to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'owner' do + let(:current_user) { owner } + + it do + is_expected.to include(:read_group) + is_expected.to include(*master_permissions) + is_expected.to include(*owner_permissions) + end + end + + context 'admin' do + let(:current_user) { admin } + + it do + is_expected.to include(:read_group) + is_expected.to include(*master_permissions) + is_expected.to include(*owner_permissions) + end + end +end -- cgit v1.2.1 From e42de89a15c858866d78a4d2a5837a0feec922a5 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 15 Dec 2016 17:30:49 +0000 Subject: Changes after review Changes after review Fix tooltip title Remove unneeded string interpolation --- app/assets/stylesheets/pages/pipelines.scss | 10 +-- app/views/ci/status/_graph_badge.html.haml | 4 +- .../projects/ci/builds/_build_pipeline.html.haml | 13 --- .../_generic_commit_status_pipeline.html.haml | 10 --- spec/features/projects/pipelines/pipeline_spec.rb | 93 ++++++++++++++-------- 5 files changed, 62 insertions(+), 68 deletions(-) delete mode 100644 app/views/projects/ci/builds/_build_pipeline.html.haml delete mode 100644 app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index d3f39570f11..be22e7bdc79 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -645,14 +645,6 @@ margin-bottom: 0; line-height: 1.2; } - - li:first-child { - padding-top: 6px; - } - - li:last-child { - padding-bottom: 6px; - } } .dropdown-build { @@ -741,4 +733,4 @@ .ci-play-icon { padding: 5px 5px 5px 7px; } -} \ No newline at end of file +} diff --git a/app/views/ci/status/_graph_badge.html.haml b/app/views/ci/status/_graph_badge.html.haml index a7e8544e7d4..c7d04ab61e9 100644 --- a/app/views/ci/status/_graph_badge.html.haml +++ b/app/views/ci/status/_graph_badge.html.haml @@ -5,7 +5,7 @@ - klass = "ci-status-icon ci-status-icon-#{status}" - if status.has_details? - = link_to status.details_path, data: { toggle: 'tooltip', title: "#{subject.name} - #{status}" } do + = link_to status.details_path, data: { toggle: 'tooltip', title: "#{subject.name} - #{status.label}" } do %span{ class: klass }= custom_icon(status.icon) .ci-status-text= subject.name - else @@ -14,6 +14,6 @@ - if status.has_action? = link_to status.action_path, method: status.action_method, - title: "#{subject.name}: #{status.action_title}", class: 'ci-action-icon-container' do + title: status.action_title, class: 'ci-action-icon-container' do %i.ci-action-icon-wrapper = icon(status.action_icon, class: status.action_class) diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml deleted file mode 100644 index ad1a7360a8b..00000000000 --- a/app/views/projects/ci/builds/_build_pipeline.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- is_playable = subject.playable? && can?(current_user, :update_build, @project) -- if is_playable - = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.js-pipeline-graph', placement: 'bottom' } do - = ci_icon_for_status('play') - .ci-status-text= subject.name -- elsif can?(current_user, :read_build, @project) - = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } do - %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} - = ci_icon_for_status(subject.status) - .ci-status-text= subject.name -- else - %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} - = ci_icon_for_status(subject.status) diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml deleted file mode 100644 index 1bba0443154..00000000000 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } } - - if subject.target_url - = link_to subject.target_url do - %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} - = ci_icon_for_status(subject.status) - %span.ci-status-text= subject.name - - else - %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} - = ci_icon_for_status(subject.status) - %span.ci-status-text= subject.name diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 80a596d34c9..0a77eaa123c 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -38,63 +38,88 @@ describe "Pipelines", feature: true, js: true do expect(page).to have_css('#js-tab-pipeline.active') end - context 'pipeline graph' do - it 'shows a running icon and a cancel action for the running build' do - title = "#{@running.name} - #{@running.status}" - - page.within("a[data-title='#{title}']") do - expect(page).to have_selector('.ci-status-icon-running') - expect(page).to have_content('deploy') + describe 'pipeline graph' do + context 'when pipeline has running builds' do + it 'shows a running icon and a cancel action for the running build' do + page.within('a[data-title="deploy - running"]') do + expect(page).to have_selector('.ci-status-icon-running') + expect(page).to have_content('deploy') + end + + page.within('a[data-title="deploy - running"] + .ci-action-icon-container') do + expect(page).to have_selector('.ci-action-icon-container .fa-ban') + end end - page.within("a[data-title='#{title}'] + .ci-action-icon-container") do - expect(page).to have_selector('.ci-action-icon-container .fa-ban') - end + it 'should be possible to cancel the running build' do + find('a[data-title="deploy - running"] + .ci-action-icon-container').trigger('click') + expect(page).not_to have_content('Cancel running') + end end - it 'shows the success icon and a retry action for the successfull build' do - title = "#{@success.name} - #{@success.status}" + context 'when pipeline has successful builds' do + it 'shows the success icon and a retry action for the successfull build' do + page.within('a[data-title="build - passed"]') do + expect(page).to have_selector('.ci-status-icon-success') + expect(page).to have_content('build') + end - page.within("a[data-title='#{title}']") do - expect(page).to have_selector('.ci-status-icon-success') - expect(page).to have_content('build') + page.within('a[data-title="build - passed"] + .ci-action-icon-container') do + expect(page).to have_selector('.ci-action-icon-container .fa-refresh') + end end - page.within("a[data-title='#{title}'] + .ci-action-icon-container") do - expect(page).to have_selector('.ci-action-icon-container .fa-refresh') + it 'should be possible to retry the success build' do + find('a[data-title="build - passed"] + .ci-action-icon-container').trigger('click') + + expect(page).not_to have_content('Retry build') end end - it 'shows the failed icon and a retry action for the failed build' do - title = "#{@failed.name} - #{@failed.status}" + context 'when pipeline has failed builds' do + it 'shows the failed icon and a retry action for the failed build' do + page.within('a[data-title="test - failed"]') do + expect(page).to have_selector('.ci-status-icon-failed') + expect(page).to have_content('test') + end - page.within("a[data-title='#{title}']") do - expect(page).to have_selector('.ci-status-icon-failed') - expect(page).to have_content('test') + page.within('a[data-title="test - failed"] + .ci-action-icon-container') do + expect(page).to have_selector('.ci-action-icon-container .fa-refresh') + end end - page.within("a[data-title='#{title}'] + .ci-action-icon-container") do - expect(page).to have_selector('.ci-action-icon-container .fa-refresh') + it 'should be possible to retry the failed build' do + find('a[data-title="test - failed"] + .ci-action-icon-container').trigger('click') + + expect(page).not_to have_content('Retry build') end end - it 'shows the skipped icon and a play action for the manual build' do - title = "#{@manual.name} - #{@manual.status}" + context 'when pipeline has manual builds' do + it 'shows the skipped icon and a play action for the manual build' do + page.within('a[data-title="manual build - manual play action"]') do + expect(page).to have_selector('.ci-status-icon-skipped') + expect(page).to have_content('manual') + end - page.within("a[data-title='#{title}']") do - expect(page).to have_selector('.ci-status-icon-skipped') - expect(page).to have_content('manual') + page.within('a[data-title="manual build - manual play action"] + .ci-action-icon-container') do + expect(page).to have_selector('.ci-action-icon-container .fa-play') + end end - page.within("a[data-title='#{title}'] + .ci-action-icon-container") do - expect(page).to have_selector('.ci-action-icon-container .fa-play') + it 'should be possible to play the manual build' do + find('a[data-title="manual build - manual play action"] + .ci-action-icon-container').trigger('click') + + expect(page).not_to have_content('Play build') end end - it 'shows the success icon and the generic comit status build' do - expect(page).to have_selector('.ci-status-icon-success') - expect(page).to have_content('jenkins') + context 'when pipeline has external build' do + it 'shows the success icon and the generic comit status build' do + expect(page).to have_selector('.ci-status-icon-success') + expect(page).to have_content('jenkins') + end end end -- cgit v1.2.1 From 958a499a7f2f9cfad35b2861c33f1a1d1e248eaf Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran <alfredo@gitlab.com> Date: Thu, 15 Dec 2016 18:02:45 -0500 Subject: Fix eslint errors --- app/assets/javascripts/dispatcher.js.es6 | 2 +- app/assets/javascripts/issues_bulk_assignment.js.es6 | 14 +++++++------- app/assets/javascripts/labels_select.js | 2 +- app/assets/javascripts/render_math.js | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 1ec950494ff..1e259a16f06 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -75,7 +75,7 @@ case 'projects:issues:index': Issuable.init(); new gl.IssuableBulkActions({ - prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_' + prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_', }); shortcut_handler = new ShortcutsNavigation(); break; diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6 index 1c8e5dede6f..52fd5d71b18 100644 --- a/app/assets/javascripts/issues_bulk_assignment.js.es6 +++ b/app/assets/javascripts/issues_bulk_assignment.js.es6 @@ -6,7 +6,7 @@ class IssuableBulkActions { constructor({ container, form, issues, prefixId } = {}) { - this.prefixId = prefixId || 'issue_'; + this.prefixId = prefixId || 'issue_'; this.form = form || this.getElement('.bulk-update'); this.$labelDropdown = this.form.find('.js-label-select'); this.issues = issues || this.getElement('.issues-list .issue'); @@ -107,7 +107,7 @@ } setOriginalDropdownData() { - let $labelSelect = $('.bulk-update .js-label-select'); + const $labelSelect = $('.bulk-update .js-label-select'); $labelSelect.data('common', this.getOriginalCommonIds()); $labelSelect.data('marked', this.getOriginalMarkedIds()); $labelSelect.data('indeterminate', this.getOriginalIndeterminateIds()); @@ -115,7 +115,7 @@ // From issuable's initial bulk selection getOriginalCommonIds() { - let labelIds = []; + const labelIds = []; this.getElement('.selected_issue:checked').each((i, el) => { labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels')); @@ -125,17 +125,17 @@ // From issuable's initial bulk selection getOriginalMarkedIds() { - var labelIds = []; + const labelIds = []; this.getElement('.selected_issue:checked').each((i, el) => { labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels')); }); - return _.intersection.apply(_, labelIds); + return _.intersection.apply(this, labelIds); } // From issuable's initial bulk selection getOriginalIndeterminateIds() { - let uniqueIds = []; - let labelIds = []; + const uniqueIds = []; + const labelIds = []; let issuableLabels = []; // Collect unique label IDs for all checked issues diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 6853d6b9db2..ec2fc87bece 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -8,7 +8,7 @@ var _this; _this = this; $('.js-label-select').each(function(i, dropdown) { - var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container; + var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer; $dropdown = $(dropdown); $dropdownContainer = $dropdown.closest('.labels-filter'); $toggleText = $dropdown.find('.dropdown-toggle-text'); diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js index a8a56430f88..209e7a8661d 100644 --- a/app/assets/javascripts/render_math.js +++ b/app/assets/javascripts/render_math.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len, no-console */ // Renders math using KaTeX in any element with the // `js-render-math` class // -- cgit v1.2.1 From fb4d763c138830b324d1b060fe1c05bf451e91e5 Mon Sep 17 00:00:00 2001 From: Allison Whilden <allison@gitlab.com> Date: Thu, 15 Dec 2016 15:15:17 -0800 Subject: [ci skip] UX Guide: add guidance on cursor usage --- doc/development/ux_guide/basics.md | 16 ++++++++++++++++ doc/development/ux_guide/img/cursors-default.png | Bin 0 -> 567 bytes doc/development/ux_guide/img/cursors-ibeam.png | Bin 0 -> 383 bytes doc/development/ux_guide/img/cursors-move.png | Bin 0 -> 276 bytes doc/development/ux_guide/img/cursors-panclosed.png | Bin 0 -> 483 bytes doc/development/ux_guide/img/cursors-panopened.png | Bin 0 -> 622 bytes doc/development/ux_guide/img/cursors-pointer.png | Bin 0 -> 574 bytes 7 files changed, 16 insertions(+) create mode 100644 doc/development/ux_guide/img/cursors-default.png create mode 100644 doc/development/ux_guide/img/cursors-ibeam.png create mode 100644 doc/development/ux_guide/img/cursors-move.png create mode 100644 doc/development/ux_guide/img/cursors-panclosed.png create mode 100644 doc/development/ux_guide/img/cursors-panopened.png create mode 100644 doc/development/ux_guide/img/cursors-pointer.png diff --git a/doc/development/ux_guide/basics.md b/doc/development/ux_guide/basics.md index 76b739386a5..e81729556d8 100644 --- a/doc/development/ux_guide/basics.md +++ b/doc/development/ux_guide/basics.md @@ -5,6 +5,7 @@ * [Typography](#typography) * [Icons](#icons) * [Color](#color) +* [Cursors](#cursors) --- @@ -59,3 +60,18 @@ GitLab uses Font Awesome icons throughout our interface. > TODO: Establish a perspective for color in terms of our personality and rationalize with Marketing usage. +--- + +## Cursors +The mouse cursor is key in helping users understand how to interact with elements on the screen. + +| | | +| :------: | :------- | +| ![Default cursor](img/cursors-default.png) | Default cursor | +| ![Pointer cursor](img/cursors-pointer.png) | Pointer cursor: used to indicate that you can click on an element to invoke a command or navigate, such as links and buttons | +| ![Move cursor](img/cursors-move.png) | Move cursor: used to indicate that you can move an element around on the screen | +| ![Pan opened cursor](img/cursors-panopened.png) | Pan cursor (opened): indicates that you can grab and move the entire canvas, affecting what is seen in the view port. | +| ![Pan closed cursor](img/cursors-panclosed.png) | Pan cursor (closed): indicates that you are actively panning the canvas. | +| ![I-beam cursor](img/cursors-ibeam.png) | I-beam cursor: indicates that this is either text that you can select and copy, or a text field that you can click into to enter text. | + + diff --git a/doc/development/ux_guide/img/cursors-default.png b/doc/development/ux_guide/img/cursors-default.png new file mode 100644 index 00000000000..c188ec4e351 Binary files /dev/null and b/doc/development/ux_guide/img/cursors-default.png differ diff --git a/doc/development/ux_guide/img/cursors-ibeam.png b/doc/development/ux_guide/img/cursors-ibeam.png new file mode 100644 index 00000000000..86f28639982 Binary files /dev/null and b/doc/development/ux_guide/img/cursors-ibeam.png differ diff --git a/doc/development/ux_guide/img/cursors-move.png b/doc/development/ux_guide/img/cursors-move.png new file mode 100644 index 00000000000..a9c610eaa88 Binary files /dev/null and b/doc/development/ux_guide/img/cursors-move.png differ diff --git a/doc/development/ux_guide/img/cursors-panclosed.png b/doc/development/ux_guide/img/cursors-panclosed.png new file mode 100644 index 00000000000..6d247a765ac Binary files /dev/null and b/doc/development/ux_guide/img/cursors-panclosed.png differ diff --git a/doc/development/ux_guide/img/cursors-panopened.png b/doc/development/ux_guide/img/cursors-panopened.png new file mode 100644 index 00000000000..76f2eeda831 Binary files /dev/null and b/doc/development/ux_guide/img/cursors-panopened.png differ diff --git a/doc/development/ux_guide/img/cursors-pointer.png b/doc/development/ux_guide/img/cursors-pointer.png new file mode 100644 index 00000000000..d86dd955fa7 Binary files /dev/null and b/doc/development/ux_guide/img/cursors-pointer.png differ -- cgit v1.2.1 From 8dc7ffa02e4aed4af10b7ea04266ac900e78420e Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Thu, 15 Dec 2016 22:35:44 -0200 Subject: Fix spec/features/admin/admin_active_tab_spec.rb --- spec/features/admin/admin_active_tab_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/admin/admin_active_tab_spec.rb b/spec/features/admin/admin_active_tab_spec.rb index f2eecc5b552..16064d60ce2 100644 --- a/spec/features/admin/admin_active_tab_spec.rb +++ b/spec/features/admin/admin_active_tab_spec.rb @@ -29,7 +29,7 @@ RSpec.describe 'admin active tab' do context 'on projects' do before do - visit admin_namespaces_projects_path + visit admin_projects_path end it_behaves_like 'page has active tab', 'Overview' -- cgit v1.2.1 From 5920c5d2caaf37899a90c4ae5989b0fc8935e847 Mon Sep 17 00:00:00 2001 From: Munken <mm.munk@gmail.com> Date: Fri, 16 Dec 2016 01:44:41 +0000 Subject: Added KaTeX license and procedure to build it for Gitlab --- vendor/assets/javascripts/katex.js | 41 +++++++++++++++++++++++++++++++++++++ vendor/assets/stylesheets/katex.css | 41 +++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/vendor/assets/javascripts/katex.js b/vendor/assets/javascripts/katex.js index 9596b839832..2ce3a5e8d4d 100644 --- a/vendor/assets/javascripts/katex.js +++ b/vendor/assets/javascripts/katex.js @@ -1,3 +1,44 @@ +/* + The MIT License (MIT) + + Copyright (c) 2015 Khan Academy + + This software also uses portions of the underscore.js project, which is + MIT licensed with the following copyright: + + Copyright (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative + Reporters & Editors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +/* + Here is how to build a version of KaTeX that works with gitlab. + + The problem is that the procedure for changing font location doesn't work for ''. + + 1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do. + 2. make (requires node) + 3. sed -i 's,fonts/,,' build/katex.css + 4. Copy build/katex.js, build/katex.css and fonts/* to gitlab. +*/ + (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.katex = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ /* eslint no-console:0 */ /** diff --git a/vendor/assets/stylesheets/katex.css b/vendor/assets/stylesheets/katex.css index e684d697072..2d0b4635ccf 100644 --- a/vendor/assets/stylesheets/katex.css +++ b/vendor/assets/stylesheets/katex.css @@ -1,3 +1,44 @@ +/* +The MIT License (MIT) + +Copyright (c) 2015 Khan Academy + +This software also uses portions of the underscore.js project, which is +MIT licensed with the following copyright: + +Copyright (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative +Reporters & Editors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/* + Here is how to build a version of KaTeX that works with gitlab. + + The problem is that the procedure for changing font location doesn't work for ''. + + 1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do. + 2. make (requires node) + 3. sed -i 's,fonts/,,' build/katex.css + 4. Copy build/katex.js, build/katex.css and fonts/* to gitlab. +*/ + @font-face { font-family: 'KaTeX_AMS'; src: url('KaTeX_AMS-Regular.eot'); -- cgit v1.2.1 From f140ae88c63d0d9976e33eb574702be5e182b851 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Thu, 15 Dec 2016 11:32:06 -0200 Subject: Don't use the Route model in migrations --- db/migrate/20161130095245_fill_routes_table.rb | 2 +- db/migrate/20161130101252_fill_projects_routes_table.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/db/migrate/20161130095245_fill_routes_table.rb b/db/migrate/20161130095245_fill_routes_table.rb index 6754e583000..c3536d6d911 100644 --- a/db/migrate/20161130095245_fill_routes_table.rb +++ b/db/migrate/20161130095245_fill_routes_table.rb @@ -16,6 +16,6 @@ class FillRoutesTable < ActiveRecord::Migration end def down - Route.delete_all(source_type: 'Namespace') + execute("DELETE FROM routes WHERE source_type = 'Namespace'") end end diff --git a/db/migrate/20161130101252_fill_projects_routes_table.rb b/db/migrate/20161130101252_fill_projects_routes_table.rb index 14700583be5..4f3fe7b03a9 100644 --- a/db/migrate/20161130101252_fill_projects_routes_table.rb +++ b/db/migrate/20161130101252_fill_projects_routes_table.rb @@ -17,6 +17,6 @@ class FillProjectsRoutesTable < ActiveRecord::Migration end def down - Route.delete_all(source_type: 'Project') + execute("DELETE FROM routes WHERE source_type = 'Project'") end end -- cgit v1.2.1 From d494c9a789799f3625c25273c3bbeb43a93a3b88 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Thu, 15 Dec 2016 11:42:49 -0200 Subject: Use optimized query to fill the routes table when running PostgreSQL --- .../20161130101252_fill_projects_routes_table.rb | 20 ++++++++++++++------ .../20161202152031_remove_duplicates_from_routes.rb | 5 +++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/db/migrate/20161130101252_fill_projects_routes_table.rb b/db/migrate/20161130101252_fill_projects_routes_table.rb index 4f3fe7b03a9..56ba6fcdbe3 100644 --- a/db/migrate/20161130101252_fill_projects_routes_table.rb +++ b/db/migrate/20161130101252_fill_projects_routes_table.rb @@ -8,12 +8,20 @@ class FillProjectsRoutesTable < ActiveRecord::Migration DOWNTIME_REASON = 'No new projects should be created during data copy' def up - execute <<-EOF - INSERT INTO routes - (source_id, source_type, path) - (SELECT projects.id, 'Project', concat(namespaces.path, '/', projects.path) FROM projects - INNER JOIN namespaces ON projects.namespace_id = namespaces.id) - EOF + if Gitlab::Database.postgresql? + execute <<-EOF + INSERT INTO routes (source_id, source_type, path) + (SELECT DISTINCT ON (namespaces.path, projects.path) projects.id, 'Project', concat(namespaces.path, '/', projects.path) + FROM projects INNER JOIN namespaces ON projects.namespace_id = namespaces.id + ORDER BY namespaces.path, projects.path, projects.id DESC) + EOF + else + execute <<-EOF + INSERT INTO routes (source_id, source_type, path) + (SELECT projects.id, 'Project', concat(namespaces.path, '/', projects.path) + FROM projects INNER JOIN namespaces ON projects.namespace_id = namespaces.id) + EOF + end end def down diff --git a/db/migrate/20161202152031_remove_duplicates_from_routes.rb b/db/migrate/20161202152031_remove_duplicates_from_routes.rb index 510796e05f2..a21bde69995 100644 --- a/db/migrate/20161202152031_remove_duplicates_from_routes.rb +++ b/db/migrate/20161202152031_remove_duplicates_from_routes.rb @@ -7,6 +7,11 @@ class RemoveDuplicatesFromRoutes < ActiveRecord::Migration DOWNTIME = false def up + # We can skip this migration when running a PostgreSQL database because + # we use an optimized query in the "FillProjectsRoutesTable" migration + # to fill these values that avoid duplicate entries in the routes table. + return unless Gitlab::Database.mysql? + select_all("SELECT path FROM #{quote_table_name(:routes)} GROUP BY path HAVING COUNT(*) > 1").each do |row| path = connection.quote(row['path']) execute(%Q{ -- cgit v1.2.1 From fd0d8a28327e2f2a35fe84e057dbc29cc18fc4cc Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Thu, 15 Dec 2016 11:46:15 -0200 Subject: Fix the AddNameIndexToNamespace migration to be reversible --- db/migrate/20161206153754_add_name_index_to_namespace.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/20161206153754_add_name_index_to_namespace.rb b/db/migrate/20161206153754_add_name_index_to_namespace.rb index aaa35ed6f0a..b3f3cb68a99 100644 --- a/db/migrate/20161206153754_add_name_index_to_namespace.rb +++ b/db/migrate/20161206153754_add_name_index_to_namespace.rb @@ -13,7 +13,7 @@ class AddNameIndexToNamespace < ActiveRecord::Migration end def down - if index_exists?(:namespaces, :name) + if index_exists?(:namespaces, [:name, :parent_id]) remove_index :namespaces, [:name, :parent_id] end end -- cgit v1.2.1 From ad4ec44673254b4bb91c6ddb391ff9553c9473a5 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Thu, 15 Dec 2016 12:56:29 -0200 Subject: Improve performance on RemoveDuplicatesFromRoutes migration --- ...20161202152031_remove_duplicates_from_routes.rb | 24 +++++++++------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/db/migrate/20161202152031_remove_duplicates_from_routes.rb b/db/migrate/20161202152031_remove_duplicates_from_routes.rb index a21bde69995..d73b0847506 100644 --- a/db/migrate/20161202152031_remove_duplicates_from_routes.rb +++ b/db/migrate/20161202152031_remove_duplicates_from_routes.rb @@ -12,20 +12,16 @@ class RemoveDuplicatesFromRoutes < ActiveRecord::Migration # to fill these values that avoid duplicate entries in the routes table. return unless Gitlab::Database.mysql? - select_all("SELECT path FROM #{quote_table_name(:routes)} GROUP BY path HAVING COUNT(*) > 1").each do |row| - path = connection.quote(row['path']) - execute(%Q{ - DELETE FROM #{quote_table_name(:routes)} - WHERE path = #{path} - AND id != ( - SELECT id FROM ( - SELECT max(id) AS id - FROM #{quote_table_name(:routes)} - WHERE path = #{path} - ) max_ids - ) - }) - end + execute <<-EOF + DELETE duplicated_rows.* + FROM routes AS duplicated_rows + INNER JOIN ( + SELECT path, MAX(id) as max_id + FROM routes + GROUP BY path + HAVING COUNT(*) > 1 + ) AS good_rows ON good_rows.path = duplicated_rows.path AND good_rows.max_id <> duplicated_rows.id; + EOF end def down -- cgit v1.2.1 From ffa35233573acd31725677547555598fc36072e0 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Fri, 16 Dec 2016 10:38:23 +0200 Subject: BB importer: Added note about linking BB users and GitLab users[ci skip] --- doc/workflow/importing/import_projects_from_bitbucket.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/workflow/importing/import_projects_from_bitbucket.md b/doc/workflow/importing/import_projects_from_bitbucket.md index 520c4216295..935d6288f3b 100644 --- a/doc/workflow/importing/import_projects_from_bitbucket.md +++ b/doc/workflow/importing/import_projects_from_bitbucket.md @@ -20,7 +20,8 @@ It takes just a few steps to import your existing Bitbucket projects to GitLab. ![Import projects](bitbucket_importer/bitbucket_import_select_project.png) -A new GitLab project will be created with your imported data. +A new GitLab project will be created with your imported data. Keep in mind that if you want to Bitbucket users +to be linked to GitLab user you have to have all of them in GitLab in advance. They will be matched by their BitBucket username. ### Note Milestones and wiki pages are not imported from Bitbucket. -- cgit v1.2.1 From e52e3ab5082599fd5a895de961b07584421a5cd2 Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Wed, 23 Nov 2016 21:03:53 +1000 Subject: Remove whole description from #merge_commit_message and add add closed issues --- app/models/merge_request.rb | 3 ++- spec/models/merge_request_spec.rb | 14 ++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index b73d7acefea..62dd02936e2 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -615,7 +615,8 @@ class MergeRequest < ActiveRecord::Base def merge_commit_message message = "Merge branch '#{source_branch}' into '#{target_branch}'\n\n" message << "#{title}\n\n" - message << "#{description}\n\n" if description.present? + mr_closes_issues = closes_issues + message << "Closed Issues: #{mr_closes_issues.map { |issue| issue.to_reference(target_project) }.join(", ")}\n\n" if mr_closes_issues.present? message << "See merge request #{to_reference}" message diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 1b71d00eb8f..e1f9d66714d 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -410,11 +410,17 @@ describe MergeRequest, models: true do .to match("Remove all technical debt\n\n") end - it 'includes its description in the body' do - request = build(:merge_request, description: 'By removing all code') + it 'includes its closed issues in the body' do + issue = create(:issue, project: subject.project) - expect(request.merge_commit_message) - .to match("By removing all code\n\n") + subject.project.team << [subject.author, :developer] + subject.description = "Closes #{issue.to_reference}" + + allow(subject.project).to receive(:default_branch). + and_return(subject.target_branch) + + expect(subject.merge_commit_message) + .to match("Closed Issues: #{issue.to_reference}") end it 'includes its reference in the body' do -- cgit v1.2.1 From 1a8f43ff3e5481d65f547ac01c03e2d64e14fff0 Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Thu, 24 Nov 2016 07:50:49 +1000 Subject: introduce MergeRequest#issues_mentioned_but_not_closing --- app/models/merge_request.rb | 16 ++++++++++++++ spec/models/merge_request_spec.rb | 45 ++++++++++++++++++++++++++------------- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 62dd02936e2..6f660bf0e77 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -568,6 +568,22 @@ class MergeRequest < ActiveRecord::Base end end + def issues_mentioned_but_not_closing(current_user = self.author) + issues = [] + + if target_branch == project.default_branch + messages = [description] + messages.concat(commits.map(&:safe_message)) if merge_request_diff + + ext = Gitlab::ReferenceExtractor.new(project, current_user) + ext.analyze(messages.join("\n")) + + issues = ext.issues + end + + issues - closes_issues + end + def target_project_path if target_project target_project.path_with_namespace diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index e1f9d66714d..688c78fb404 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -252,7 +252,7 @@ describe MergeRequest, models: true do end end - describe 'detection of issues to be closed' do + describe 'detection of issues' do let(:issue0) { create :issue, project: subject.project } let(:issue1) { create :issue, project: subject.project } @@ -265,29 +265,44 @@ describe MergeRequest, models: true do allow(subject).to receive(:commits).and_return([commit0, commit1, commit2]) end - it 'accesses the set of issues that will be closed on acceptance' do - allow(subject.project).to receive(:default_branch). - and_return(subject.target_branch) + describe 'detection of issues to be closed' do + it 'accesses the set of issues that will be closed on acceptance' do + allow(subject.project).to receive(:default_branch). + and_return(subject.target_branch) - closed = subject.closes_issues + closed = subject.closes_issues - expect(closed).to include(issue0, issue1) - end + expect(closed).to include(issue0, issue1) + end + + it 'only lists issues as to be closed if it targets the default branch' do + allow(subject.project).to receive(:default_branch).and_return('master') + subject.target_branch = 'something-else' + + expect(subject.closes_issues).to be_empty + end + + it 'detects issues mentioned in the description' do + issue2 = create(:issue, project: subject.project) + + subject.description = "Closes #{issue2.to_reference}" - it 'only lists issues as to be closed if it targets the default branch' do - allow(subject.project).to receive(:default_branch).and_return('master') - subject.target_branch = 'something-else' + allow(subject.project).to receive(:default_branch). + and_return(subject.target_branch) - expect(subject.closes_issues).to be_empty + expect(subject.closes_issues).to include(issue2) + end end - it 'detects issues mentioned in the description' do - issue2 = create(:issue, project: subject.project) - subject.description = "Closes #{issue2.to_reference}" + it 'detects issues mentioned but not closed' do + mentioned_issue = create(:issue, project: subject.project) + + subject.description = "Is related to #{mentioned_issue.to_reference}" + allow(subject.project).to receive(:default_branch). and_return(subject.target_branch) - expect(subject.closes_issues).to include(issue2) + expect(subject.issues_mentioned_but_not_closing).to match_array([mentioned_issue]) end end -- cgit v1.2.1 From 80915c35f48463c3a983129961f3130bd9703754 Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Thu, 24 Nov 2016 19:21:27 +1000 Subject: diplays mentioned but not merged message on MR show page --- app/helpers/merge_requests_helper.rb | 11 +++++++++++ app/views/projects/merge_requests/widget/_open.html.haml | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index a6659ea2fd1..dcd35dc6282 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -59,6 +59,17 @@ module MergeRequestsHelper @mr_closes_issues ||= @merge_request.closes_issues end + def mr_issues_mentioned_but_not_closing + @mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing + end + + def mr_issues_mentioned_but_not_closing_message + verb = mr_issues_mentioned_but_not_closing.size > 1 ? 'are' : 'is' + issue_text = 'issue'.pluralize(mr_issues_mentioned_but_not_closing.size) + + "The following #{issue_text} #{verb} mentioned but not being closed:" + end + def mr_change_branches_path(merge_request) new_namespace_project_merge_request_path( @project.namespace, @project, diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index eee711dc5af..1f794cad663 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -30,6 +30,15 @@ - elsif @merge_request.can_be_merged? || resolved_conflicts = render 'projects/merge_requests/widget/open/accept' + - if mr_issues_mentioned_but_not_closing.present? + .mr-widget-footer + %span + %i.fa.fa-warning + = mr_issues_mentioned_but_not_closing_message + = succeed '.' do + != markdown issues_sentence(mr_issues_mentioned_but_not_closing), pipeline: :gfm, author: @merge_request.author + = mr_assign_issues_link + - if mr_closes_issues.present? .mr-widget-footer %span -- cgit v1.2.1 From c1515cd865ec11110f9b34c41a41747dcbc3b402 Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Fri, 25 Nov 2016 06:46:16 +1000 Subject: better mentioned but not closing message and icon --- app/helpers/merge_requests_helper.rb | 7 ------- app/views/projects/merge_requests/widget/_open.html.haml | 4 ++-- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index dcd35dc6282..20218775659 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -63,13 +63,6 @@ module MergeRequestsHelper @mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing end - def mr_issues_mentioned_but_not_closing_message - verb = mr_issues_mentioned_but_not_closing.size > 1 ? 'are' : 'is' - issue_text = 'issue'.pluralize(mr_issues_mentioned_but_not_closing.size) - - "The following #{issue_text} #{verb} mentioned but not being closed:" - end - def mr_change_branches_path(merge_request) new_namespace_project_merge_request_path( @project.namespace, @project, diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index 1f794cad663..ebea48a4321 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -33,8 +33,8 @@ - if mr_issues_mentioned_but_not_closing.present? .mr-widget-footer %span - %i.fa.fa-warning - = mr_issues_mentioned_but_not_closing_message + %i.fa.fa-info-circle + #{"Issue".pluralize(mr_issues_mentioned_but_not_closing.size)} mentioned but not being closed: = succeed '.' do != markdown issues_sentence(mr_issues_mentioned_but_not_closing), pipeline: :gfm, author: @merge_request.author = mr_assign_issues_link -- cgit v1.2.1 From 0e76daf3da35920d10053dc4d8707e1b6aa7c913 Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Fri, 25 Nov 2016 06:49:56 +1000 Subject: only look for issues mentioned on description on MergeRequest#issues_mentioned_but_not_closing --- app/models/merge_request.rb | 6 ++++-- spec/models/merge_request_spec.rb | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 6f660bf0e77..b8c139d01e2 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -570,18 +570,20 @@ class MergeRequest < ActiveRecord::Base def issues_mentioned_but_not_closing(current_user = self.author) issues = [] + closing_issues = [] if target_branch == project.default_branch messages = [description] - messages.concat(commits.map(&:safe_message)) if merge_request_diff ext = Gitlab::ReferenceExtractor.new(project, current_user) ext.analyze(messages.join("\n")) issues = ext.issues + closing_issues = Gitlab::ClosingIssueExtractor.new(project, current_user). + closed_by_message(messages.join("\n")) end - issues - closes_issues + issues - closing_issues end def target_project_path diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 688c78fb404..b2c26874552 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -294,7 +294,7 @@ describe MergeRequest, models: true do end end - it 'detects issues mentioned but not closed' do + it 'detects issues mentioned in description but not closed' do mentioned_issue = create(:issue, project: subject.project) subject.description = "Is related to #{mentioned_issue.to_reference}" -- cgit v1.2.1 From b4764a8dd22c29b7edc3065af9a99713aa5708d3 Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Fri, 25 Nov 2016 07:01:05 +1000 Subject: shorter lines on MergeRequest#merge_commit_message --- app/models/merge_request.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index b8c139d01e2..5a5b8bd6010 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -631,10 +631,17 @@ class MergeRequest < ActiveRecord::Base end def merge_commit_message + closes_issues_references = closes_issues.map do |issue| + issue.to_reference(target_project) + end + message = "Merge branch '#{source_branch}' into '#{target_branch}'\n\n" message << "#{title}\n\n" - mr_closes_issues = closes_issues - message << "Closed Issues: #{mr_closes_issues.map { |issue| issue.to_reference(target_project) }.join(", ")}\n\n" if mr_closes_issues.present? + + if closes_issues_references.present? + message << "Closed Issues: #{closes_issues_references.join(", ")}\n\n" + end + message << "See merge request #{to_reference}" message -- cgit v1.2.1 From 00a842eacc4b2ff1514b258fbf08e4017f3be447 Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Fri, 25 Nov 2016 19:49:56 +1000 Subject: Add toggle links for using default message and description on change merge commit message container --- .../merge_requests/widget/open/_accept.html.haml | 1 + .../shared/_commit_message_container.html.haml | 28 ++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 435fe835fae..66096ff7476 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -41,6 +41,7 @@ Modify commit message .js-toggle-content.hide.prepend-top-default = render 'shared/commit_message_container', params: params, + description: @merge_request.description, text: @merge_request.merge_commit_message, rows: 14, hint: true diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml index 0a38327baa2..adee374413f 100644 --- a/app/views/shared/_commit_message_container.html.haml +++ b/app/views/shared/_commit_message_container.html.haml @@ -14,3 +14,31 @@ %p.hint Try to keep the first line under 52 characters and the others under 72. + - if local_assigns[:description] + %p.hint.use-description-hint + = link_to "#", class: "use-description-link" do + Use Merge Request description as merge commit message + %p.hint.use-default-message-hint.hide + = link_to "#", class: "use-default-message-link" do + Use default Gitlab merge commit message + + + :javascript + $('.use-description-link').on('click', function(e) { + e.preventDefault(); + + $('.use-description-hint').hide(); + $('.use-default-message-hint').show(); + $('.js-commit-message').val("#{escape_javascript local_assigns[:description]}"); + }); + + $('.use-default-message-link').on('click', function(e) { + e.preventDefault(); + + var defaultMessage = "#{escape_javascript (params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder])}"; + + $('.use-description-hint').show(); + $('.use-default-message-hint').hide(); + $('.js-commit-message').val(defaultMessage); + }); + -- cgit v1.2.1 From 1a72dc2486601eadec03122f8124d3b553df3571 Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Sun, 27 Nov 2016 20:40:56 +1000 Subject: keep branch being merged, MR title and MR reference in merge commit message when using description --- app/views/shared/_commit_message_container.html.haml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml index adee374413f..706eef5a331 100644 --- a/app/views/shared/_commit_message_container.html.haml +++ b/app/views/shared/_commit_message_container.html.haml @@ -27,15 +27,21 @@ $('.use-description-link').on('click', function(e) { e.preventDefault(); + var message = "Merge branch '#{j @merge_request.source_branch}' into '#{j @merge_request.target_branch}'\n\n" + message = message + "#{j @merge_request.title}\n\n" + message = message + "#{j local_assigns[:description]}\n\n"; + message = message + "See merge request #{j @merge_request.to_reference}" + + $('.use-description-hint').hide(); $('.use-default-message-hint').show(); - $('.js-commit-message').val("#{escape_javascript local_assigns[:description]}"); + $('.js-commit-message').val(message) }); $('.use-default-message-link').on('click', function(e) { e.preventDefault(); - var defaultMessage = "#{escape_javascript (params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder])}"; + var defaultMessage = "#{j (params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder])}"; $('.use-description-hint').show(); $('.use-default-message-hint').hide(); -- cgit v1.2.1 From d1980ef9c8c059fb9d4be1a8339dea05e9a442f1 Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Mon, 28 Nov 2016 07:37:57 +1000 Subject: only render MR description toggle javascript if description is available --- .../shared/_commit_message_container.html.haml | 39 +++++++++++----------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml index 706eef5a331..a151731ba0a 100644 --- a/app/views/shared/_commit_message_container.html.haml +++ b/app/views/shared/_commit_message_container.html.haml @@ -14,7 +14,7 @@ %p.hint Try to keep the first line under 52 characters and the others under 72. - - if local_assigns[:description] + - if local_assigns[:description] %p.hint.use-description-hint = link_to "#", class: "use-description-link" do Use Merge Request description as merge commit message @@ -22,29 +22,28 @@ = link_to "#", class: "use-default-message-link" do Use default Gitlab merge commit message + :javascript + $('.use-description-link').on('click', function(e) { + e.preventDefault(); - :javascript - $('.use-description-link').on('click', function(e) { - e.preventDefault(); + var message = "Merge branch '#{j @merge_request.source_branch}' into '#{j @merge_request.target_branch}'\n\n" + message = message + "#{j @merge_request.title}\n\n" + message = message + "#{j local_assigns[:description]}\n\n"; + message = message + "See merge request #{j @merge_request.to_reference}" - var message = "Merge branch '#{j @merge_request.source_branch}' into '#{j @merge_request.target_branch}'\n\n" - message = message + "#{j @merge_request.title}\n\n" - message = message + "#{j local_assigns[:description]}\n\n"; - message = message + "See merge request #{j @merge_request.to_reference}" + $('.use-description-hint').hide(); + $('.use-default-message-hint').show(); + $('.js-commit-message').val(message) + }); - $('.use-description-hint').hide(); - $('.use-default-message-hint').show(); - $('.js-commit-message').val(message) - }); + $('.use-default-message-link').on('click', function(e) { + e.preventDefault(); - $('.use-default-message-link').on('click', function(e) { - e.preventDefault(); + var defaultMessage = "#{j (params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder])}"; - var defaultMessage = "#{j (params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder])}"; - - $('.use-description-hint').show(); - $('.use-default-message-hint').hide(); - $('.js-commit-message').val(defaultMessage); - }); + $('.use-description-hint').show(); + $('.use-default-message-hint').hide(); + $('.js-commit-message').val(defaultMessage); + }); -- cgit v1.2.1 From 78f221d12e28b6ea10f8fbc7f83fa39caaad05d0 Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Mon, 28 Nov 2016 19:02:37 +1000 Subject: describe #closes_issues and describe # #issues_mentioned_but_not_closing on merge_request_spec.rb --- spec/models/merge_request_spec.rb | 38 ++++++++++++++------------------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index b2c26874552..9ca60e27900 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -252,7 +252,7 @@ describe MergeRequest, models: true do end end - describe 'detection of issues' do + describe '#closes_issues' do let(:issue0) { create :issue, project: subject.project } let(:issue1) { create :issue, project: subject.project } @@ -265,38 +265,28 @@ describe MergeRequest, models: true do allow(subject).to receive(:commits).and_return([commit0, commit1, commit2]) end - describe 'detection of issues to be closed' do - it 'accesses the set of issues that will be closed on acceptance' do - allow(subject.project).to receive(:default_branch). - and_return(subject.target_branch) - - closed = subject.closes_issues - - expect(closed).to include(issue0, issue1) - end - - it 'only lists issues as to be closed if it targets the default branch' do - allow(subject.project).to receive(:default_branch).and_return('master') - subject.target_branch = 'something-else' - - expect(subject.closes_issues).to be_empty - end + it 'accesses the set of issues that will be closed on acceptance' do + allow(subject.project).to receive(:default_branch). + and_return(subject.target_branch) - it 'detects issues mentioned in the description' do - issue2 = create(:issue, project: subject.project) + closed = subject.closes_issues - subject.description = "Closes #{issue2.to_reference}" + expect(closed).to include(issue0, issue1) + end - allow(subject.project).to receive(:default_branch). - and_return(subject.target_branch) + it 'only lists issues as to be closed if it targets the default branch' do + allow(subject.project).to receive(:default_branch).and_return('master') + subject.target_branch = 'something-else' - expect(subject.closes_issues).to include(issue2) - end + expect(subject.closes_issues).to be_empty end + end + describe '#issues_mentioned_but_not_closing' do it 'detects issues mentioned in description but not closed' do mentioned_issue = create(:issue, project: subject.project) + subject.project.team << [subject.author, :developer] subject.description = "Is related to #{mentioned_issue.to_reference}" allow(subject.project).to receive(:default_branch). -- cgit v1.2.1 From 512c870ed46b5e441fd0b8daa8bd9cab449f7ac0 Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Mon, 28 Nov 2016 19:06:18 +1000 Subject: Remove unnecessary code from MergeRequest#issues_mentioned_but_not_closing --- app/models/merge_request.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 5a5b8bd6010..dba0c463fd6 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -573,14 +573,12 @@ class MergeRequest < ActiveRecord::Base closing_issues = [] if target_branch == project.default_branch - messages = [description] - ext = Gitlab::ReferenceExtractor.new(project, current_user) - ext.analyze(messages.join("\n")) + ext.analyze(description) issues = ext.issues closing_issues = Gitlab::ClosingIssueExtractor.new(project, current_user). - closed_by_message(messages.join("\n")) + closed_by_message(description) end issues - closing_issues -- cgit v1.2.1 From 58609f842e1344579ed14745bb6bcb365059166f Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Mon, 28 Nov 2016 19:48:55 +1000 Subject: backend completely drives creation of merge commit message --- app/models/merge_request.rb | 3 +- .../projects/merge_requests/widget/_open.html.haml | 3 +- .../merge_requests/widget/open/_accept.html.haml | 3 +- .../shared/_commit_message_container.html.haml | 40 ++++++++++------------ spec/models/merge_request_spec.rb | 14 ++++++++ 5 files changed, 37 insertions(+), 26 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index dba0c463fd6..2d7be2c2c7e 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -628,7 +628,7 @@ class MergeRequest < ActiveRecord::Base self.target_project.repository.branch_names.include?(self.target_branch) end - def merge_commit_message + def merge_commit_message(include_description: false) closes_issues_references = closes_issues.map do |issue| issue.to_reference(target_project) end @@ -640,6 +640,7 @@ class MergeRequest < ActiveRecord::Base message << "Closed Issues: #{closes_issues_references.join(", ")}\n\n" end + message << "#{description}\n\n" if include_description && description.present? message << "See merge request #{to_reference}" message diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index ebea48a4321..bf1e49c98ce 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -37,12 +37,11 @@ #{"Issue".pluralize(mr_issues_mentioned_but_not_closing.size)} mentioned but not being closed: = succeed '.' do != markdown issues_sentence(mr_issues_mentioned_but_not_closing), pipeline: :gfm, author: @merge_request.author - = mr_assign_issues_link - if mr_closes_issues.present? .mr-widget-footer %span - %i.fa.fa-check + = icon('check') Accepting this merge request will close #{"issue".pluralize(mr_closes_issues.size)} = succeed '.' do != markdown issues_sentence(mr_closes_issues), pipeline: :gfm, author: @merge_request.author diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 66096ff7476..d6f7f23533c 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -41,7 +41,8 @@ Modify commit message .js-toggle-content.hide.prepend-top-default = render 'shared/commit_message_container', params: params, - description: @merge_request.description, + message_with_description: @merge_request.merge_commit_message(include_description: true), + message_without_description: @merge_request.merge_commit_message, text: @merge_request.merge_commit_message, rows: 14, hint: true diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml index a151731ba0a..3e0186983e4 100644 --- a/app/views/shared/_commit_message_container.html.haml +++ b/app/views/shared/_commit_message_container.html.haml @@ -8,42 +8,38 @@ = text_area_tag 'commit_message', (params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder]), class: 'form-control js-commit-message', placeholder: local_assigns[:placeholder], + data: local_assigns.slice(:message_with_description, :message_without_description), required: true, rows: (local_assigns[:rows] || 3), id: "commit_message-#{nonce}" - if local_assigns[:hint] %p.hint Try to keep the first line under 52 characters and the others under 72. - - if local_assigns[:description] - %p.hint.use-description-hint - = link_to "#", class: "use-description-link" do - Use Merge Request description as merge commit message - %p.hint.use-default-message-hint.hide - = link_to "#", class: "use-default-message-link" do - Use default Gitlab merge commit message + -if local_assigns.slice(:message_with_description, :message_without_description).present? + %p.hint.with-description-hint + = link_to "#", class: "with-description-link" do + Include description in commit message + %p.hint.without-description-hint.hide + = link_to "#", class: "without-description-link" do + Don't include description in commit message :javascript - $('.use-description-link').on('click', function(e) { + $('.with-description-link').on('click', function(e) { e.preventDefault(); - var message = "Merge branch '#{j @merge_request.source_branch}' into '#{j @merge_request.target_branch}'\n\n" - message = message + "#{j @merge_request.title}\n\n" - message = message + "#{j local_assigns[:description]}\n\n"; - message = message + "See merge request #{j @merge_request.to_reference}" + var textarea = $('.js-commit-message') - - $('.use-description-hint').hide(); - $('.use-default-message-hint').show(); - $('.js-commit-message').val(message) + textarea.val(textarea.data('messageWithDescription')) + $('.with-description-hint').hide(); + $('.without-description-hint').show(); }); - $('.use-default-message-link').on('click', function(e) { + $('.without-description-link').on('click', function(e) { e.preventDefault(); - var defaultMessage = "#{j (params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder])}"; + var textarea = $('.js-commit-message') - $('.use-description-hint').show(); - $('.use-default-message-hint').hide(); - $('.js-commit-message').val(defaultMessage); + textarea.val(textarea.data('messageWithoutDescription')) + $('.with-description-hint').show(); + $('.without-description-hint').hide(); }); - diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 9ca60e27900..f74c89bba4d 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -440,6 +440,20 @@ describe MergeRequest, models: true do expect(request.merge_commit_message).not_to match("Title\n\n\n\n") end + + it 'includes its description in the body' do + request = build(:merge_request, description: 'By removing all code') + + expect(request.merge_commit_message(include_description: true)) + .to match("By removing all code\n\n") + end + + it 'does not includes its description in the body' do + request = build(:merge_request, description: 'By removing all code') + + expect(request.merge_commit_message) + .not_to match("By removing all code\n\n") + end end describe "#reset_merge_when_build_succeeds" do -- cgit v1.2.1 From e97c7100aed6fb4ca072c80a78b95d5f51805197 Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Mon, 28 Nov 2016 21:30:25 +1000 Subject: move javascript code from _commit_message_container view to javascripts/merge_request.js --- app/assets/javascripts/merge_request.js | 26 ++++++++++++++++++++++ .../shared/_commit_message_container.html.haml | 21 ----------------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 70f9a8d1955..309724071d2 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -27,6 +27,8 @@ // Prevent duplicate event bindings this.disableTaskList(); this.initMRBtnListeners(); + this.initMessageWithDescriptionListener(); + this.initMessageWithoutDescriptionListener(); if ($("a.btn-close").length) { this.initTaskList(); } @@ -108,6 +110,30 @@ // note so that we can re-use its form here }; + MergeRequest.prototype.initMessageWithDescriptionListener = function() { + return $('a.with-description-link').on('click', function(e) { + e.preventDefault(); + + var textarea = $('textarea.js-commit-message'); + + textarea.val(textarea.data('messageWithDescription')); + $('p.with-description-hint').hide(); + $('p.without-description-hint').show(); + }); + }; + + MergeRequest.prototype.initMessageWithoutDescriptionListener = function() { + return $('a.without-description-link').on('click', function(e) { + e.preventDefault(); + + var textarea = $('textarea.js-commit-message'); + + textarea.val(textarea.data('messageWithoutDescription')); + $('p.with-description-hint').show(); + $('p.without-description-hint').hide(); + }); + }; + return MergeRequest; })(); diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml index 3e0186983e4..803cbb47e55 100644 --- a/app/views/shared/_commit_message_container.html.haml +++ b/app/views/shared/_commit_message_container.html.haml @@ -22,24 +22,3 @@ %p.hint.without-description-hint.hide = link_to "#", class: "without-description-link" do Don't include description in commit message - - :javascript - $('.with-description-link').on('click', function(e) { - e.preventDefault(); - - var textarea = $('.js-commit-message') - - textarea.val(textarea.data('messageWithDescription')) - $('.with-description-hint').hide(); - $('.without-description-hint').show(); - }); - - $('.without-description-link').on('click', function(e) { - e.preventDefault(); - - var textarea = $('.js-commit-message') - - textarea.val(textarea.data('messageWithoutDescription')) - $('.with-description-hint').show(); - $('.without-description-hint').hide(); - }); -- cgit v1.2.1 From 4181528569a81004d4e64f3e9726fc653a322cc7 Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Mon, 28 Nov 2016 22:11:45 +1000 Subject: Better `Closes issues` text for MergeRequest#merge_commit_message --- app/models/merge_request.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 2d7be2c2c7e..48c30b08502 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -637,7 +637,8 @@ class MergeRequest < ActiveRecord::Base message << "#{title}\n\n" if closes_issues_references.present? - message << "Closed Issues: #{closes_issues_references.join(", ")}\n\n" + issue_text = 'issue'.pluralize(closes_issues_references.size) + message << "Closes #{issue_text} #{closes_issues_references.to_sentence}\n\n" end message << "#{description}\n\n" if include_description && description.present? -- cgit v1.2.1 From 9e321043c72322ae12aba230b49f9da326e66e56 Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Tue, 29 Nov 2016 07:10:59 +1000 Subject: extract duplicate logic into a variable on _commit_message_container --- app/views/shared/_commit_message_container.html.haml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml index 803cbb47e55..2b2da446d09 100644 --- a/app/views/shared/_commit_message_container.html.haml +++ b/app/views/shared/_commit_message_container.html.haml @@ -1,5 +1,6 @@ .form-group.commit_message-group - nonce = SecureRandom.hex + - descriptions = local_assigns.slice(:message_with_description, :message_without_description) = label_tag "commit_message-#{nonce}", class: 'control-label' do Commit message .col-sm-10 @@ -8,14 +9,14 @@ = text_area_tag 'commit_message', (params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder]), class: 'form-control js-commit-message', placeholder: local_assigns[:placeholder], - data: local_assigns.slice(:message_with_description, :message_without_description), + data: descriptions, required: true, rows: (local_assigns[:rows] || 3), id: "commit_message-#{nonce}" - if local_assigns[:hint] %p.hint Try to keep the first line under 52 characters and the others under 72. - -if local_assigns.slice(:message_with_description, :message_without_description).present? + - if descriptions.present? %p.hint.with-description-hint = link_to "#", class: "with-description-link" do Include description in commit message -- cgit v1.2.1 From fedba9fc2568ec784eddbe6aff0fc9ca5f95b116 Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Tue, 29 Nov 2016 07:29:15 +1000 Subject: add mentioned but not closed message to the same line as closes issueswq --- .../projects/merge_requests/widget/_open.html.haml | 27 +++++++++++----------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index bf1e49c98ce..4aae3ba63a9 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -30,19 +30,20 @@ - elsif @merge_request.can_be_merged? || resolved_conflicts = render 'projects/merge_requests/widget/open/accept' - - if mr_issues_mentioned_but_not_closing.present? - .mr-widget-footer - %span - %i.fa.fa-info-circle - #{"Issue".pluralize(mr_issues_mentioned_but_not_closing.size)} mentioned but not being closed: - = succeed '.' do - != markdown issues_sentence(mr_issues_mentioned_but_not_closing), pipeline: :gfm, author: @merge_request.author - - - if mr_closes_issues.present? + - if mr_closes_issues.present? || mr_issues_mentioned_but_not_closing .mr-widget-footer %span = icon('check') - Accepting this merge request will close #{"issue".pluralize(mr_closes_issues.size)} - = succeed '.' do - != markdown issues_sentence(mr_closes_issues), pipeline: :gfm, author: @merge_request.author - = mr_assign_issues_link + - if mr_closes_issues.present? + Accepting this merge request will close #{"issue".pluralize(mr_closes_issues.size)} + = succeed '.' do + != markdown issues_sentence(mr_closes_issues), pipeline: :gfm, author: @merge_request.author + = mr_assign_issues_link + - if mr_issues_mentioned_but_not_closing.present? + #{"Issue".pluralize(mr_issues_mentioned_but_not_closing.size)} + = succeed '' do + != markdown issues_sentence(mr_issues_mentioned_but_not_closing), pipeline: :gfm, author: @merge_request.author + = succeed '' do + mentioned but not closed. + + -- cgit v1.2.1 From 943ef94912410dd028626711c97cd1ae881a5e4c Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Tue, 29 Nov 2016 07:30:55 +1000 Subject: better text for mentioned but not closed --- app/views/projects/merge_requests/widget/_open.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index 4aae3ba63a9..695359a48a3 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -44,6 +44,6 @@ = succeed '' do != markdown issues_sentence(mr_issues_mentioned_but_not_closing), pipeline: :gfm, author: @merge_request.author = succeed '' do - mentioned but not closed. + mentioned but will not closed. -- cgit v1.2.1 From 99dd58ec557779eadd83aa597d8c16996be60df1 Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Mon, 5 Dec 2016 20:46:43 +1000 Subject: Unify commit message listeners in one function --- app/assets/javascripts/merge_request.js | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 309724071d2..194a27ae22b 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -27,8 +27,7 @@ // Prevent duplicate event bindings this.disableTaskList(); this.initMRBtnListeners(); - this.initMessageWithDescriptionListener(); - this.initMessageWithoutDescriptionListener(); + this.initCommitMessageListeners(); if ($("a.btn-close").length) { this.initTaskList(); } @@ -110,24 +109,20 @@ // note so that we can re-use its form here }; - MergeRequest.prototype.initMessageWithDescriptionListener = function() { - return $('a.with-description-link').on('click', function(e) { - e.preventDefault(); + MergeRequest.prototype.initCommitMessageListeners = function() { + var textarea = $('textarea.js-commit-message'); - var textarea = $('textarea.js-commit-message'); + $('a.with-description-link').on('click', function(e) { + e.preventDefault(); textarea.val(textarea.data('messageWithDescription')); $('p.with-description-hint').hide(); $('p.without-description-hint').show(); }); - }; - MergeRequest.prototype.initMessageWithoutDescriptionListener = function() { - return $('a.without-description-link').on('click', function(e) { + $('a.without-description-link').on('click', function(e) { e.preventDefault(); - var textarea = $('textarea.js-commit-message'); - textarea.val(textarea.data('messageWithoutDescription')); $('p.with-description-hint').show(); $('p.without-description-hint').hide(); -- cgit v1.2.1 From 603ef5d49ed453cbb47b68d3af078529c6b834a1 Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Mon, 5 Dec 2016 21:29:17 +1000 Subject: Show either description or closes issues references on MergeRequest#merge_commit_message so closes issues references are not duplicated --- app/models/merge_request.rb | 14 ++++++++------ app/views/projects/merge_requests/widget/_open.html.haml | 8 +++----- spec/models/merge_request_spec.rb | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 48c30b08502..da293c3738f 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -633,18 +633,20 @@ class MergeRequest < ActiveRecord::Base issue.to_reference(target_project) end - message = "Merge branch '#{source_branch}' into '#{target_branch}'\n\n" - message << "#{title}\n\n" + message = [ + "Merge branch '#{source_branch}' into '#{target_branch}'", + title + ] - if closes_issues_references.present? + if !include_description && closes_issues_references.present? issue_text = 'issue'.pluralize(closes_issues_references.size) - message << "Closes #{issue_text} #{closes_issues_references.to_sentence}\n\n" + message << "Closes #{issue_text} #{closes_issues_references.to_sentence}" end - message << "#{description}\n\n" if include_description && description.present? + message << "#{description}" if include_description && description.present? message << "See merge request #{to_reference}" - message + message.join("\n\n") end def reset_merge_when_build_succeeds diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index 695359a48a3..f4aa1609a1b 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -30,7 +30,7 @@ - elsif @merge_request.can_be_merged? || resolved_conflicts = render 'projects/merge_requests/widget/open/accept' - - if mr_closes_issues.present? || mr_issues_mentioned_but_not_closing + - if mr_closes_issues.present? || mr_issues_mentioned_but_not_closing.present? .mr-widget-footer %span = icon('check') @@ -41,9 +41,7 @@ = mr_assign_issues_link - if mr_issues_mentioned_but_not_closing.present? #{"Issue".pluralize(mr_issues_mentioned_but_not_closing.size)} - = succeed '' do - != markdown issues_sentence(mr_issues_mentioned_but_not_closing), pipeline: :gfm, author: @merge_request.author - = succeed '' do - mentioned but will not closed. + != markdown issues_sentence(mr_issues_mentioned_but_not_closing), pipeline: :gfm, author: @merge_request.author + #{mr_issues_mentioned_but_not_closing.size > 1 ? 'are' : 'is'} mentioned but will not closed. diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index f74c89bba4d..1e9790cf644 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -425,7 +425,7 @@ describe MergeRequest, models: true do and_return(subject.target_branch) expect(subject.merge_commit_message) - .to match("Closed Issues: #{issue.to_reference}") + .to match("Closes issue #{issue.to_reference}") end it 'includes its reference in the body' do -- cgit v1.2.1 From 5d478e5ceec3db9c38ef2ab1fafd7235fe3bb244 Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Mon, 5 Dec 2016 21:43:40 +1000 Subject: add js prefix to classes used to toggle description on commit message in merge request --- app/assets/javascripts/merge_request.js | 12 ++++++------ app/views/shared/_commit_message_container.html.haml | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 194a27ae22b..462dbc44e9f 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -112,20 +112,20 @@ MergeRequest.prototype.initCommitMessageListeners = function() { var textarea = $('textarea.js-commit-message'); - $('a.with-description-link').on('click', function(e) { + $('a.js-with-description-link').on('click', function(e) { e.preventDefault(); textarea.val(textarea.data('messageWithDescription')); - $('p.with-description-hint').hide(); - $('p.without-description-hint').show(); + $('p.js-with-description-hint').hide(); + $('p.js-without-description-hint').show(); }); - $('a.without-description-link').on('click', function(e) { + $('a.js-without-description-link').on('click', function(e) { e.preventDefault(); textarea.val(textarea.data('messageWithoutDescription')); - $('p.with-description-hint').show(); - $('p.without-description-hint').hide(); + $('p.js-with-description-hint').show(); + $('p.js-without-description-hint').hide(); }); }; diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml index 2b2da446d09..c196bc06b17 100644 --- a/app/views/shared/_commit_message_container.html.haml +++ b/app/views/shared/_commit_message_container.html.haml @@ -17,9 +17,9 @@ Try to keep the first line under 52 characters and the others under 72. - if descriptions.present? - %p.hint.with-description-hint - = link_to "#", class: "with-description-link" do + %p.hint.js-with-description-hint + = link_to "#", class: "js-with-description-link" do Include description in commit message - %p.hint.without-description-hint.hide - = link_to "#", class: "without-description-link" do + %p.hint.js-without-description-hint.hide + = link_to "#", class: "js-without-description-link" do Don't include description in commit message -- cgit v1.2.1 From 7d7ae494d476f2e0588740612846da4284b3da0c Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Mon, 5 Dec 2016 21:44:29 +1000 Subject: add guard clause to MergeRequest#issues_mentioned_but_not_closing --- app/models/merge_request.rb | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index da293c3738f..acaf14a12e9 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -569,17 +569,14 @@ class MergeRequest < ActiveRecord::Base end def issues_mentioned_but_not_closing(current_user = self.author) - issues = [] - closing_issues = [] + return [] unless target_branch == project.default_branch - if target_branch == project.default_branch - ext = Gitlab::ReferenceExtractor.new(project, current_user) - ext.analyze(description) + ext = Gitlab::ReferenceExtractor.new(project, current_user) + ext.analyze(description) - issues = ext.issues - closing_issues = Gitlab::ClosingIssueExtractor.new(project, current_user). - closed_by_message(description) - end + issues = ext.issues + closing_issues = Gitlab::ClosingIssueExtractor.new(project, current_user). + closed_by_message(description) issues - closing_issues end -- cgit v1.2.1 From 5311d7f0aec15768e78924b8fb7cb17cf487baa5 Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Mon, 5 Dec 2016 22:52:47 +1000 Subject: Add feature spec to verify all 4 different states of closing issues message on Merge Request show page. --- spec/features/merge_requests/closes_issues_spec.rb | 57 ++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 spec/features/merge_requests/closes_issues_spec.rb diff --git a/spec/features/merge_requests/closes_issues_spec.rb b/spec/features/merge_requests/closes_issues_spec.rb new file mode 100644 index 00000000000..cfa94e13df2 --- /dev/null +++ b/spec/features/merge_requests/closes_issues_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +feature 'Merge Commit Description', feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:issue_1) { create(:issue, project: project)} + let(:issue_2) { create(:issue, project: project)} + let(:merge_request) do + create( + :merge_request, + :simple, + source_project: project, + description: merge_request_description + ) + end + let(:merge_request_description) { 'Merge Request Description' } + + before do + project.team << [user, :master] + + login_as user + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + + click_link 'Modify commit message' + end + + context 'not closing or mentioning any issue' do + it 'does not display closing issue message' do + expect(page).not_to have_css('.mr-widget-footer') + end + end + + context 'closing issues but not mentioning any other issue' do + let(:merge_request_description) { "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}" } + + it 'does not display closing issue message' do + expect(page).to have_content("Accepting this merge request will close issues #{issue_1.to_reference} and #{issue_2.to_reference}") + end + end + + context 'mentioning issues but not closing them' do + let(:merge_request_description) { "Description\n\nRefers to #{issue_1.to_reference} and #{issue_2.to_reference}" } + + it 'does not display closing issue message' do + expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but will not closed.") + end + end + + context 'closing some issues and mentioning, but not closing, others' do + let(:merge_request_description) { "Description\n\ncloses #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" } + + it 'does not display closing issue message' do + expect(page).to have_content("Accepting this merge request will close issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not closed.") + end + end +end -- cgit v1.2.1 From 4525cdaec3f9af4a38711137b5fcdcff68cd7d1a Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Thu, 8 Dec 2016 21:39:16 +1000 Subject: add eslint disable prefix for prefer-arrow-callback rule on header of merge_request.js file --- app/assets/javascripts/merge_request.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 462dbc44e9f..244c2f6746c 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, padded-blocks, max-len, prefer-arrow-callback */ /* global MergeRequestTabs */ /*= require jquery.waitforimages */ -- cgit v1.2.1 From f3378630c15d43080d2bda03f5165653092a660b Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Sat, 10 Dec 2016 21:58:15 +1000 Subject: add feature specs to test toggling of merge commit message between message with description and without --- spec/features/merge_requests/closes_issues_spec.rb | 4 +- .../merge_commit_message_toggle_spec.rb | 74 ++++++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 spec/features/merge_requests/merge_commit_message_toggle_spec.rb diff --git a/spec/features/merge_requests/closes_issues_spec.rb b/spec/features/merge_requests/closes_issues_spec.rb index cfa94e13df2..dc32c8f7373 100644 --- a/spec/features/merge_requests/closes_issues_spec.rb +++ b/spec/features/merge_requests/closes_issues_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Merge Commit Description', feature: true do +feature 'Merge Request closing issues message', feature: true do let(:user) { create(:user) } let(:project) { create(:project, :public) } let(:issue_1) { create(:issue, project: project)} @@ -21,8 +21,6 @@ feature 'Merge Commit Description', feature: true do login_as user visit namespace_project_merge_request_path(project.namespace, project, merge_request) - - click_link 'Modify commit message' end context 'not closing or mentioning any issue' do diff --git a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb new file mode 100644 index 00000000000..2c78234bd0f --- /dev/null +++ b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +feature 'Clicking toggle commit message link', feature: true, js: true do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:issue_1) { create(:issue, project: project)} + let(:issue_2) { create(:issue, project: project)} + let(:merge_request) do + create( + :merge_request, + :simple, + source_project: project, + description: "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}" + ) + end + let(:textbox) { page.find(:css, '.js-commit-message', visible: false) } + let(:include_link) { page.find(:css, '.js-with-description-link', visible: false) } + let(:do_not_include_link) { page.find(:css, '.js-without-description-link', visible: false) } + let(:default_message) do + [ + "Merge branch 'feature' into 'master'", + merge_request.title, + "Closes issues #{issue_1.to_reference} and #{issue_2.to_reference}", + "See merge request #{merge_request.to_reference}" + ].join("\n\n") + end + let(:message_with_description) do + [ + "Merge branch 'feature' into 'master'", + merge_request.title, + merge_request.description, + "See merge request #{merge_request.to_reference}" + ].join("\n\n") + end + + before do + project.team << [user, :master] + + login_as user + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + + expect(textbox).not_to be_visible + click_link "Modify commit message" + expect(textbox).to be_visible + end + + it "toggles commit message between message with description and without description " do + expect(textbox.value).to eq(default_message) + + click_link "Include description in commit message" + + expect(textbox.value).to eq(message_with_description) + + click_link "Don't include description in commit message" + + expect(textbox.value).to eq(default_message) + end + + it "toggles link between 'Include description' and 'Don't include description'" do + expect(include_link).to be_visible + expect(do_not_include_link).not_to be_visible + + click_link "Include description in commit message" + + expect(include_link).not_to be_visible + expect(do_not_include_link).to be_visible + + click_link "Don't include description in commit message" + + expect(include_link).to be_visible + expect(do_not_include_link).not_to be_visible + end +end -- cgit v1.2.1 From 3e3d6b53dcdc50bbe45c4b3ff43faf3d2728f72c Mon Sep 17 00:00:00 2001 From: Gabriel Gizotti <gabriel@gizotti.com> Date: Tue, 13 Dec 2016 23:00:00 +1000 Subject: Change closes issues reference text on MergeRequest#merge_commit_message to match existing text generated by the system --- app/models/merge_request.rb | 3 +-- spec/features/merge_requests/merge_commit_message_toggle_spec.rb | 2 +- spec/models/merge_request_spec.rb | 6 +++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index acaf14a12e9..b7c775777c7 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -636,8 +636,7 @@ class MergeRequest < ActiveRecord::Base ] if !include_description && closes_issues_references.present? - issue_text = 'issue'.pluralize(closes_issues_references.size) - message << "Closes #{issue_text} #{closes_issues_references.to_sentence}" + message << "Closes #{closes_issues_references.to_sentence}" end message << "#{description}" if include_description && description.present? diff --git a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb index 2c78234bd0f..3dbe26cddb0 100644 --- a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb +++ b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb @@ -20,7 +20,7 @@ feature 'Clicking toggle commit message link', feature: true, js: true do [ "Merge branch 'feature' into 'master'", merge_request.title, - "Closes issues #{issue_1.to_reference} and #{issue_2.to_reference}", + "Closes #{issue_1.to_reference} and #{issue_2.to_reference}", "See merge request #{merge_request.to_reference}" ].join("\n\n") end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 1e9790cf644..5da00a8636a 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -416,16 +416,16 @@ describe MergeRequest, models: true do end it 'includes its closed issues in the body' do - issue = create(:issue, project: subject.project) + issue = create(:issue, project: subject.project) subject.project.team << [subject.author, :developer] - subject.description = "Closes #{issue.to_reference}" + subject.description = "This issue Closes #{issue.to_reference}" allow(subject.project).to receive(:default_branch). and_return(subject.target_branch) expect(subject.merge_commit_message) - .to match("Closes issue #{issue.to_reference}") + .to match("Closes #{issue.to_reference}") end it 'includes its reference in the body' do -- cgit v1.2.1 From 1d0ccec6dd8375b751846f69bb170ebd11e9a391 Mon Sep 17 00:00:00 2001 From: Timothy Andrew <mail@timothyandrew.net> Date: Tue, 22 Nov 2016 14:23:53 +0530 Subject: Add a `scopes` column to the `personal_access_tokens` table --- app/models/personal_access_token.rb | 2 ++ ..._add_column_scopes_to_personal_access_tokens.rb | 36 ++++++++++++++++++++ ...al_access_tokens_default_back_to_empty_array.rb | 39 ++++++++++++++++++++++ db/schema.rb | 1 + spec/factories/personal_access_tokens.rb | 1 + 5 files changed, 79 insertions(+) create mode 100644 db/migrate/20160823083941_add_column_scopes_to_personal_access_tokens.rb create mode 100644 db/migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index c4b095e0c04..10a34c42fd8 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -2,6 +2,8 @@ class PersonalAccessToken < ActiveRecord::Base include TokenAuthenticatable add_authentication_token_field :token + serialize :scopes, Array + belongs_to :user scope :active, -> { where(revoked: false).where("expires_at >= NOW() OR expires_at IS NULL") } diff --git a/db/migrate/20160823083941_add_column_scopes_to_personal_access_tokens.rb b/db/migrate/20160823083941_add_column_scopes_to_personal_access_tokens.rb new file mode 100644 index 00000000000..ab7f0365603 --- /dev/null +++ b/db/migrate/20160823083941_add_column_scopes_to_personal_access_tokens.rb @@ -0,0 +1,36 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddColumnScopesToPersonalAccessTokens < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + disable_ddl_transaction! + + def up + # The default needs to be `[]`, but all existing access tokens need to have `scopes` set to `['api']`. + # It's easier to achieve this by adding the column with the `['api']` default, and then changing the default to + # `[]`. + add_column_with_default :personal_access_tokens, :scopes, :string, default: ['api'].to_yaml + end + + def down + remove_column :personal_access_tokens, :scopes + end +end diff --git a/db/migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb b/db/migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb new file mode 100644 index 00000000000..018cc3d4747 --- /dev/null +++ b/db/migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb @@ -0,0 +1,39 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class ChangePersonalAccessTokensDefaultBackToEmptyArray < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def up + # The default needs to be `[]`, but all existing access tokens need to have `scopes` set to `['api']`. + # It's easier to achieve this by adding the column with the `['api']` default, and then changing the default to + # `[]`. + change_column_default :personal_access_tokens, :scopes, [].to_yaml + end + + def down + # The default needs to be `[]`, but all existing access tokens need to have `scopes` set to `['api']`. + # It's easier to achieve this by adding the column with the `['api']` default, and then changing the default to + # `[]`. + change_column_default :personal_access_tokens, :scopes, ['api'].to_yaml + end +end diff --git a/db/schema.rb b/db/schema.rb index 67ff83d96d9..a1a22df0d53 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -854,6 +854,7 @@ ActiveRecord::Schema.define(version: 20161212142807) do t.datetime "expires_at" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "scopes", default: "--- []\n", null: false end add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree diff --git a/spec/factories/personal_access_tokens.rb b/spec/factories/personal_access_tokens.rb index da4c72bcb5b..811eab7e15b 100644 --- a/spec/factories/personal_access_tokens.rb +++ b/spec/factories/personal_access_tokens.rb @@ -5,5 +5,6 @@ FactoryGirl.define do name { FFaker::Product.brand } revoked false expires_at { 5.days.from_now } + scopes ['api'] end end -- cgit v1.2.1 From 6c809dfae84e702f7a49d3fac5725745264e0ff9 Mon Sep 17 00:00:00 2001 From: Timothy Andrew <mail@timothyandrew.net> Date: Tue, 22 Nov 2016 14:27:31 +0530 Subject: Allow creating personal access tokens / OAuth applications with scopes. --- app/assets/stylesheets/pages/profile.scss | 10 +++++++++ app/controllers/admin/applications_controller.rb | 6 ++++- app/controllers/concerns/oauth_applications.rb | 14 ++++++++++++ app/controllers/oauth/applications_controller.rb | 6 +++++ .../profiles/personal_access_tokens_controller.rb | 12 +++++----- app/views/admin/applications/_form.html.haml | 10 +++++++++ app/views/admin/applications/show.html.haml | 15 +++++++++++-- app/views/doorkeeper/applications/_form.html.haml | 9 ++++++++ app/views/doorkeeper/applications/show.html.haml | 15 ++++++++++++- .../personal_access_tokens/_form.html.haml | 22 ++++++++++++++++++ .../personal_access_tokens/index.html.haml | 17 +++----------- .../profiles/personal_access_tokens_spec.rb | 26 ++++++++++++++++++++++ 12 files changed, 138 insertions(+), 24 deletions(-) create mode 100644 app/controllers/concerns/oauth_applications.rb create mode 100644 app/views/profiles/personal_access_tokens/_form.html.haml diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 8a5b0e20a86..8b1976bd925 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -262,3 +262,13 @@ table.u2f-registrations { border-right: solid 1px transparent; } } + +.oauth-application-show { + .scope-name { + font-weight: 600; + } + + .scopes-list { + padding-left: 18px; + } +} \ No newline at end of file diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb index 471d24934a0..759044910bb 100644 --- a/app/controllers/admin/applications_controller.rb +++ b/app/controllers/admin/applications_controller.rb @@ -1,4 +1,6 @@ class Admin::ApplicationsController < Admin::ApplicationController + include OauthApplications + before_action :set_application, only: [:show, :edit, :update, :destroy] def index @@ -10,9 +12,11 @@ class Admin::ApplicationsController < Admin::ApplicationController def new @application = Doorkeeper::Application.new + @scopes = Doorkeeper.configuration.scopes end def edit + @scopes = Doorkeeper.configuration.scopes end def create @@ -47,6 +51,6 @@ class Admin::ApplicationsController < Admin::ApplicationController # Only allow a trusted parameter "white list" through. def application_params - params[:doorkeeper_application].permit(:name, :redirect_uri) + params[:doorkeeper_application].permit(:name, :redirect_uri, :scopes) end end diff --git a/app/controllers/concerns/oauth_applications.rb b/app/controllers/concerns/oauth_applications.rb new file mode 100644 index 00000000000..34ad43ededd --- /dev/null +++ b/app/controllers/concerns/oauth_applications.rb @@ -0,0 +1,14 @@ +module OauthApplications + extend ActiveSupport::Concern + + included do + before_action :prepare_scopes, only: [:create, :update] + end + + def prepare_scopes + scopes = params.dig(:doorkeeper_application, :scopes) + if scopes + params[:doorkeeper_application][:scopes] = scopes.join(' ') + end + end +end diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index 0f54dfa4efc..b5449a6c30e 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -2,6 +2,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController include Gitlab::CurrentSettings include Gitlab::GonHelper include PageLayoutHelper + include OauthApplications before_action :verify_user_oauth_applications_enabled before_action :authenticate_user! @@ -13,6 +14,10 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController set_index_vars end + def edit + @scopes = Doorkeeper.configuration.scopes + end + def create @application = Doorkeeper::Application.new(application_params) @@ -40,6 +45,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController @authorized_tokens = current_user.oauth_authorized_tokens @authorized_anonymous_tokens = @authorized_tokens.reject(&:application) @authorized_apps = @authorized_tokens.map(&:application).uniq.reject(&:nil?) + @scopes = Doorkeeper.configuration.scopes # Don't overwrite a value possibly set by `create` @application ||= Doorkeeper::Application.new diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb index 508b82a9a6c..6e007f17913 100644 --- a/app/controllers/profiles/personal_access_tokens_controller.rb +++ b/app/controllers/profiles/personal_access_tokens_controller.rb @@ -1,8 +1,6 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController - before_action :load_personal_access_tokens, only: :index - def index - @personal_access_token = current_user.personal_access_tokens.build + set_index_vars end def create @@ -12,7 +10,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController flash[:personal_access_token] = @personal_access_token.token redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created." else - load_personal_access_tokens + set_index_vars render :index end end @@ -32,10 +30,12 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController private def personal_access_token_params - params.require(:personal_access_token).permit(:name, :expires_at) + params.require(:personal_access_token).permit(:name, :expires_at, scopes: []) end - def load_personal_access_tokens + def set_index_vars + @personal_access_token ||= current_user.personal_access_tokens.build + @scopes = Gitlab::Auth::SCOPES @active_personal_access_tokens = current_user.personal_access_tokens.active.order(:expires_at) @inactive_personal_access_tokens = current_user.personal_access_tokens.inactive end diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml index 4aacbb8cd77..36d2f415a05 100644 --- a/app/views/admin/applications/_form.html.haml +++ b/app/views/admin/applications/_form.html.haml @@ -18,6 +18,16 @@ Use %code= Doorkeeper.configuration.native_redirect_uri for local tests + + .form-group + = f.label :scopes, class: 'col-sm-2 control-label' + .col-sm-10 + - @scopes.each do |scope| + %fieldset + = check_box_tag 'doorkeeper_application[scopes][]', scope, application.scopes.include?(scope), id: "doorkeeper_application_scopes_#{scope}" + = label_tag "doorkeeper_application_scopes_#{scope}", scope + %span= "(#{t(scope, scope: [:doorkeeper, :scopes])})" + .form-actions = f.submit 'Submit', class: "btn btn-save wide" = link_to "Cancel", admin_applications_path, class: "btn btn-default" diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml index 3eb9d61972b..3418dc96496 100644 --- a/app/views/admin/applications/show.html.haml +++ b/app/views/admin/applications/show.html.haml @@ -2,8 +2,7 @@ %h3.page-title Application: #{@application.name} - -.table-holder +.table-holder.oauth-application-show %table.table %tr %td @@ -23,6 +22,18 @@ - @application.redirect_uri.split.each do |uri| %div %span.monospace= uri + + - if @application.scopes.present? + %tr + %td + Scopes + %td + %ul.scopes-list.append-bottom-0 + - @application.scopes.each do |scope| + %li + %span.scope-name= scope + = "(#{t(scope, scope: [:doorkeeper, :scopes])})" + .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/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml index 5c98265727a..6fdb04077b6 100644 --- a/app/views/doorkeeper/applications/_form.html.haml +++ b/app/views/doorkeeper/applications/_form.html.haml @@ -17,5 +17,14 @@ %code= Doorkeeper.configuration.native_redirect_uri for local tests + .form-group + = f.label :scopes, class: 'label-light' + - @scopes.each do |scope| + %fieldset + = check_box_tag 'doorkeeper_application[scopes][]', scope, application.scopes.include?(scope), id: "doorkeeper_application_scopes_#{scope}" + = label_tag "doorkeeper_application_scopes_#{scope}", scope + %span= "(#{t(scope, scope: [:doorkeeper, :scopes])})" + + .prepend-top-default = f.submit 'Save application', class: "btn btn-create" diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml index 47442b78d48..a18e133c8de 100644 --- a/app/views/doorkeeper/applications/show.html.haml +++ b/app/views/doorkeeper/applications/show.html.haml @@ -2,7 +2,7 @@ %h3.page-title Application: #{@application.name} -.table-holder +.table-holder.oauth-application-show %table.table %tr %td @@ -22,6 +22,19 @@ - @application.redirect_uri.split.each do |uri| %div %span.monospace= uri + + - if @application.scopes.present? + %tr + %td + Scopes + %td + %ul.scopes-list.append-bottom-0 + - @application.scopes.each do |scope| + %li + %span.scope-name= scope + = "(#{t(scope, scope: [:doorkeeper, :scopes])})" + + .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/profiles/personal_access_tokens/_form.html.haml b/app/views/profiles/personal_access_tokens/_form.html.haml new file mode 100644 index 00000000000..6083fdaa31d --- /dev/null +++ b/app/views/profiles/personal_access_tokens/_form.html.haml @@ -0,0 +1,22 @@ += form_for [:profile, @personal_access_token], method: :post, html: { class: 'js-requires-input' } do |f| + + = form_errors(@personal_access_token) + + .form-group + = f.label :name, class: 'label-light' + = f.text_field :name, class: "form-control", required: true + + .form-group + = f.label :expires_at, class: 'label-light' + = f.text_field :expires_at, class: "datepicker form-control", required: false + + .form-group + = f.label :scopes, class: 'label-light' + - @scopes.each do |scope| + %fieldset + = check_box_tag 'personal_access_token[scopes][]', scope, @personal_access_token.scopes.include?(scope), id: "personal_access_token_scopes_#{scope}" + = label_tag "personal_access_token_scopes_#{scope}", scope + %span= "(#{t(scope, scope: [:doorkeeper, :scopes])})" + + .prepend-top-default + = f.submit 'Create Personal Access Token', class: "btn btn-create" diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index 05a2ea67aa2..39eef0f6baf 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -28,21 +28,8 @@ Add a Personal Access Token %p.profile-settings-content Pick a name for the application, and we'll give you a unique token. - = form_for [:profile, @personal_access_token], - method: :post, html: { class: 'js-requires-input' } do |f| - = form_errors(@personal_access_token) - - .form-group - = f.label :name, class: 'label-light' - = f.text_field :name, class: "form-control", required: true - - .form-group - = f.label :expires_at, class: 'label-light' - = f.text_field :expires_at, class: "datepicker form-control", required: false - - .prepend-top-default - = f.submit 'Create Personal Access Token', class: "btn btn-create" + = render "form" %hr @@ -56,6 +43,7 @@ %th Name %th Created %th Expires + %th Scopes %th %tbody - @active_personal_access_tokens.each do |token| @@ -67,6 +55,7 @@ = token.expires_at.to_date.to_s(:medium) - else %span.personal-access-tokens-never-expires-label Never + %td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>" %td= link_to "Revoke", revoke_profile_personal_access_token_path(token), method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this token? This action cannot be undone." } - else diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb index a85930c7543..0ffeeff0921 100644 --- a/spec/features/profiles/personal_access_tokens_spec.rb +++ b/spec/features/profiles/personal_access_tokens_spec.rb @@ -51,6 +51,32 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do expect(active_personal_access_tokens).to have_text(Date.today.next_month.at_beginning_of_month.to_s(:medium)) end + context "scopes" do + it "allows creation of a token with scopes" do + visit profile_personal_access_tokens_path + fill_in "Name", with: FFaker::Product.brand + + check "api" + check "read_user" + + expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1) + expect(created_personal_access_token).to eq(PersonalAccessToken.last.token) + expect(PersonalAccessToken.last.scopes).to match_array(['api', 'read_user']) + expect(active_personal_access_tokens).to have_text('api') + expect(active_personal_access_tokens).to have_text('read_user') + end + + it "allows creation of a token with no scopes" do + visit profile_personal_access_tokens_path + fill_in "Name", with: FFaker::Product.brand + + expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1) + expect(created_personal_access_token).to eq(PersonalAccessToken.last.token) + expect(PersonalAccessToken.last.scopes).to eq([]) + expect(active_personal_access_tokens).to have_text('no scopes') + end + end + context "when creation fails" do it "displays an error message" do disallow_personal_access_token_saves! -- cgit v1.2.1 From e0ef9dc83ebfe102aaf980495b14fd6a06a24fd1 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Fri, 16 Dec 2016 12:12:53 +0200 Subject: BB importer: Milestone importer --- lib/bitbucket/representation/issue.rb | 4 ++++ lib/gitlab/bitbucket_import/importer.rb | 2 ++ spec/lib/bitbucket/representation/issue_spec.rb | 6 ++++++ 3 files changed, 12 insertions(+) diff --git a/lib/bitbucket/representation/issue.rb b/lib/bitbucket/representation/issue.rb index ffe8a65d839..3af731753d1 100644 --- a/lib/bitbucket/representation/issue.rb +++ b/lib/bitbucket/representation/issue.rb @@ -27,6 +27,10 @@ module Bitbucket raw['title'] end + def milestone + raw.dig('milestone', 'name') + end + def created_at raw['created_on'] end diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 567f2b314aa..53c95ea4079 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -67,6 +67,7 @@ module Gitlab description += issue.description label_name = issue.kind + milestone = issue.milestone ? project.milestones.find_or_create_by(title: issue.milestone) : nil issue = project.issues.create!( iid: issue.iid, @@ -74,6 +75,7 @@ module Gitlab description: description, state: issue.state, author_id: gitlab_user_id(project, issue.author), + milestone: milestone, created_at: issue.created_at, updated_at: issue.updated_at ) diff --git a/spec/lib/bitbucket/representation/issue_spec.rb b/spec/lib/bitbucket/representation/issue_spec.rb index e1f3419c77e..9a195bebd31 100644 --- a/spec/lib/bitbucket/representation/issue_spec.rb +++ b/spec/lib/bitbucket/representation/issue_spec.rb @@ -9,6 +9,12 @@ describe Bitbucket::Representation::Issue do it { expect(described_class.new('kind' => 'bug').kind).to eq('bug') } end + describe '#milestone' do + it { expect(described_class.new({ 'milestone' => { 'name' => '1.0' } }).milestone).to eq('1.0') } + it { expect(described_class.new({}).milestone).to be_nil } + end + + describe '#author' do it { expect(described_class.new({ 'reporter' => { 'username' => 'Ben' } }).author).to eq('Ben') } it { expect(described_class.new({}).author).to be_nil } -- cgit v1.2.1 From 9f97fa4d9f4e86e8f1ff1db4621bcf81390936da Mon Sep 17 00:00:00 2001 From: Mark Fletcher <mark@gitlab.com> Date: Wed, 14 Dec 2016 20:45:39 +0000 Subject: Ensure issuable state changes only fire webhooks once * Webhooks for close and reopen events now fired in respective services only * Prevents generic 'update' webhooks firing too --- app/services/issuable_base_service.rb | 7 ++++++- ...9-2-webhooks-fired-for-issue-closed-and-reopened.yml | 4 ++++ spec/services/issues/update_service_spec.rb | 5 +++++ spec/services/merge_requests/update_service_spec.rb | 5 +++++ .../services/issuable_update_service_shared_examples.rb | 17 +++++++++++++++++ 5 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml create mode 100644 spec/support/services/issuable_update_service_shared_examples.rb diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index b5f63cc5a1a..ab3d2a9a0cd 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -184,7 +184,8 @@ class IssuableBaseService < BaseService old_labels = issuable.labels.to_a old_mentioned_users = issuable.mentioned_users.to_a - params[:label_ids] = process_label_ids(params, existing_label_ids: issuable.label_ids) + label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids) + params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids) if params.present? && update_issuable(issuable, params) # We do not touch as it will affect a update on updated_at field @@ -201,6 +202,10 @@ class IssuableBaseService < BaseService issuable end + def labels_changing?(old_label_ids, new_label_ids) + old_label_ids.sort != new_label_ids.sort + end + def change_state(issuable) case params.delete(:state_event) when 'reopen' diff --git a/changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml b/changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml new file mode 100644 index 00000000000..b12eab26b67 --- /dev/null +++ b/changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml @@ -0,0 +1,4 @@ +--- +title: Ensure issuable state changes only fire webhooks once +merge_request: +author: diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 500d224ff98..eafbea46905 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -376,5 +376,10 @@ describe Issues::UpdateService, services: true do let(:mentionable) { issue } include_examples 'updating mentions', Issues::UpdateService end + + include_examples 'issuable update service' do + let(:open_issuable) { issue } + let(:closed_issuable) { create(:closed_issue, project: project) } + end end end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 790ef765f3a..88c786947d3 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -320,5 +320,10 @@ describe MergeRequests::UpdateService, services: true do expect(issue_ids).to be_empty end end + + include_examples 'issuable update service' do + let(:open_issuable) { merge_request } + let(:closed_issuable) { create(:closed_merge_request, source_project: project) } + end end end diff --git a/spec/support/services/issuable_update_service_shared_examples.rb b/spec/support/services/issuable_update_service_shared_examples.rb new file mode 100644 index 00000000000..a3336755773 --- /dev/null +++ b/spec/support/services/issuable_update_service_shared_examples.rb @@ -0,0 +1,17 @@ +shared_examples 'issuable update service' do + context 'changing state' do + before { expect(project).to receive(:execute_hooks).once } + + context 'to reopened' do + it 'executes hooks only once' do + described_class.new(project, user, state_event: 'reopen').execute(closed_issuable) + end + end + + context 'to closed' do + it 'executes hooks only once' do + described_class.new(project, user, state_event: 'close').execute(open_issuable) + end + end + end +end -- cgit v1.2.1 From 2490f804cd4c12533fd6c70fc8014a06f2d19d47 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra <dimitriehoekstra@gmail.com> Date: Fri, 16 Dec 2016 11:59:10 +0100 Subject: Additional rounded label fixes --- app/assets/stylesheets/framework/variables.scss | 2 +- app/assets/stylesheets/pages/labels.scss | 4 +++- changelogs/unreleased/rounded-labels-fixes.yml | 4 ++++ 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/rounded-labels-fixes.yml diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 936aaf38254..d201a538195 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -436,7 +436,7 @@ $jq-ui-default-color: #777; $label-gray-bg: #f8fafc; $label-inverse-bg: #333; $label-remove-border: rgba(0, 0, 0, .1); -$label-border-radius: 14px; +$label-border-radius: 100px; /* * Lint diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 25c91203ff4..703a429d63c 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -98,7 +98,7 @@ } .label { - padding: 9px; + padding: 8px 9px 9px 9px; font-size: 14px; } } @@ -201,6 +201,8 @@ .label-remove { border-left: 1px solid $label-remove-border; z-index: 3; + border-radius: $label-border-radius; + padding: 6px 10px 6px 9px; } .btn { diff --git a/changelogs/unreleased/rounded-labels-fixes.yml b/changelogs/unreleased/rounded-labels-fixes.yml new file mode 100644 index 00000000000..e0fbc6e3b5a --- /dev/null +++ b/changelogs/unreleased/rounded-labels-fixes.yml @@ -0,0 +1,4 @@ +--- +title: Additional rounded label fixes +merge_request: +author: -- cgit v1.2.1 From 7fa06ed55d18af4d055041eb27d38fecf9b5548f Mon Sep 17 00:00:00 2001 From: Timothy Andrew <mail@timothyandrew.net> Date: Tue, 22 Nov 2016 14:34:23 +0530 Subject: Calls to the API are checked for scope. - Move the `Oauth2::AccessTokenValidationService` class to `AccessTokenValidationService`, since it is now being used for personal access token validation as well. - Each API endpoint declares the scopes it accepts (if any). Currently, the top level API module declares the `api` scope, and the `Users` API module declares the `read_user` scope (for GET requests). - Move the `find_user_by_private_token` from the API `Helpers` module to the `APIGuard` module, to avoid littering `Helpers` with more auth-related methods to support `find_user_by_private_token` --- app/services/access_token_validation_service.rb | 34 ++++++++++++ .../oauth2/access_token_validation_service.rb | 42 --------------- config/initializers/doorkeeper.rb | 4 +- config/locales/doorkeeper.en.yml | 1 + lib/api/api.rb | 2 + lib/api/api_guard.rb | 62 ++++++++++++++++------ lib/api/helpers.rb | 15 ++---- lib/api/users.rb | 5 +- lib/gitlab/auth.rb | 4 ++ spec/requests/api/doorkeeper_access_spec.rb | 2 +- spec/requests/api/helpers_spec.rb | 43 +++++++++------ .../access_token_validation_service_spec.rb | 42 +++++++++++++++ 12 files changed, 164 insertions(+), 92 deletions(-) create mode 100644 app/services/access_token_validation_service.rb delete mode 100644 app/services/oauth2/access_token_validation_service.rb create mode 100644 spec/services/access_token_validation_service_spec.rb diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb new file mode 100644 index 00000000000..69449f3a445 --- /dev/null +++ b/app/services/access_token_validation_service.rb @@ -0,0 +1,34 @@ +module AccessTokenValidationService + # Results: + VALID = :valid + EXPIRED = :expired + REVOKED = :revoked + INSUFFICIENT_SCOPE = :insufficient_scope + + class << self + def validate(token, scopes: []) + if token.expired? + return EXPIRED + + elsif token.revoked? + return REVOKED + + elsif !self.sufficient_scope?(token, scopes) + return INSUFFICIENT_SCOPE + + else + return VALID + end + end + + # True if the token's scope contains any of the required scopes. + def sufficient_scope?(token, required_scopes) + if required_scopes.blank? + true + else + # Check whether the token is allowed access to any of the required scopes. + Set.new(required_scopes).intersection(Set.new(token.scopes)).present? + end + end + end +end diff --git a/app/services/oauth2/access_token_validation_service.rb b/app/services/oauth2/access_token_validation_service.rb deleted file mode 100644 index 264fdccde8f..00000000000 --- a/app/services/oauth2/access_token_validation_service.rb +++ /dev/null @@ -1,42 +0,0 @@ -module Oauth2::AccessTokenValidationService - # Results: - VALID = :valid - EXPIRED = :expired - REVOKED = :revoked - INSUFFICIENT_SCOPE = :insufficient_scope - - class << self - def validate(token, scopes: []) - if token.expired? - return EXPIRED - - elsif token.revoked? - return REVOKED - - elsif !self.sufficient_scope?(token, scopes) - return INSUFFICIENT_SCOPE - - else - return VALID - end - end - - protected - - # True if the token's scope is a superset of required scopes, - # or the required scopes is empty. - def sufficient_scope?(token, scopes) - if scopes.blank? - # if no any scopes required, the scopes of token is sufficient. - return true - else - # If there are scopes required, then check whether - # the set of authorized scopes is a superset of the set of required scopes - required_scopes = Set.new(scopes) - authorized_scopes = Set.new(token.scopes) - - return authorized_scopes >= required_scopes - end - end - end -end diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index fc4b0a72add..88cd0f5f652 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -52,8 +52,8 @@ Doorkeeper.configure do # Define access token scopes for your provider # For more information go to # https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes - default_scopes :api - # optional_scopes :write, :update + default_scopes(*Gitlab::Auth::DEFAULT_SCOPES) + optional_scopes(*Gitlab::Auth::OPTIONAL_SCOPES) # Change the way client credentials are retrieved from the request object. # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index a4032a21420..1d728282d90 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -59,6 +59,7 @@ en: unknown: "The access token is invalid" scopes: api: Access your API + read_user: Read user information flash: applications: diff --git a/lib/api/api.rb b/lib/api/api.rb index cec2702e44d..9d5adffd8f4 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -3,6 +3,8 @@ module API include APIGuard version 'v3', using: :path + before { allow_access_with_scope :api } + rescue_from Gitlab::Access::AccessDeniedError do rack_response({ 'message' => '403 Forbidden' }.to_json, 403) end diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index 8cc7a26f1fa..cd266669b1e 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -6,6 +6,9 @@ module API module APIGuard extend ActiveSupport::Concern + PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN" + PRIVATE_TOKEN_PARAM = :private_token + included do |base| # OAuth2 Resource Server Authentication use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request| @@ -41,30 +44,59 @@ module API # Defaults to empty array. # def doorkeeper_guard(scopes: []) - access_token = find_access_token - return nil unless access_token - - case validate_access_token(access_token, scopes) - when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE - raise InsufficientScopeError.new(scopes) + if access_token = find_access_token + case AccessTokenValidationService.validate(access_token, scopes: scopes) + when AccessTokenValidationService::INSUFFICIENT_SCOPE + raise InsufficientScopeError.new(scopes) + when AccessTokenValidationService::EXPIRED + raise ExpiredError + when AccessTokenValidationService::REVOKED + raise RevokedError + when AccessTokenValidationService::VALID + @current_user = User.find(access_token.resource_owner_id) + end + end + end - when Oauth2::AccessTokenValidationService::EXPIRED - raise ExpiredError + def find_user_by_private_token(scopes: []) + token_string = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s - when Oauth2::AccessTokenValidationService::REVOKED - raise RevokedError + return nil unless token_string.present? - when Oauth2::AccessTokenValidationService::VALID - @current_user = User.find(access_token.resource_owner_id) - end + find_user_by_authentication_token(token_string) || find_user_by_personal_access_token(token_string, scopes) end def current_user @current_user end + # Set the authorization scope(s) allowed for the current request. + # + # Note: A call to this method adds to any previous scopes in place. This is done because + # `Grape` callbacks run from the outside-in: the top-level callback (API::API) runs first, then + # the next-level callback (API::API::Users, for example) runs. All these scopes are valid for the + # given endpoint (GET `/api/users` is accessible by the `api` and `read_user` scopes), and so they + # need to be stored. + def allow_access_with_scope(*scopes) + @scopes ||= [] + @scopes.concat(scopes.map(&:to_s)) + end + private + def find_user_by_authentication_token(token_string) + User.find_by_authentication_token(token_string) + end + + def find_user_by_personal_access_token(token_string, scopes) + access_token = PersonalAccessToken.active.find_by_token(token_string) + return unless access_token + + if AccessTokenValidationService.sufficient_scope?(access_token, scopes) + User.find(access_token.user_id) + end + end + def find_access_token @access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods) end @@ -72,10 +104,6 @@ module API def doorkeeper_request @doorkeeper_request ||= ActionDispatch::Request.new(env) end - - def validate_access_token(access_token, scopes) - Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes) - end end module ClassMethods diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 746849ef4c0..4be659fc20b 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -2,8 +2,6 @@ module API module Helpers include Gitlab::Utils - PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN" - PRIVATE_TOKEN_PARAM = :private_token SUDO_HEADER = "HTTP_SUDO" SUDO_PARAM = :sudo @@ -308,7 +306,7 @@ module API private def private_token - params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER] + params[APIGuard::PRIVATE_TOKEN_PARAM] || env[APIGuard::PRIVATE_TOKEN_HEADER] end def warden @@ -323,18 +321,11 @@ module API warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD']) end - def find_user_by_private_token - token = private_token - return nil unless token.present? - - User.find_by_authentication_token(token) || User.find_by_personal_access_token(token) - end - def initial_current_user return @initial_current_user if defined?(@initial_current_user) - @initial_current_user ||= find_user_by_private_token - @initial_current_user ||= doorkeeper_guard + @initial_current_user ||= find_user_by_private_token(scopes: @scopes) + @initial_current_user ||= doorkeeper_guard(scopes: @scopes) @initial_current_user ||= find_user_from_warden unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed? diff --git a/lib/api/users.rb b/lib/api/users.rb index c7db2d71017..0842c3874c5 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -2,7 +2,10 @@ module API class Users < Grape::API include PaginationParams - before { authenticate! } + before do + allow_access_with_scope :read_user if request.get? + authenticate! + end resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do helpers do diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index aca5d0020cf..c3c464248ef 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -2,6 +2,10 @@ module Gitlab module Auth class MissingPersonalTokenError < StandardError; end + SCOPES = [:api, :read_user] + DEFAULT_SCOPES = [:api] + OPTIONAL_SCOPES = SCOPES - DEFAULT_SCOPES + class << self def find_for_git_client(login, password, project:, ip:) raise "Must provide an IP for rate limiting" if ip.nil? diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb index 5262a623761..bd9ecaf2685 100644 --- a/spec/requests/api/doorkeeper_access_spec.rb +++ b/spec/requests/api/doorkeeper_access_spec.rb @@ -5,7 +5,7 @@ describe API::API, api: true do let!(:user) { create(:user) } let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) } - let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id } + let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "api" } describe "when unauthenticated" do it "returns authentication success" do diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 4035fd97af5..15b93118ee4 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe API::Helpers, api: true do + include API::APIGuard::HelperMethods include API::Helpers include SentryHelper @@ -15,24 +16,24 @@ describe API::Helpers, api: true do def set_env(user_or_token, identifier) clear_env clear_param - env[API::Helpers::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token env[API::Helpers::SUDO_HEADER] = identifier.to_s end def set_param(user_or_token, identifier) clear_env clear_param - params[API::Helpers::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token + params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token params[API::Helpers::SUDO_PARAM] = identifier.to_s end def clear_env - env.delete(API::Helpers::PRIVATE_TOKEN_HEADER) + env.delete(API::APIGuard::PRIVATE_TOKEN_HEADER) env.delete(API::Helpers::SUDO_HEADER) end def clear_param - params.delete(API::Helpers::PRIVATE_TOKEN_PARAM) + params.delete(API::APIGuard::PRIVATE_TOKEN_PARAM) params.delete(API::Helpers::SUDO_PARAM) end @@ -94,22 +95,22 @@ describe API::Helpers, api: true do describe "when authenticating using a user's private token" do it "returns nil for an invalid token" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token' + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token' allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } expect(current_user).to be_nil end it "returns nil for a user without access" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) expect(current_user).to be_nil end it "leaves user as is when sudo not specified" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token expect(current_user).to eq(user) clear_env - params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token + params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user.private_token expect(current_user).to eq(user) end end @@ -117,37 +118,45 @@ describe API::Helpers, api: true do describe "when authenticating using a user's personal access tokens" do let(:personal_access_token) { create(:personal_access_token, user: user) } + before do + allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false } + end + it "returns nil for an invalid token" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token' - allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token' expect(current_user).to be_nil end it "returns nil for a user without access" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) expect(current_user).to be_nil end + it "returns nil for a token without the appropriate scope" do + personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user']) + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token + allow_access_with_scope('write_user') + expect(current_user).to be_nil + end + it "leaves user as is when sudo not specified" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token expect(current_user).to eq(user) clear_env - params[API::Helpers::PRIVATE_TOKEN_PARAM] = personal_access_token.token + params[API::APIGuard::PRIVATE_TOKEN_PARAM] = personal_access_token.token expect(current_user).to eq(user) end it 'does not allow revoked tokens' do personal_access_token.revoke! - env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token - allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token expect(current_user).to be_nil end it 'does not allow expired tokens' do personal_access_token.update_attributes!(expires_at: 1.day.ago) - env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token - allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token expect(current_user).to be_nil end end diff --git a/spec/services/access_token_validation_service_spec.rb b/spec/services/access_token_validation_service_spec.rb new file mode 100644 index 00000000000..8808934fa24 --- /dev/null +++ b/spec/services/access_token_validation_service_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe AccessTokenValidationService, services: true do + + describe ".sufficient_scope?" do + it "returns true if the required scope is present in the token's scopes" do + token = double("token", scopes: [:api, :read_user]) + + expect(described_class.sufficient_scope?(token, [:api])).to be(true) + end + + it "returns true if more than one of the required scopes is present in the token's scopes" do + token = double("token", scopes: [:api, :read_user, :other_scope]) + + expect(described_class.sufficient_scope?(token, [:api, :other_scope])).to be(true) + end + + it "returns true if the list of required scopes is an exact match for the token's scopes" do + token = double("token", scopes: [:api, :read_user, :other_scope]) + + expect(described_class.sufficient_scope?(token, [:api, :read_user, :other_scope])).to be(true) + end + + it "returns true if the list of required scopes contains all of the token's scopes, in addition to others" do + token = double("token", scopes: [:api, :read_user]) + + expect(described_class.sufficient_scope?(token, [:api, :read_user, :other_scope])).to be(true) + end + + it 'returns true if the list of required scopes is blank' do + token = double("token", scopes: []) + + expect(described_class.sufficient_scope?(token, [])).to be(true) + end + + it "returns false if there are no scopes in common between the required scopes and the token scopes" do + token = double("token", scopes: [:api, :read_user]) + + expect(described_class.sufficient_scope?(token, [:other_scope])).to be(false) + end + end +end -- cgit v1.2.1 From 36b3210b9ec4fffd9fa5a73626907e8a6a59f435 Mon Sep 17 00:00:00 2001 From: Timothy Andrew <mail@timothyandrew.net> Date: Tue, 22 Nov 2016 14:43:37 +0530 Subject: Validate access token scopes in `Gitlab::Auth` - This module is used for git-over-http, as well as JWT. - The only valid scope here is `api`, currently. --- lib/gitlab/auth.rb | 14 ++++++++++--- spec/lib/gitlab/auth_spec.rb | 46 +++++++++++++++++++++++++++++++++++------- spec/requests/git_http_spec.rb | 2 +- 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index c3c464248ef..c6a23aa2bdf 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -92,7 +92,7 @@ module Gitlab def oauth_access_token_check(login, password) if login == "oauth2" && password.present? token = Doorkeeper::AccessToken.by_token(password) - if token && token.accessible? + if token && token.accessible? && token_has_scope?(token) user = User.find_by(id: token.resource_owner_id) Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities) end @@ -101,12 +101,20 @@ module Gitlab def personal_access_token_check(login, password) if login && password - user = User.find_by_personal_access_token(password) + token = PersonalAccessToken.active.find_by_token(password) validation = User.by_login(login) - Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities) if user.present? && user == validation + + if token && token.user == validation && token_has_scope?(token) + Gitlab::Auth::Result.new(validation, nil, :personal_token, full_authentication_abilities) + end + end end + def token_has_scope?(token) + AccessTokenValidationService.sufficient_scope?(token, ['api']) + end + def lfs_token_check(login, password) deploy_key_matches = login.match(/\Alfs\+deploy-key-(\d+)\z/) diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index c9d64e99f88..b64413cda12 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -79,14 +79,46 @@ describe Gitlab::Auth, lib: true do expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities)) end - it 'recognizes OAuth tokens' do - user = create(:user) - application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) - token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id) - ip = 'ip' + context "while using OAuth tokens as passwords" do + it 'succeeds for OAuth tokens with the `api` scope' do + user = create(:user) + application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) + token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api") + ip = 'ip' + + expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'oauth2') + expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)) + end + + it 'fails for OAuth tokens with other scopes' do + user = create(:user) + application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) + token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "read_user") + ip = 'ip' + + expect(gl_auth).to receive(:rate_limit!).with(ip, success: false, login: 'oauth2') + expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, nil)) + end + end + + context "while using personal access tokens as passwords" do + it 'succeeds for personal access tokens with the `api` scope' do + user = create(:user) + personal_access_token = create(:personal_access_token, user: user, scopes: ['api']) + ip = 'ip' + + expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.email) + expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities)) + end + + it 'fails for personal access tokens with other scopes' do + user = create(:user) + personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user']) + ip = 'ip' - expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'oauth2') - expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)) + expect(gl_auth).to receive(:rate_limit!).with(ip, success: false, login: user.email) + expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, nil)) + end end it 'returns double nil for invalid credentials' do diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index f1728d61def..d71bb08c218 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -230,7 +230,7 @@ describe 'Git HTTP requests', lib: true do context "when an oauth token is provided" do before do application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) - @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id) + @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api") end it "downloads get status 200" do -- cgit v1.2.1 From ac9835c602f1c9b5a35ef40df079faf1d4b91f7b Mon Sep 17 00:00:00 2001 From: Timothy Andrew <mail@timothyandrew.net> Date: Tue, 22 Nov 2016 16:20:21 +0530 Subject: Update CHANGELOG --- changelogs/unreleased/20492-access-token-scopes.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/20492-access-token-scopes.yml diff --git a/changelogs/unreleased/20492-access-token-scopes.yml b/changelogs/unreleased/20492-access-token-scopes.yml new file mode 100644 index 00000000000..a9424ded662 --- /dev/null +++ b/changelogs/unreleased/20492-access-token-scopes.yml @@ -0,0 +1,4 @@ +--- +title: Add scopes for personal access tokens and OAuth tokens +merge_request: 5951 +author: -- cgit v1.2.1 From 4d6da770de94f4bf140507cdf43461b67269ce28 Mon Sep 17 00:00:00 2001 From: Timothy Andrew <mail@timothyandrew.net> Date: Thu, 24 Nov 2016 13:07:22 +0530 Subject: Implement minor changes from @dbalexandre's review. - Mainly whitespace changes. - Require the migration adding the `scope` column to the `personal_access_tokens` table to have downtime, since API calls will fail if the new code is in place, but the migration hasn't run. - Minor refactoring - load `@scopes` in a `before_action`, since we're doing it in three different places. --- app/controllers/admin/applications_controller.rb | 3 +-- app/controllers/concerns/oauth_applications.rb | 5 ++++ app/controllers/oauth/applications_controller.rb | 6 +---- app/views/doorkeeper/applications/_form.html.haml | 1 - app/views/doorkeeper/applications/show.html.haml | 1 - ..._add_column_scopes_to_personal_access_tokens.rb | 23 +++--------------- ...al_access_tokens_default_back_to_empty_array.rb | 28 +++------------------- lib/api/api_guard.rb | 26 +++++++++++--------- lib/gitlab/auth.rb | 1 - .../access_token_validation_service_spec.rb | 1 - 10 files changed, 28 insertions(+), 67 deletions(-) diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb index 759044910bb..62f62e99a97 100644 --- a/app/controllers/admin/applications_controller.rb +++ b/app/controllers/admin/applications_controller.rb @@ -2,6 +2,7 @@ class Admin::ApplicationsController < Admin::ApplicationController include OauthApplications before_action :set_application, only: [:show, :edit, :update, :destroy] + before_action :load_scopes, only: [:new, :edit] def index @applications = Doorkeeper::Application.where("owner_id IS NULL") @@ -12,11 +13,9 @@ class Admin::ApplicationsController < Admin::ApplicationController def new @application = Doorkeeper::Application.new - @scopes = Doorkeeper.configuration.scopes end def edit - @scopes = Doorkeeper.configuration.scopes end def create diff --git a/app/controllers/concerns/oauth_applications.rb b/app/controllers/concerns/oauth_applications.rb index 34ad43ededd..7210ed3eb32 100644 --- a/app/controllers/concerns/oauth_applications.rb +++ b/app/controllers/concerns/oauth_applications.rb @@ -7,8 +7,13 @@ module OauthApplications def prepare_scopes scopes = params.dig(:doorkeeper_application, :scopes) + if scopes params[:doorkeeper_application][:scopes] = scopes.join(' ') end end + + def load_scopes + @scopes = Doorkeeper.configuration.scopes + end end diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index b5449a6c30e..2ae4785b12c 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -7,6 +7,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController before_action :verify_user_oauth_applications_enabled before_action :authenticate_user! before_action :add_gon_variables + before_action :load_scopes, only: [:index, :create, :edit] layout 'profile' @@ -14,10 +15,6 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController set_index_vars end - def edit - @scopes = Doorkeeper.configuration.scopes - end - def create @application = Doorkeeper::Application.new(application_params) @@ -45,7 +42,6 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController @authorized_tokens = current_user.oauth_authorized_tokens @authorized_anonymous_tokens = @authorized_tokens.reject(&:application) @authorized_apps = @authorized_tokens.map(&:application).uniq.reject(&:nil?) - @scopes = Doorkeeper.configuration.scopes # Don't overwrite a value possibly set by `create` @application ||= Doorkeeper::Application.new diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml index 6fdb04077b6..96677dc1a4d 100644 --- a/app/views/doorkeeper/applications/_form.html.haml +++ b/app/views/doorkeeper/applications/_form.html.haml @@ -25,6 +25,5 @@ = label_tag "doorkeeper_application_scopes_#{scope}", scope %span= "(#{t(scope, scope: [:doorkeeper, :scopes])})" - .prepend-top-default = f.submit 'Save application', class: "btn btn-create" diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml index a18e133c8de..5473a8e0ddc 100644 --- a/app/views/doorkeeper/applications/show.html.haml +++ b/app/views/doorkeeper/applications/show.html.haml @@ -34,7 +34,6 @@ %span.scope-name= scope = "(#{t(scope, scope: [:doorkeeper, :scopes])})" - .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/db/migrate/20160823083941_add_column_scopes_to_personal_access_tokens.rb b/db/migrate/20160823083941_add_column_scopes_to_personal_access_tokens.rb index ab7f0365603..91479de840b 100644 --- a/db/migrate/20160823083941_add_column_scopes_to_personal_access_tokens.rb +++ b/db/migrate/20160823083941_add_column_scopes_to_personal_access_tokens.rb @@ -1,32 +1,15 @@ -# See http://doc.gitlab.com/ce/development/migration_style_guide.html -# for more information on how to write migrations for GitLab. +# The default needs to be `[]`, but all existing access tokens need to have `scopes` set to `['api']`. +# It's easier to achieve this by adding the column with the `['api']` default, and then changing the default to +# `[]`. class AddColumnScopesToPersonalAccessTokens < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers - # Set this constant to true if this migration requires downtime. DOWNTIME = false - # When a migration requires downtime you **must** uncomment the following - # constant and define a short and easy to understand explanation as to why the - # migration requires downtime. - # DOWNTIME_REASON = '' - - # When using the methods "add_concurrent_index" or "add_column_with_default" - # you must disable the use of transactions as these methods can not run in an - # existing transaction. When using "add_concurrent_index" make sure that this - # method is the _only_ method called in the migration, any other changes - # should go in a separate migration. This ensures that upon failure _only_ the - # index creation fails and can be retried or reverted easily. - # - # To disable transactions uncomment the following line and remove these - # comments: disable_ddl_transaction! def up - # The default needs to be `[]`, but all existing access tokens need to have `scopes` set to `['api']`. - # It's easier to achieve this by adding the column with the `['api']` default, and then changing the default to - # `[]`. add_column_with_default :personal_access_tokens, :scopes, :string, default: ['api'].to_yaml end diff --git a/db/migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb b/db/migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb index 018cc3d4747..c8ceb116b8a 100644 --- a/db/migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb +++ b/db/migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb @@ -1,39 +1,17 @@ -# See http://doc.gitlab.com/ce/development/migration_style_guide.html -# for more information on how to write migrations for GitLab. +# The default needs to be `[]`, but all existing access tokens need to have `scopes` set to `['api']`. +# It's easier to achieve this by adding the column with the `['api']` default, and then changing the default to +# `[]`. class ChangePersonalAccessTokensDefaultBackToEmptyArray < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers - # Set this constant to true if this migration requires downtime. DOWNTIME = false - # When a migration requires downtime you **must** uncomment the following - # constant and define a short and easy to understand explanation as to why the - # migration requires downtime. - # DOWNTIME_REASON = '' - - # When using the methods "add_concurrent_index" or "add_column_with_default" - # you must disable the use of transactions as these methods can not run in an - # existing transaction. When using "add_concurrent_index" make sure that this - # method is the _only_ method called in the migration, any other changes - # should go in a separate migration. This ensures that upon failure _only_ the - # index creation fails and can be retried or reverted easily. - # - # To disable transactions uncomment the following line and remove these - # comments: - # disable_ddl_transaction! - def up - # The default needs to be `[]`, but all existing access tokens need to have `scopes` set to `['api']`. - # It's easier to achieve this by adding the column with the `['api']` default, and then changing the default to - # `[]`. change_column_default :personal_access_tokens, :scopes, [].to_yaml end def down - # The default needs to be `[]`, but all existing access tokens need to have `scopes` set to `['api']`. - # It's easier to achieve this by adding the column with the `['api']` default, and then changing the default to - # `[]`. change_column_default :personal_access_tokens, :scopes, ['api'].to_yaml end end diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index cd266669b1e..563224a580f 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -44,17 +44,21 @@ module API # Defaults to empty array. # def doorkeeper_guard(scopes: []) - if access_token = find_access_token - case AccessTokenValidationService.validate(access_token, scopes: scopes) - when AccessTokenValidationService::INSUFFICIENT_SCOPE - raise InsufficientScopeError.new(scopes) - when AccessTokenValidationService::EXPIRED - raise ExpiredError - when AccessTokenValidationService::REVOKED - raise RevokedError - when AccessTokenValidationService::VALID - @current_user = User.find(access_token.resource_owner_id) - end + access_token = find_access_token + return nil unless access_token + + case AccessTokenValidationService.validate(access_token, scopes: scopes) + when AccessTokenValidationService::INSUFFICIENT_SCOPE + raise InsufficientScopeError.new(scopes) + + when AccessTokenValidationService::EXPIRED + raise ExpiredError + + when AccessTokenValidationService::REVOKED + raise RevokedError + + when AccessTokenValidationService::VALID + @current_user = User.find(access_token.resource_owner_id) end end diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index c6a23aa2bdf..c425702fd75 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -107,7 +107,6 @@ module Gitlab if token && token.user == validation && token_has_scope?(token) Gitlab::Auth::Result.new(validation, nil, :personal_token, full_authentication_abilities) end - end end diff --git a/spec/services/access_token_validation_service_spec.rb b/spec/services/access_token_validation_service_spec.rb index 8808934fa24..332e745aa36 100644 --- a/spec/services/access_token_validation_service_spec.rb +++ b/spec/services/access_token_validation_service_spec.rb @@ -1,7 +1,6 @@ require 'spec_helper' describe AccessTokenValidationService, services: true do - describe ".sufficient_scope?" do it "returns true if the required scope is present in the token's scopes" do token = double("token", scopes: [:api, :read_user]) -- cgit v1.2.1 From 990ae6b8e5f2797a6c168f9c16a725a159570058 Mon Sep 17 00:00:00 2001 From: Timothy Andrew <mail@timothyandrew.net> Date: Thu, 24 Nov 2016 14:22:03 +0530 Subject: Move the scopes form/list view into a partial. - The list of scopes that's displayed while creating a personal access token is identical to the list that's displayed while creating an OAuth application. Extract these into a partial. - The list of scopes that's displayed while in the show page for an OAuth token in the profile settings and admin settings are identical. Extract these into a partial. --- app/views/admin/applications/show.html.haml | 11 +---------- app/views/doorkeeper/applications/_form.html.haml | 6 +----- app/views/doorkeeper/applications/show.html.haml | 11 +---------- app/views/profiles/personal_access_tokens/_form.html.haml | 6 +----- app/views/shared/tokens/_scopes_form.html.haml | 5 +++++ app/views/shared/tokens/_scopes_list.html.haml | 10 ++++++++++ 6 files changed, 19 insertions(+), 30 deletions(-) create mode 100644 app/views/shared/tokens/_scopes_form.html.haml create mode 100644 app/views/shared/tokens/_scopes_list.html.haml diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml index 3418dc96496..6e7b7003ac5 100644 --- a/app/views/admin/applications/show.html.haml +++ b/app/views/admin/applications/show.html.haml @@ -23,16 +23,7 @@ %div %span.monospace= uri - - if @application.scopes.present? - %tr - %td - Scopes - %td - %ul.scopes-list.append-bottom-0 - - @application.scopes.each do |scope| - %li - %span.scope-name= scope - = "(#{t(scope, scope: [:doorkeeper, :scopes])})" + = render partial: "shared/tokens/scopes_list" .form-actions = link_to 'Edit', edit_admin_application_path(@application), class: 'btn btn-primary wide pull-left' diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml index 96677dc1a4d..a6ad0bb8d1b 100644 --- a/app/views/doorkeeper/applications/_form.html.haml +++ b/app/views/doorkeeper/applications/_form.html.haml @@ -19,11 +19,7 @@ .form-group = f.label :scopes, class: 'label-light' - - @scopes.each do |scope| - %fieldset - = check_box_tag 'doorkeeper_application[scopes][]', scope, application.scopes.include?(scope), id: "doorkeeper_application_scopes_#{scope}" - = label_tag "doorkeeper_application_scopes_#{scope}", scope - %span= "(#{t(scope, scope: [:doorkeeper, :scopes])})" + = render partial: 'shared/tokens/scopes_form', locals: { prefix: 'doorkeeper_application', token: application } .prepend-top-default = f.submit 'Save application', class: "btn btn-create" diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml index 5473a8e0ddc..e528cb825f5 100644 --- a/app/views/doorkeeper/applications/show.html.haml +++ b/app/views/doorkeeper/applications/show.html.haml @@ -23,16 +23,7 @@ %div %span.monospace= uri - - if @application.scopes.present? - %tr - %td - Scopes - %td - %ul.scopes-list.append-bottom-0 - - @application.scopes.each do |scope| - %li - %span.scope-name= scope - = "(#{t(scope, scope: [:doorkeeper, :scopes])})" + = render partial: "shared/tokens/scopes_list" .form-actions = link_to 'Edit', edit_oauth_application_path(@application), class: 'btn btn-primary wide pull-left' diff --git a/app/views/profiles/personal_access_tokens/_form.html.haml b/app/views/profiles/personal_access_tokens/_form.html.haml index 6083fdaa31d..5651b242129 100644 --- a/app/views/profiles/personal_access_tokens/_form.html.haml +++ b/app/views/profiles/personal_access_tokens/_form.html.haml @@ -12,11 +12,7 @@ .form-group = f.label :scopes, class: 'label-light' - - @scopes.each do |scope| - %fieldset - = check_box_tag 'personal_access_token[scopes][]', scope, @personal_access_token.scopes.include?(scope), id: "personal_access_token_scopes_#{scope}" - = label_tag "personal_access_token_scopes_#{scope}", scope - %span= "(#{t(scope, scope: [:doorkeeper, :scopes])})" + = render partial: 'shared/tokens/scopes_form', locals: { prefix: 'personal_access_token', token: @personal_access_token } .prepend-top-default = f.submit 'Create Personal Access Token', class: "btn btn-create" diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml new file mode 100644 index 00000000000..5dbbd9e4808 --- /dev/null +++ b/app/views/shared/tokens/_scopes_form.html.haml @@ -0,0 +1,5 @@ +- @scopes.each do |scope| + %fieldset + = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}" + = label_tag "#{prefix}_scopes_#{scope}", scope + %span= "(#{t(scope, scope: [:doorkeeper, :scopes])})" diff --git a/app/views/shared/tokens/_scopes_list.html.haml b/app/views/shared/tokens/_scopes_list.html.haml new file mode 100644 index 00000000000..9e3b562f0f5 --- /dev/null +++ b/app/views/shared/tokens/_scopes_list.html.haml @@ -0,0 +1,10 @@ +- if @application.scopes.present? + %tr + %td + Scopes + %td + %ul.scopes-list.append-bottom-0 + - @application.scopes.each do |scope| + %li + %span.scope-name= scope + = "(#{t(scope, scope: [:doorkeeper, :scopes])})" -- cgit v1.2.1 From dc95bcbb165289d9754e6bf66288c8d4350f6e57 Mon Sep 17 00:00:00 2001 From: Timothy Andrew <mail@timothyandrew.net> Date: Thu, 24 Nov 2016 14:39:12 +0530 Subject: Refactor access token validation in `Gitlab::Auth` - Based on @dbalexandre's review - Extract token validity conditions into two separate methods, for personal access tokens and OAuth tokens. --- lib/gitlab/auth.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index c425702fd75..c21afaa1551 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -92,7 +92,7 @@ module Gitlab def oauth_access_token_check(login, password) if login == "oauth2" && password.present? token = Doorkeeper::AccessToken.by_token(password) - if token && token.accessible? && token_has_scope?(token) + if valid_oauth_token?(token) user = User.find_by(id: token.resource_owner_id) Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities) end @@ -104,12 +104,20 @@ module Gitlab token = PersonalAccessToken.active.find_by_token(password) validation = User.by_login(login) - if token && token.user == validation && token_has_scope?(token) + if valid_personal_access_token?(token, validation) Gitlab::Auth::Result.new(validation, nil, :personal_token, full_authentication_abilities) end end end + def valid_oauth_token?(token) + token && token.accessible? && token_has_scope?(token) + end + + def valid_personal_access_token?(token, user) + token && token.user == user && token_has_scope?(token) + end + def token_has_scope?(token) AccessTokenValidationService.sufficient_scope?(token, ['api']) end -- cgit v1.2.1 From fc7a5a3806c7c7317731ea305715fe0573b90d88 Mon Sep 17 00:00:00 2001 From: Timothy Andrew <mail@timothyandrew.net> Date: Mon, 28 Nov 2016 12:30:41 +0530 Subject: Modify `ApiHelpers` spec to adhere to the Four-Phase test style. - Use whitespace to separate the setup, expectation and teardown phases. --- spec/requests/api/helpers_spec.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 15b93118ee4..c3d7ac3eef8 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -97,20 +97,26 @@ describe API::Helpers, api: true do it "returns nil for an invalid token" do env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token' allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + expect(current_user).to be_nil end it "returns nil for a user without access" do env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) + expect(current_user).to be_nil end it "leaves user as is when sudo not specified" do env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token + expect(current_user).to eq(user) + clear_env + params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user.private_token + expect(current_user).to eq(user) end end @@ -124,12 +130,14 @@ describe API::Helpers, api: true do it "returns nil for an invalid token" do env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token' + expect(current_user).to be_nil end it "returns nil for a user without access" do env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) + expect(current_user).to be_nil end @@ -137,6 +145,7 @@ describe API::Helpers, api: true do personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user']) env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token allow_access_with_scope('write_user') + expect(current_user).to be_nil end @@ -145,18 +154,21 @@ describe API::Helpers, api: true do expect(current_user).to eq(user) clear_env params[API::APIGuard::PRIVATE_TOKEN_PARAM] = personal_access_token.token + expect(current_user).to eq(user) end it 'does not allow revoked tokens' do personal_access_token.revoke! env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token + expect(current_user).to be_nil end it 'does not allow expired tokens' do personal_access_token.update_attributes!(expires_at: 1.day.ago) env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token + expect(current_user).to be_nil end end -- cgit v1.2.1 From f14d423dc7c9ec2d97c83f0c8893661922df4360 Mon Sep 17 00:00:00 2001 From: Timothy Andrew <mail@timothyandrew.net> Date: Mon, 28 Nov 2016 13:13:53 +0530 Subject: Add a controller spec for personal access tokens. Split the existing feature spec into both feature and controller specs. Feature specs assert on browser DOM, and controller specs assert on database state. --- .../profiles/personal_access_tokens_spec.rb | 49 +++++++++++++++++++++ .../profiles/personal_access_tokens_spec.rb | 51 +++++----------------- 2 files changed, 60 insertions(+), 40 deletions(-) create mode 100644 spec/controllers/profiles/personal_access_tokens_spec.rb diff --git a/spec/controllers/profiles/personal_access_tokens_spec.rb b/spec/controllers/profiles/personal_access_tokens_spec.rb new file mode 100644 index 00000000000..45534a3a587 --- /dev/null +++ b/spec/controllers/profiles/personal_access_tokens_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe Profiles::PersonalAccessTokensController do + let(:user) { create(:user) } + + describe '#create' do + def created_token + PersonalAccessToken.order(:created_at).last + end + + before { sign_in(user) } + + it "allows creation of a token" do + name = FFaker::Product.brand + + post :create, personal_access_token: { name: name } + + expect(created_token).not_to be_nil + expect(created_token.name).to eq(name) + expect(created_token.expires_at).to be_nil + expect(PersonalAccessToken.active).to include(created_token) + end + + it "allows creation of a token with an expiry date" do + expires_at = 5.days.from_now + + post :create, personal_access_token: { name: FFaker::Product.brand, expires_at: expires_at } + + expect(created_token).not_to be_nil + expect(created_token.expires_at.to_i).to eq(expires_at.to_i) + end + + context "scopes" do + it "allows creation of a token with scopes" do + post :create, personal_access_token: { name: FFaker::Product.brand, scopes: ['api', 'read_user'] } + + expect(created_token).not_to be_nil + expect(created_token.scopes).to eq(['api', 'read_user']) + end + + it "allows creation of a token with no scopes" do + post :create, personal_access_token: { name: FFaker::Product.brand, scopes: [] } + + expect(created_token).not_to be_nil + expect(created_token.scopes).to eq([]) + end + end + end +end diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb index 0ffeeff0921..55a01057c83 100644 --- a/spec/features/profiles/personal_access_tokens_spec.rb +++ b/spec/features/profiles/personal_access_tokens_spec.rb @@ -27,54 +27,25 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do describe "token creation" do it "allows creation of a token" do - visit profile_personal_access_tokens_path - fill_in "Name", with: FFaker::Product.brand - - expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1) - expect(created_personal_access_token).to eq(PersonalAccessToken.last.token) - expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.name) - expect(active_personal_access_tokens).to have_text("Never") - end + name = FFaker::Product.brand - it "allows creation of a token with an expiry date" do visit profile_personal_access_tokens_path - fill_in "Name", with: FFaker::Product.brand + fill_in "Name", with: name # Set date to 1st of next month find_field("Expires at").trigger('focus') find("a[title='Next']").click click_on "1" - expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1) - expect(created_personal_access_token).to eq(PersonalAccessToken.last.token) - expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.name) - expect(active_personal_access_tokens).to have_text(Date.today.next_month.at_beginning_of_month.to_s(:medium)) - end - - context "scopes" do - it "allows creation of a token with scopes" do - visit profile_personal_access_tokens_path - fill_in "Name", with: FFaker::Product.brand - - check "api" - check "read_user" - - expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1) - expect(created_personal_access_token).to eq(PersonalAccessToken.last.token) - expect(PersonalAccessToken.last.scopes).to match_array(['api', 'read_user']) - expect(active_personal_access_tokens).to have_text('api') - expect(active_personal_access_tokens).to have_text('read_user') - end - - it "allows creation of a token with no scopes" do - visit profile_personal_access_tokens_path - fill_in "Name", with: FFaker::Product.brand + # Scopes + check "api" + check "read_user" - expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1) - expect(created_personal_access_token).to eq(PersonalAccessToken.last.token) - expect(PersonalAccessToken.last.scopes).to eq([]) - expect(active_personal_access_tokens).to have_text('no scopes') - end + click_on "Create Personal Access Token" + expect(active_personal_access_tokens).to have_text(name) + expect(active_personal_access_tokens).to have_text(Date.today.next_month.at_beginning_of_month.to_s(:medium)) + expect(active_personal_access_tokens).to have_text('api') + expect(active_personal_access_tokens).to have_text('read_user') end context "when creation fails" do @@ -111,7 +82,7 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do disallow_personal_access_token_saves! visit profile_personal_access_tokens_path - expect { click_on "Revoke" }.not_to change { PersonalAccessToken.inactive.count } + click_on "Revoke" expect(active_personal_access_tokens).to have_text(personal_access_token.name) expect(page).to have_content("Could not revoke") end -- cgit v1.2.1 From f706a973c26f9de9a1f1599d532b33e9e66a80bb Mon Sep 17 00:00:00 2001 From: Timothy Andrew <mail@timothyandrew.net> Date: Mon, 5 Dec 2016 09:49:51 +0530 Subject: View-related (and other minor) changes to !5951 based on @rymai's review. - The `scopes_form` partial can be used in the `admin/applications` view as well - Don't allow partials to access instance variables directly. Instead, pass in the instance variables as local variables, and use `local_assigns.fetch` to assert that the variables are passed in as expected. - Change a few instances of `render :partial` to `render` - Remove an instance of `required: false` in a view, since this is the default - Inline many instances of a local variable (`ip = 'ip'`) in `auth_spec` --- app/views/admin/applications/_form.html.haml | 6 +-- app/views/admin/applications/show.html.haml | 2 +- app/views/doorkeeper/applications/_form.html.haml | 2 +- app/views/doorkeeper/applications/show.html.haml | 2 +- .../personal_access_tokens/_form.html.haml | 11 ++++-- .../personal_access_tokens/index.html.haml | 2 +- app/views/shared/tokens/_scopes_form.html.haml | 6 ++- app/views/shared/tokens/_scopes_list.html.haml | 23 ++++++----- spec/lib/gitlab/auth_spec.rb | 46 +++++++++------------- 9 files changed, 48 insertions(+), 52 deletions(-) diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml index 36d2f415a05..c689b26d6e6 100644 --- a/app/views/admin/applications/_form.html.haml +++ b/app/views/admin/applications/_form.html.haml @@ -22,11 +22,7 @@ .form-group = f.label :scopes, class: 'col-sm-2 control-label' .col-sm-10 - - @scopes.each do |scope| - %fieldset - = check_box_tag 'doorkeeper_application[scopes][]', scope, application.scopes.include?(scope), id: "doorkeeper_application_scopes_#{scope}" - = label_tag "doorkeeper_application_scopes_#{scope}", scope - %span= "(#{t(scope, scope: [:doorkeeper, :scopes])})" + = render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes .form-actions = f.submit 'Submit', class: "btn btn-save wide" diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml index 6e7b7003ac5..14683cc66e9 100644 --- a/app/views/admin/applications/show.html.haml +++ b/app/views/admin/applications/show.html.haml @@ -23,7 +23,7 @@ %div %span.monospace= uri - = render partial: "shared/tokens/scopes_list" + = render "shared/tokens/scopes_list", token: @application .form-actions = link_to 'Edit', edit_admin_application_path(@application), class: 'btn btn-primary wide pull-left' diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml index a6ad0bb8d1b..b3313c7c985 100644 --- a/app/views/doorkeeper/applications/_form.html.haml +++ b/app/views/doorkeeper/applications/_form.html.haml @@ -19,7 +19,7 @@ .form-group = f.label :scopes, class: 'label-light' - = render partial: 'shared/tokens/scopes_form', locals: { prefix: 'doorkeeper_application', token: application } + = render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes .prepend-top-default = f.submit 'Save application', class: "btn btn-create" diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml index e528cb825f5..559de63d96d 100644 --- a/app/views/doorkeeper/applications/show.html.haml +++ b/app/views/doorkeeper/applications/show.html.haml @@ -23,7 +23,7 @@ %div %span.monospace= uri - = render partial: "shared/tokens/scopes_list" + = render "shared/tokens/scopes_list", token: @application .form-actions = link_to 'Edit', edit_oauth_application_path(@application), class: 'btn btn-primary wide pull-left' diff --git a/app/views/profiles/personal_access_tokens/_form.html.haml b/app/views/profiles/personal_access_tokens/_form.html.haml index 5651b242129..3f6efa33953 100644 --- a/app/views/profiles/personal_access_tokens/_form.html.haml +++ b/app/views/profiles/personal_access_tokens/_form.html.haml @@ -1,6 +1,9 @@ -= form_for [:profile, @personal_access_token], method: :post, html: { class: 'js-requires-input' } do |f| +- personal_access_token = local_assigns.fetch(:personal_access_token) +- scopes = local_assigns.fetch(:scopes) - = form_errors(@personal_access_token) += form_for [:profile, personal_access_token], method: :post, html: { class: 'js-requires-input' } do |f| + + = form_errors(personal_access_token) .form-group = f.label :name, class: 'label-light' @@ -8,11 +11,11 @@ .form-group = f.label :expires_at, class: 'label-light' - = f.text_field :expires_at, class: "datepicker form-control", required: false + = f.text_field :expires_at, class: "datepicker form-control" .form-group = f.label :scopes, class: 'label-light' - = render partial: 'shared/tokens/scopes_form', locals: { prefix: 'personal_access_token', token: @personal_access_token } + = render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: personal_access_token, scopes: scopes .prepend-top-default = f.submit 'Create Personal Access Token', class: "btn btn-create" diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index 39eef0f6baf..bb4effeeeb1 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -29,7 +29,7 @@ %p.profile-settings-content Pick a name for the application, and we'll give you a unique token. - = render "form" + = render "form", personal_access_token: @personal_access_token, scopes: @scopes %hr diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml index 5dbbd9e4808..5074afb63a1 100644 --- a/app/views/shared/tokens/_scopes_form.html.haml +++ b/app/views/shared/tokens/_scopes_form.html.haml @@ -1,4 +1,8 @@ -- @scopes.each do |scope| +- scopes = local_assigns.fetch(:scopes) +- prefix = local_assigns.fetch(:prefix) +- token = local_assigns.fetch(:token) + +- scopes.each do |scope| %fieldset = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}" = label_tag "#{prefix}_scopes_#{scope}", scope diff --git a/app/views/shared/tokens/_scopes_list.html.haml b/app/views/shared/tokens/_scopes_list.html.haml index 9e3b562f0f5..f99e905e95c 100644 --- a/app/views/shared/tokens/_scopes_list.html.haml +++ b/app/views/shared/tokens/_scopes_list.html.haml @@ -1,10 +1,13 @@ -- if @application.scopes.present? - %tr - %td - Scopes - %td - %ul.scopes-list.append-bottom-0 - - @application.scopes.each do |scope| - %li - %span.scope-name= scope - = "(#{t(scope, scope: [:doorkeeper, :scopes])})" +- token = local_assigns.fetch(:token) + +- return unless token.scopes.present? + +%tr + %td + Scopes + %td + %ul.scopes-list.append-bottom-0 + - token.scopes.each do |scope| + %li + %span.scope-name= scope + = "(#{t(scope, scope: [:doorkeeper, :scopes])})" diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index b64413cda12..f251c0dd25a 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -47,36 +47,31 @@ describe Gitlab::Auth, lib: true do project.create_drone_ci_service(active: true) project.drone_ci_service.update(token: 'token') - ip = 'ip' - - expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'drone-ci-token') - expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities)) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'drone-ci-token') + expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities)) end it 'recognizes master passwords' do user = create(:user, password: 'password') - ip = 'ip' - expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username) - expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username) + expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) end it 'recognizes user lfs tokens' do user = create(:user) - ip = 'ip' token = Gitlab::LfsToken.new(user).token - expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username) - expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities)) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username) + expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities)) end it 'recognizes deploy key lfs tokens' do key = create(:deploy_key) - ip = 'ip' token = Gitlab::LfsToken.new(key).token - expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: "lfs+deploy-key-#{key.id}") - expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities)) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}") + expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities)) end context "while using OAuth tokens as passwords" do @@ -84,20 +79,18 @@ describe Gitlab::Auth, lib: true do user = create(:user) application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api") - ip = 'ip' - expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'oauth2') - expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'oauth2') + expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)) end it 'fails for OAuth tokens with other scopes' do user = create(:user) application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "read_user") - ip = 'ip' - expect(gl_auth).to receive(:rate_limit!).with(ip, success: false, login: 'oauth2') - expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, nil)) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'oauth2') + expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil)) end end @@ -105,28 +98,25 @@ describe Gitlab::Auth, lib: true do it 'succeeds for personal access tokens with the `api` scope' do user = create(:user) personal_access_token = create(:personal_access_token, user: user, scopes: ['api']) - ip = 'ip' - expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.email) - expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities)) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.email) + expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities)) end it 'fails for personal access tokens with other scopes' do user = create(:user) personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user']) - ip = 'ip' - expect(gl_auth).to receive(:rate_limit!).with(ip, success: false, login: user.email) - expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, nil)) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: user.email) + expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil)) end end it 'returns double nil for invalid credentials' do login = 'foo' - ip = 'ip' - expect(gl_auth).to receive(:rate_limit!).with(ip, success: false, login: login) - expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login) + expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new) end end -- cgit v1.2.1 From b303948ff549ce57d3b6985c2c366dfcdc5a2ca3 Mon Sep 17 00:00:00 2001 From: Timothy Andrew <mail@timothyandrew.net> Date: Mon, 5 Dec 2016 22:55:53 +0530 Subject: Convert AccessTokenValidationService into a class. - Previously, AccessTokenValidationService was a module, and all its public methods accepted a token. It makes sense to convert it to a class which accepts a token during initialization. - Also rename the `sufficient_scope?` method to `include_any_scope?` - Based on feedback from @rymai --- app/services/access_token_validation_service.rb | 38 ++++++++++------------ lib/api/api_guard.rb | 4 +-- lib/gitlab/auth.rb | 2 +- .../access_token_validation_service_spec.rb | 14 ++++---- 4 files changed, 28 insertions(+), 30 deletions(-) diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb index 69449f3a445..ddaaed90e5b 100644 --- a/app/services/access_token_validation_service.rb +++ b/app/services/access_token_validation_service.rb @@ -1,34 +1,32 @@ -module AccessTokenValidationService +AccessTokenValidationService = Struct.new(:token) do # Results: VALID = :valid EXPIRED = :expired REVOKED = :revoked INSUFFICIENT_SCOPE = :insufficient_scope - class << self - def validate(token, scopes: []) - if token.expired? - return EXPIRED + def validate(scopes: []) + if token.expired? + return EXPIRED - elsif token.revoked? - return REVOKED + elsif token.revoked? + return REVOKED - elsif !self.sufficient_scope?(token, scopes) - return INSUFFICIENT_SCOPE + elsif !self.include_any_scope?(scopes) + return INSUFFICIENT_SCOPE - else - return VALID - end + else + return VALID end + end - # True if the token's scope contains any of the required scopes. - def sufficient_scope?(token, required_scopes) - if required_scopes.blank? - true - else - # Check whether the token is allowed access to any of the required scopes. - Set.new(required_scopes).intersection(Set.new(token.scopes)).present? - end + # True if the token's scope contains any of the passed scopes. + def include_any_scope?(scopes) + if scopes.blank? + true + else + # Check whether the token is allowed access to any of the required scopes. + Set.new(scopes).intersection(Set.new(token.scopes)).present? end end end diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index 563224a580f..df6db140d0e 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -47,7 +47,7 @@ module API access_token = find_access_token return nil unless access_token - case AccessTokenValidationService.validate(access_token, scopes: scopes) + case AccessTokenValidationService.new(access_token).validate(scopes: scopes) when AccessTokenValidationService::INSUFFICIENT_SCOPE raise InsufficientScopeError.new(scopes) @@ -96,7 +96,7 @@ module API access_token = PersonalAccessToken.active.find_by_token(token_string) return unless access_token - if AccessTokenValidationService.sufficient_scope?(access_token, scopes) + if AccessTokenValidationService.new(access_token).include_any_scope?(scopes) User.find(access_token.user_id) end end diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index c21afaa1551..2879a4d2f5d 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -119,7 +119,7 @@ module Gitlab end def token_has_scope?(token) - AccessTokenValidationService.sufficient_scope?(token, ['api']) + AccessTokenValidationService.new(token).include_any_scope?(['api']) end def lfs_token_check(login, password) diff --git a/spec/services/access_token_validation_service_spec.rb b/spec/services/access_token_validation_service_spec.rb index 332e745aa36..87f093ee8ce 100644 --- a/spec/services/access_token_validation_service_spec.rb +++ b/spec/services/access_token_validation_service_spec.rb @@ -1,41 +1,41 @@ require 'spec_helper' describe AccessTokenValidationService, services: true do - describe ".sufficient_scope?" do + describe ".include_any_scope?" do it "returns true if the required scope is present in the token's scopes" do token = double("token", scopes: [:api, :read_user]) - expect(described_class.sufficient_scope?(token, [:api])).to be(true) + expect(described_class.new(token).include_any_scope?([:api])).to be(true) end it "returns true if more than one of the required scopes is present in the token's scopes" do token = double("token", scopes: [:api, :read_user, :other_scope]) - expect(described_class.sufficient_scope?(token, [:api, :other_scope])).to be(true) + expect(described_class.new(token).include_any_scope?([:api, :other_scope])).to be(true) end it "returns true if the list of required scopes is an exact match for the token's scopes" do token = double("token", scopes: [:api, :read_user, :other_scope]) - expect(described_class.sufficient_scope?(token, [:api, :read_user, :other_scope])).to be(true) + expect(described_class.new(token).include_any_scope?([:api, :read_user, :other_scope])).to be(true) end it "returns true if the list of required scopes contains all of the token's scopes, in addition to others" do token = double("token", scopes: [:api, :read_user]) - expect(described_class.sufficient_scope?(token, [:api, :read_user, :other_scope])).to be(true) + expect(described_class.new(token).include_any_scope?([:api, :read_user, :other_scope])).to be(true) end it 'returns true if the list of required scopes is blank' do token = double("token", scopes: []) - expect(described_class.sufficient_scope?(token, [])).to be(true) + expect(described_class.new(token).include_any_scope?([])).to be(true) end it "returns false if there are no scopes in common between the required scopes and the token scopes" do token = double("token", scopes: [:api, :read_user]) - expect(described_class.sufficient_scope?(token, [:other_scope])).to be(false) + expect(described_class.new(token).include_any_scope?([:other_scope])).to be(false) end end end -- cgit v1.2.1 From 5becbe2495850923604c71b4c807666ea94819b3 Mon Sep 17 00:00:00 2001 From: Timothy Andrew <mail@timothyandrew.net> Date: Mon, 5 Dec 2016 22:58:19 +0530 Subject: Rename the `token_has_scope?` method. `valid_api_token?` is a better name. Scopes are just (potentially) one facet of a "valid" token. --- lib/gitlab/auth.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 2879a4d2f5d..8dda65c71ef 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -111,14 +111,14 @@ module Gitlab end def valid_oauth_token?(token) - token && token.accessible? && token_has_scope?(token) + token && token.accessible? && valid_api_token?(token) end def valid_personal_access_token?(token, user) - token && token.user == user && token_has_scope?(token) + token && token.user == user && valid_api_token?(token) end - def token_has_scope?(token) + def valid_api_token?(token) AccessTokenValidationService.new(token).include_any_scope?(['api']) end -- cgit v1.2.1 From eb434b15ebbc7d0b7ed79bb2daa45601e3c918ca Mon Sep 17 00:00:00 2001 From: Timothy Andrew <mail@timothyandrew.net> Date: Fri, 16 Dec 2016 14:57:09 +0530 Subject: Make `ChangePersonalAccessTokensDefaultBackToEmptyArray` a "post" migration. If we leave this as a regular migration, we could have the following flow: 1. Application knows nothing about scopes. 2. First migration runs, all existing personal access tokens have `api` scope 3. Application still knows nothing about scopes. 4. Second migration runs, all tokens created after this point have no scope 5. Application still knows nothing about scopes. 6. Tokens created at this time _should have the API scope, but instead have no scope_ 7. Application code is reloaded, application knows about scopes 8. Tokens created after this point only have no scope if the user deliberately chooses to have no scopes. Point #6 is the problem here. To avoid this, we move the second migration to a "post" migration, which runs after the application code is deployed/reloaded. --- ...sonal_access_tokens_default_back_to_empty_array.rb | 17 ----------------- ...sonal_access_tokens_default_back_to_empty_array.rb | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 17 deletions(-) delete mode 100644 db/migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb create mode 100644 db/post_migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb diff --git a/db/migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb b/db/migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb deleted file mode 100644 index c8ceb116b8a..00000000000 --- a/db/migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb +++ /dev/null @@ -1,17 +0,0 @@ -# The default needs to be `[]`, but all existing access tokens need to have `scopes` set to `['api']`. -# It's easier to achieve this by adding the column with the `['api']` default, and then changing the default to -# `[]`. - -class ChangePersonalAccessTokensDefaultBackToEmptyArray < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - - DOWNTIME = false - - def up - change_column_default :personal_access_tokens, :scopes, [].to_yaml - end - - def down - change_column_default :personal_access_tokens, :scopes, ['api'].to_yaml - end -end diff --git a/db/post_migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb b/db/post_migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb new file mode 100644 index 00000000000..7df561d82dd --- /dev/null +++ b/db/post_migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb @@ -0,0 +1,19 @@ +# The default needs to be `[]`, but all existing access tokens need to have `scopes` set to `['api']`. +# It's easier to achieve this by adding the column with the `['api']` default (regular migration), and +# then changing the default to `[]` (in this post-migration). +# +# Details: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951#note_19721973 + +class ChangePersonalAccessTokensDefaultBackToEmptyArray < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + change_column_default :personal_access_tokens, :scopes, [].to_yaml + end + + def down + change_column_default :personal_access_tokens, :scopes, ['api'].to_yaml + end +end -- cgit v1.2.1 From 731bf34223efc9e0f28b1c3e77e1dbbf8bd5e601 Mon Sep 17 00:00:00 2001 From: Munken <mm.munk@gmail.com> Date: Fri, 16 Dec 2016 11:02:36 +0000 Subject: Clearer comment as to why the procedure is needed --- vendor/assets/javascripts/katex.js | 2 +- vendor/assets/stylesheets/katex.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vendor/assets/javascripts/katex.js b/vendor/assets/javascripts/katex.js index 2ce3a5e8d4d..beb31ca6a7e 100644 --- a/vendor/assets/javascripts/katex.js +++ b/vendor/assets/javascripts/katex.js @@ -31,7 +31,7 @@ /* Here is how to build a version of KaTeX that works with gitlab. - The problem is that the procedure for changing font location doesn't work for ''. + The problem is that the standard procedure for changing font location doesn't work for the empty string. 1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do. 2. make (requires node) diff --git a/vendor/assets/stylesheets/katex.css b/vendor/assets/stylesheets/katex.css index 2d0b4635ccf..3e62df2329c 100644 --- a/vendor/assets/stylesheets/katex.css +++ b/vendor/assets/stylesheets/katex.css @@ -31,7 +31,7 @@ SOFTWARE. /* Here is how to build a version of KaTeX that works with gitlab. - The problem is that the procedure for changing font location doesn't work for ''. + The problem is that the standard procedure for changing font location doesn't work for the empty string. 1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do. 2. make (requires node) -- cgit v1.2.1 From 3b4e81eed50dac796de5720b9975125dc8de609b Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Fri, 16 Dec 2016 12:12:53 +0200 Subject: BB importer: Milestone importer --- lib/bitbucket/representation/issue.rb | 4 ++++ lib/gitlab/bitbucket_import/importer.rb | 2 ++ spec/lib/bitbucket/representation/issue_spec.rb | 6 ++++++ 3 files changed, 12 insertions(+) diff --git a/lib/bitbucket/representation/issue.rb b/lib/bitbucket/representation/issue.rb index ffe8a65d839..3af731753d1 100644 --- a/lib/bitbucket/representation/issue.rb +++ b/lib/bitbucket/representation/issue.rb @@ -27,6 +27,10 @@ module Bitbucket raw['title'] end + def milestone + raw.dig('milestone', 'name') + end + def created_at raw['created_on'] end diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 567f2b314aa..53c95ea4079 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -67,6 +67,7 @@ module Gitlab description += issue.description label_name = issue.kind + milestone = issue.milestone ? project.milestones.find_or_create_by(title: issue.milestone) : nil issue = project.issues.create!( iid: issue.iid, @@ -74,6 +75,7 @@ module Gitlab description: description, state: issue.state, author_id: gitlab_user_id(project, issue.author), + milestone: milestone, created_at: issue.created_at, updated_at: issue.updated_at ) diff --git a/spec/lib/bitbucket/representation/issue_spec.rb b/spec/lib/bitbucket/representation/issue_spec.rb index e1f3419c77e..9a195bebd31 100644 --- a/spec/lib/bitbucket/representation/issue_spec.rb +++ b/spec/lib/bitbucket/representation/issue_spec.rb @@ -9,6 +9,12 @@ describe Bitbucket::Representation::Issue do it { expect(described_class.new('kind' => 'bug').kind).to eq('bug') } end + describe '#milestone' do + it { expect(described_class.new({ 'milestone' => { 'name' => '1.0' } }).milestone).to eq('1.0') } + it { expect(described_class.new({}).milestone).to be_nil } + end + + describe '#author' do it { expect(described_class.new({ 'reporter' => { 'username' => 'Ben' } }).author).to eq('Ben') } it { expect(described_class.new({}).author).to be_nil } -- cgit v1.2.1 From 8feba01708f10d0ea6f9bb4d49b4cbf05b47ab9b Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Fri, 16 Dec 2016 11:29:16 +0000 Subject: Fix specs in Ruby 2.1 Ruby 2.1 requires a basename argument to `Tempfile.open`, so just call it something that makes sense in context for the spec. --- spec/lib/gitlab/middleware/multipart_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb index c79c6494576..ab1ab22795c 100644 --- a/spec/lib/gitlab/middleware/multipart_spec.rb +++ b/spec/lib/gitlab/middleware/multipart_spec.rb @@ -7,7 +7,7 @@ describe Gitlab::Middleware::Multipart do let(:middleware) { described_class.new(app) } it 'opens top-level files' do - Tempfile.open do |tempfile| + Tempfile.open('top-level') do |tempfile| env = post_env({ 'file' => tempfile.path }, { 'file.name' => 'filename' }, Gitlab::Workhorse.secret, 'gitlab-workhorse') expect(app).to receive(:call) do |env| @@ -33,7 +33,7 @@ describe Gitlab::Middleware::Multipart do end it 'opens files one level deep' do - Tempfile.open do |tempfile| + Tempfile.open('one-level') do |tempfile| in_params = { 'user' => { 'avatar' => { '.name' => 'filename' } } } env = post_env({ 'user[avatar]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse') @@ -48,7 +48,7 @@ describe Gitlab::Middleware::Multipart do end it 'opens files two levels deep' do - Tempfile.open do |tempfile| + Tempfile.open('two-levels') do |tempfile| in_params = { 'project' => { 'milestone' => { 'themesong' => { '.name' => 'filename' } } } } env = post_env({ 'project[milestone][themesong]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse') -- cgit v1.2.1 From 18b65cb8e07548c67056fe7994f1cee6da4de08e Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Fri, 16 Dec 2016 12:29:29 +0100 Subject: Fix rubocop --- spec/models/project_services/chat_notification_service_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/models/project_services/chat_notification_service_spec.rb b/spec/models/project_services/chat_notification_service_spec.rb index b4fb1cd9ed9..c98e7ee14fd 100644 --- a/spec/models/project_services/chat_notification_service_spec.rb +++ b/spec/models/project_services/chat_notification_service_spec.rb @@ -2,7 +2,6 @@ require 'spec_helper' describe ChatNotificationService, models: true do describe "Associations" do - before do allow(subject).to receive(:activated?).and_return(true) end -- cgit v1.2.1 From ca16a6bdf8feb92ae2b24cff86143fdaf668ce7d Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 16 Dec 2016 11:30:22 +0000 Subject: Fix broken test --- spec/features/commits_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 23a504ff965..8f561c8f90b 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -107,7 +107,7 @@ describe 'Commits' do describe 'Cancel build' do it 'cancels build' do visit ci_status_path(pipeline) - click_on 'Cancel' + find('a.btn[title="Cancel"]').click expect(page).to have_content 'canceled' end end -- cgit v1.2.1 From 595da33d738041cf0b2c28a87989aa271f5019cc Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra <dimitriehoekstra@gmail.com> Date: Fri, 16 Dec 2016 13:05:44 +0100 Subject: fixed scss linting issue --- app/assets/stylesheets/pages/labels.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 703a429d63c..d129eb12a45 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -98,7 +98,7 @@ } .label { - padding: 8px 9px 9px 9px; + padding: 8px 9px 9px; font-size: 14px; } } -- cgit v1.2.1 From c945a0a7141ddf80e58e821178195cc48b8143f0 Mon Sep 17 00:00:00 2001 From: Adam Niedzielski <adamsunday@gmail.com> Date: Fri, 16 Dec 2016 13:24:03 +0100 Subject: Pass variables from deployment project services to CI runner This commit introduces the concept of deployment variables - variables that are collected from deployment services and passed to CI runner during a deployment build. Deployment services specify the variables by overriding "predefined_variables" method. This commit also configures variables for KubernetesService --- app/models/ci/build.rb | 3 +- app/models/project.rb | 6 ++++ app/models/project_services/deployment_service.rb | 4 +++ app/models/project_services/kubernetes_service.rb | 10 +++++++ .../unreleased/expose-deployment-variables.yml | 4 +++ doc/ci/variables/README.md | 15 ++++++++++ doc/project_services/kubernetes.md | 11 ++++++++ spec/models/build_spec.rb | 11 ++++++++ .../project_services/kubernetes_service_spec.rb | 33 ++++++++++++++++++++++ spec/models/project_spec.rb | 20 +++++++++++++ 10 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/expose-deployment-variables.yml diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index fdbf28a1d68..591aba6bdc9 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -155,7 +155,7 @@ module Ci end def has_environment? - self.environment.present? + environment.present? end def starts_environment? @@ -221,6 +221,7 @@ module Ci variables += pipeline.predefined_variables variables += runner.predefined_variables if runner variables += project.container_registry_variables + variables += project.deployment_variables if has_environment? variables += yaml_variables variables += user_variables variables += project.secret_variables diff --git a/app/models/project.rb b/app/models/project.rb index 2c726cfc5df..5f8058dac60 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1229,6 +1229,12 @@ class Project < ActiveRecord::Base end end + def deployment_variables + return [] unless deployment_service + + deployment_service.predefined_variables + end + def append_or_update_attribute(name, value) old_values = public_send(name.to_s) diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb index 55e98c31251..da6be9dd7b7 100644 --- a/app/models/project_services/deployment_service.rb +++ b/app/models/project_services/deployment_service.rb @@ -8,4 +8,8 @@ class DeploymentService < Service def supported_events [] end + + def predefined_variables + [] + end end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 80ae1191108..f5fbf8b353b 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -83,6 +83,16 @@ class KubernetesService < DeploymentService { success: false, result: err } end + def predefined_variables + variables = [ + { key: 'KUBE_URL', value: api_url, public: true }, + { key: 'KUBE_TOKEN', value: token, public: false }, + { key: 'KUBE_NAMESPACE', value: namespace, public: true } + ] + variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } if ca_pem.present? + variables + end + private def build_kubeclient(api_path = '/api', api_version = 'v1') diff --git a/changelogs/unreleased/expose-deployment-variables.yml b/changelogs/unreleased/expose-deployment-variables.yml new file mode 100644 index 00000000000..7663d5b6ae5 --- /dev/null +++ b/changelogs/unreleased/expose-deployment-variables.yml @@ -0,0 +1,4 @@ +--- +title: Pass variables from deployment project services to CI runner +merge_request: 8107 +author: diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index eb540a50606..baa5fc67816 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -13,6 +13,7 @@ this order: 1. [Secret variables](#secret-variables) 1. YAML-defined [job-level variables](../yaml/README.md#job-variables) 1. YAML-defined [global variables](../yaml/README.md#variables) +1. [Deployment variables](#deployment-variables) 1. [Predefined variables](#predefined-variables-environment-variables) (are the lowest in the chain) @@ -148,6 +149,20 @@ Secret variables can be added by going to your project's Once you set them, they will be available for all subsequent builds. +## Deployment variables + +>**Note:** +This feature requires GitLab CI 8.15 or higher. + +[Project services](../../project_services/project_services.md) that are +responsible for deployment configuration may define their own variables that +are set in the build environment. These variables are only defined for +[deployment builds](../environments.md). Please consult the documentation of +the project services that you are using to learn which variables they define. + +An example project service that defines deployment variables is +[Kubernetes Service](../../project_services/kubernetes.md). + ## Debug tracing > Introduced in GitLab Runner 1.7. diff --git a/doc/project_services/kubernetes.md b/doc/project_services/kubernetes.md index cb577b608b4..fda364b864e 100644 --- a/doc/project_services/kubernetes.md +++ b/doc/project_services/kubernetes.md @@ -36,3 +36,14 @@ to create one. You can also view or create service tokens in the Fill in the service token and namespace according to the values you just got. If the API is using a self-signed TLS certificate, you'll also need to include the `ca.crt` contents as the `Custom CA bundle`. + +## Deployment variables + +The Kubernetes service exposes following +[deployment variables](../ci/variables/README.md#deployment-variables) in the +GitLab CI build environment: + +- `KUBE_URL` - equal to the API URL +- `KUBE_TOKEN` +- `KUBE_NAMESPACE` +- `KUBE_CA_PEM` - only if a custom CA bundle was specified diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index d5f2ffcff59..6f1c2ae0fd8 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -506,6 +506,17 @@ describe Ci::Build, models: true do it { is_expected.to include({ key: 'CI_RUNNER_TAGS', value: 'docker, linux', public: true }) } end + context 'when build is for a deployment' do + let(:deployment_variable) { { key: 'KUBERNETES_TOKEN', value: 'TOKEN', public: false } } + + before do + build.environment = 'production' + allow(project).to receive(:deployment_variables).and_return([deployment_variable]) + end + + it { is_expected.to include(deployment_variable) } + end + context 'returns variables in valid order' do before do allow(build).to receive(:predefined_variables) { ['predefined'] } diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index ffb92012b89..3603602e41d 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -123,4 +123,37 @@ describe KubernetesService, models: true do end end end + + describe '#predefined_variables' do + before do + subject.api_url = 'https://kube.domain.com' + subject.token = 'token' + subject.namespace = 'my-project' + subject.ca_pem = 'CA PEM DATA' + end + + it 'sets KUBE_URL' do + expect(subject.predefined_variables).to include( + { key: 'KUBE_URL', value: 'https://kube.domain.com', public: true } + ) + end + + it 'sets KUBE_TOKEN' do + expect(subject.predefined_variables).to include( + { key: 'KUBE_TOKEN', value: 'token', public: false } + ) + end + + it 'sets KUBE_NAMESPACE' do + expect(subject.predefined_variables).to include( + { key: 'KUBE_NAMESPACE', value: 'my-project', public: true } + ) + end + + it 'sets KUBE_CA_PEM' do + expect(subject.predefined_variables).to include( + { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true } + ) + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 21ff238841e..c7d914a81f9 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1696,6 +1696,26 @@ describe Project, models: true do end end + describe '#deployment_variables' do + context 'when project has no deployment service' do + let(:project) { create(:empty_project) } + + it 'returns an empty array' do + expect(project.deployment_variables).to eq [] + end + end + + context 'when project has a deployment service' do + let(:project) { create(:kubernetes_project) } + + it 'returns variables from this service' do + expect(project.deployment_variables).to include( + { key: 'KUBE_TOKEN', value: project.kubernetes_service.token, public: false } + ) + end + end + end + def enable_lfs allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) end -- cgit v1.2.1 From df6c1b84a842d6dc54b27e396b60ffd4d7723c4a Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 16 Dec 2016 12:29:58 +0000 Subject: Prevent enviroment table to overflow when name has underscores Fix error in linter Add changelog entry --- .../environments/components/environment_item.js.es6 | 2 +- app/assets/stylesheets/pages/environments.scss | 14 ++++++++++---- changelogs/unreleased/25207-text-overflow-env-table.yml | 4 ++++ 3 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 changelogs/unreleased/25207-text-overflow-env-table.yml diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 2e046a60146..4674d5202e6 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -449,7 +449,7 @@ </span> </td> - <td> + <td class="environments-build-cell"> <a v-if="shouldRenderBuildName" class="build-link" :href="model.last_deployment.deployable.build_path"> diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 92dd9885ab8..3d60426de01 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -30,19 +30,25 @@ display: table-cell; } + .environments-name, .environments-commit, .environments-actions { width: 20%; } - .environments-deploy, - .environments-build, .environments-date { width: 10%; } - .environments-name { - width: 30%; + .environments-deploy, + .environments-build { + width: 15%; + } + + .environment-name, + .environments-build-cell, + .deployment-column { + word-break: break-all; } .deployment-column { diff --git a/changelogs/unreleased/25207-text-overflow-env-table.yml b/changelogs/unreleased/25207-text-overflow-env-table.yml new file mode 100644 index 00000000000..69348281a50 --- /dev/null +++ b/changelogs/unreleased/25207-text-overflow-env-table.yml @@ -0,0 +1,4 @@ +--- +title: Prevent enviroment table to overflow when name has underscores +merge_request: 8142 +author: -- cgit v1.2.1 From b0501c34c478a528f2aa7633dfa6d13e9c61af64 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Fri, 16 Dec 2016 15:40:38 +0200 Subject: BB importer: address review comment --- doc/workflow/importing/import_projects_from_bitbucket.md | 2 +- lib/gitlab/bitbucket_import/importer.rb | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/doc/workflow/importing/import_projects_from_bitbucket.md b/doc/workflow/importing/import_projects_from_bitbucket.md index 935d6288f3b..9e1e3c7ba08 100644 --- a/doc/workflow/importing/import_projects_from_bitbucket.md +++ b/doc/workflow/importing/import_projects_from_bitbucket.md @@ -20,7 +20,7 @@ It takes just a few steps to import your existing Bitbucket projects to GitLab. ![Import projects](bitbucket_importer/bitbucket_import_select_project.png) -A new GitLab project will be created with your imported data. Keep in mind that if you want to Bitbucket users +A new GitLab project will be created with your imported data. Keep in mind that if you want Bitbucket users to be linked to GitLab user you have to have all of them in GitLab in advance. They will be matched by their BitBucket username. ### Note diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 53c95ea4079..63a4407cb78 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -28,6 +28,7 @@ module Gitlab def handle_errors return unless errors.any? + project.update_column(:import_error, { message: 'The remote data could not be fully imported.', errors: errors @@ -35,15 +36,12 @@ module Gitlab end def gitlab_user_id(project, username) - if username - user = find_user(username) - (user && user.id) || project.creator_id - else - project.creator_id - end + user = find_user(username) + user.try(:id) || project.creator_id end def find_user(username) + return nil unless username User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", username) end -- cgit v1.2.1 From 27f271ee1ed977f8070f528f1d5e21ad577f5409 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Fri, 16 Dec 2016 14:54:23 +0100 Subject: Refactor Bitbucket import docs --- doc/README.md | 2 +- doc/integration/bitbucket.md | 18 +++--- .../img/bitbucket_oauth_settings_page.png | Bin 30275 -> 5607 bytes .../bitbucket_import_grant_access.png | Bin 30083 -> 0 bytes .../bitbucket_import_new_project.png | Bin 16502 -> 0 bytes .../bitbucket_import_select_bitbucket.png | Bin 46606 -> 0 bytes .../bitbucket_import_select_project.png | Bin 15288 -> 0 bytes .../img/bitbucket_import_grant_access.png | Bin 0 -> 7248 bytes .../importing/img/bitbucket_import_new_project.png | Bin 0 -> 1316 bytes .../img/bitbucket_import_select_project.png | Bin 0 -> 8688 bytes ...ort_projects_from_github_select_auth_method.png | Bin 17613 -> 17612 bytes .../importing/import_projects_from_bitbucket.md | 64 ++++++++++++++++----- 12 files changed, 59 insertions(+), 25 deletions(-) delete mode 100644 doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.png delete mode 100644 doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png delete mode 100644 doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png delete mode 100644 doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png create mode 100644 doc/workflow/importing/img/bitbucket_import_grant_access.png create mode 100644 doc/workflow/importing/img/bitbucket_import_new_project.png create mode 100644 doc/workflow/importing/img/bitbucket_import_select_project.png diff --git a/doc/README.md b/doc/README.md index eba1e9845b1..a60a5359540 100644 --- a/doc/README.md +++ b/doc/README.md @@ -8,7 +8,7 @@ - [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab. - [Container Registry](user/project/container_registry.md) Learn how to use GitLab Container Registry. - [GitLab basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab. -- [Importing to GitLab](workflow/importing/README.md). +- [Importing to GitLab](workflow/importing/README.md) Import your projects from GitHub, Bitbucket, GitLab.com, FogBugz and SVN into GitLab. - [Importing and exporting projects between instances](user/project/settings/import_export.md). - [Markdown](user/markdown.md) GitLab's advanced formatting system. - [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab. diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index 9cdb101f457..5df6e103f42 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -18,8 +18,10 @@ Bitbucket.org. ## Bitbucket OmniAuth provider > **Note:** -Make sure to first follow the [Initial OmniAuth configuration][init-oauth] -before proceeding with setting up the Bitbucket integration. +GitLab 8.15 significantly simplified the way to integrate Bitbucket.org with +GitLab. You are encouraged to upgrade your GitLab instance if you haven't done +already. If you're using GitLab 8.14 and below, [use the previous integration +docs][bb-old]. To enable the Bitbucket OmniAuth provider you must register your application with Bitbucket.org. Bitbucket will generate an application ID and secret key for @@ -111,16 +113,12 @@ well, the user will be returned to GitLab and will be signed in. ## Bitbucket project import -You should be able to see the "Import projects from Bitbucket" option on the New Project page -enabled. - -## Acknowledgements - -Special thanks to the writer behind the following article: - -- http://stratus3d.com/blog/2015/09/06/migrating-from-bitbucket-to-local-gitlab-server/ +Once the above configuration is set up, you can use Bitbucket to sign into +GitLab and [start importing your projects][bb-import]. [init-oauth]: omniauth.md#initial-omniauth-configuration +[bb-import]: ../workflow/importing/import_projects_from_bitbucket.md +[bb-old]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-14-stable/doc/integration/bitbucket.md [bitbucket-docs]: https://confluence.atlassian.com/bitbucket/use-the-ssh-protocol-with-bitbucket-cloud-221449711.html#UsetheSSHprotocolwithBitbucketCloud-KnownhostorBitbucket%27spublickeyfingerprints [reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure [restart GitLab]: ../administration/restart_gitlab.md#installations-from-source diff --git a/doc/integration/img/bitbucket_oauth_settings_page.png b/doc/integration/img/bitbucket_oauth_settings_page.png index 24acc6e1f5a..21ce82a6074 100644 Binary files a/doc/integration/img/bitbucket_oauth_settings_page.png and b/doc/integration/img/bitbucket_oauth_settings_page.png differ diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.png deleted file mode 100644 index df55a081803..00000000000 Binary files a/doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.png and /dev/null differ diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png deleted file mode 100644 index 5253889d251..00000000000 Binary files a/doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png and /dev/null differ diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png deleted file mode 100644 index ffa87ce5b2e..00000000000 Binary files a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png and /dev/null differ diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png deleted file mode 100644 index 1a5661de75d..00000000000 Binary files a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png and /dev/null differ diff --git a/doc/workflow/importing/img/bitbucket_import_grant_access.png b/doc/workflow/importing/img/bitbucket_import_grant_access.png new file mode 100644 index 00000000000..429904e621d Binary files /dev/null and b/doc/workflow/importing/img/bitbucket_import_grant_access.png differ diff --git a/doc/workflow/importing/img/bitbucket_import_new_project.png b/doc/workflow/importing/img/bitbucket_import_new_project.png new file mode 100644 index 00000000000..8ed528c2f09 Binary files /dev/null and b/doc/workflow/importing/img/bitbucket_import_new_project.png differ diff --git a/doc/workflow/importing/img/bitbucket_import_select_project.png b/doc/workflow/importing/img/bitbucket_import_select_project.png new file mode 100644 index 00000000000..1bca6166ec8 Binary files /dev/null and b/doc/workflow/importing/img/bitbucket_import_select_project.png differ diff --git a/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png b/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png index f50d9266991..1ccb38a815e 100644 Binary files a/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png and b/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png differ diff --git a/doc/workflow/importing/import_projects_from_bitbucket.md b/doc/workflow/importing/import_projects_from_bitbucket.md index 935d6288f3b..fbbbc7f4a72 100644 --- a/doc/workflow/importing/import_projects_from_bitbucket.md +++ b/doc/workflow/importing/import_projects_from_bitbucket.md @@ -1,27 +1,63 @@ # Import your project from Bitbucket to GitLab -It takes just a few steps to import your existing Bitbucket projects to GitLab. But keep in mind that it is possible only if Bitbucket support is enabled on your GitLab instance. You can read more about Bitbucket support [here](../../integration/bitbucket.md). +Import your projects from Bitbucket to GitLab with minimal effort. + +## Overview + +>**Note:** +The [Bitbucket integration][bb-import] must be first enabled in order to be +able to import your projects from Bitbucket. Ask your GitLab administrator +to enable this if not already. + +- At its current state, the Bitbucket importer can import: + - the repository description (GitLab 7.7+) + - the Git repository data (GitLab 7.7+) + - the issues (GitLab 7.7+) + - the pull requests (GitLab 8.4+) + - the wiki pages (GitLab 8.4+) + - the milestones (GitLab 8.7+) + - the labels (GitLab 8.7+) + - the release note descriptions (GitLab 8.12+) +- References to pull requests and issues are preserved (GitLab 8.7+) +- Repository public access is retained. If a repository is private in Bitbucket + it will be created as private in GitLab as well. -* Sign in to GitLab.com and go to your dashboard +Milestones and wiki pages are not imported from Bitbucket. -* Click on "New project" +## How it works -![New project in GitLab](bitbucket_importer/bitbucket_import_new_project.png) +When issues/pull requests are being imported, the Bitbucket importer tries to find +the Bitbucket author/assignee in GitLab's database using the Bitbucket ID. For this +to work, the Bitbucket author/assignee should have signed in beforehand in GitLab +and [**associated their Bitbucket account**][social sign-in]. If the user is not +found in GitLab's database, the project creator (most of the times the current +user that started the import process) is set as the author, but a reference on +the issue about the original Bitbucket author is kept. -* Click on the "Bitbucket" button +The importer will create any new namespaces (groups) if they don't exist or in +the case the namespace is taken, the repository will be imported under the user's +namespace that started the import process. -![Bitbucket](bitbucket_importer/bitbucket_import_select_bitbucket.png) +## Importing your Bitbucket repositories -* Grant GitLab access to your Bitbucket account +1. Sign in to GitLab and go to your dashboard. +1. Click on **New project**. -![Grant access](bitbucket_importer/bitbucket_import_grant_access.png) + ![New project in GitLab](img/bitbucket_import_new_project.png) -* Click on the projects that you'd like to import or "Import all projects" +1. Click on the "Bitbucket" button -![Import projects](bitbucket_importer/bitbucket_import_select_project.png) + ![Bitbucket](img/import_projects_from_github_new_project_page.png) -A new GitLab project will be created with your imported data. Keep in mind that if you want to Bitbucket users -to be linked to GitLab user you have to have all of them in GitLab in advance. They will be matched by their BitBucket username. +1. Grant GitLab access to your Bitbucket account -### Note -Milestones and wiki pages are not imported from Bitbucket. + ![Grant access](img/bitbucket_import_grant_access.png) + +1. Click on the projects that you'd like to import or **Import all projects**. + You can also select the namespace under which each project will be + imported. + + ![Import projects](img/bitbucket_import_select_project.png) + +[bb-import]: ../../integration/bitbucket.md +[social sign-in]: ../../user/profile/account/social_sign_in.md -- cgit v1.2.1 From 55f224e4e785d0e1515ac4a840e689cb6d9c7d24 Mon Sep 17 00:00:00 2001 From: Drew Blessing <drew@gitlab.com> Date: Mon, 17 Oct 2016 09:39:14 -0500 Subject: Add GitLab host to 2FA QR and manual info The two factor authentication account string only had the user's email address. This led to ambiguous entries in two factor code generating apps. This adds the GitLab host to the account string in the standard format (according to Google). No matter the code generator this change disambiguates the entry. --- app/controllers/profiles/two_factor_auths_controller.rb | 8 ++++++-- app/views/profiles/two_factor_auths/show.html.haml | 2 +- changelogs/unreleased/add_info_to_qr.yml | 4 ++++ 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/add_info_to_qr.yml diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 9eb75bb3891..18044ca78e2 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -22,6 +22,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController end @qr_code = build_qr_code + @account_string = account_string setup_u2f_registration end @@ -78,11 +79,14 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController private def build_qr_code - issuer = "#{issuer_host} | #{current_user.email}" - uri = current_user.otp_provisioning_uri(current_user.email, issuer: issuer) + uri = current_user.otp_provisioning_uri(account_string, issuer: issuer_host) RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3) end + def account_string + "#{issuer_host}:#{current_user.email}" + end + def issuer_host Gitlab.config.gitlab.host end diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index 03ac739ade5..558a1d56151 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -30,7 +30,7 @@ To add the entry manually, provide the following details to the application on your phone. %p.prepend-top-0.append-bottom-0 Account: - = current_user.email + = @account_string %p.prepend-top-0.append-bottom-0 Key: = current_user.otp_secret.scan(/.{4}/).join(' ') diff --git a/changelogs/unreleased/add_info_to_qr.yml b/changelogs/unreleased/add_info_to_qr.yml new file mode 100644 index 00000000000..a4b0354a9c9 --- /dev/null +++ b/changelogs/unreleased/add_info_to_qr.yml @@ -0,0 +1,4 @@ +--- +title: Add GitLab host to 2FA QR code and manual info +merge_request: 6941 +author: -- cgit v1.2.1 From 445e83ebee1a224bd576db738f4bf597b37a5f6c Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Fri, 16 Dec 2016 16:21:33 +0100 Subject: Use new generic image for project import --- .../import_projects_from_github_new_project_page.png | Bin 11047 -> 0 bytes .../img/import_projects_from_new_project_page.png | Bin 0 -> 36821 bytes .../importing/import_projects_from_bitbucket.md | 2 +- doc/workflow/importing/import_projects_from_github.md | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 doc/workflow/importing/img/import_projects_from_github_new_project_page.png create mode 100644 doc/workflow/importing/img/import_projects_from_new_project_page.png diff --git a/doc/workflow/importing/img/import_projects_from_github_new_project_page.png b/doc/workflow/importing/img/import_projects_from_github_new_project_page.png deleted file mode 100644 index b23ade4480c..00000000000 Binary files a/doc/workflow/importing/img/import_projects_from_github_new_project_page.png and /dev/null differ diff --git a/doc/workflow/importing/img/import_projects_from_new_project_page.png b/doc/workflow/importing/img/import_projects_from_new_project_page.png new file mode 100644 index 00000000000..97ca30b2087 Binary files /dev/null and b/doc/workflow/importing/img/import_projects_from_new_project_page.png differ diff --git a/doc/workflow/importing/import_projects_from_bitbucket.md b/doc/workflow/importing/import_projects_from_bitbucket.md index fbbbc7f4a72..f0b73ccbcd2 100644 --- a/doc/workflow/importing/import_projects_from_bitbucket.md +++ b/doc/workflow/importing/import_projects_from_bitbucket.md @@ -47,7 +47,7 @@ namespace that started the import process. 1. Click on the "Bitbucket" button - ![Bitbucket](img/import_projects_from_github_new_project_page.png) + ![Bitbucket](img/import_projects_from_new_project_page.png) 1. Grant GitLab access to your Bitbucket account diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md index c36dfdb78ec..b3660aa8030 100644 --- a/doc/workflow/importing/import_projects_from_github.md +++ b/doc/workflow/importing/import_projects_from_github.md @@ -40,7 +40,7 @@ namespace that started the import process. The importer page is visible when you create a new project. -![New project page on GitLab](img/import_projects_from_github_new_project_page.png) +![New project page on GitLab](img/import_projects_from_new_project_page.png) Click on the **GitHub** link and the import authorization process will start. There are two ways to authorize access to your GitHub repositories: -- cgit v1.2.1 From 7985b52286237b3801fc112b8bf3841599931c23 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Fri, 16 Dec 2016 17:35:31 +0200 Subject: BB importer: Adressed more review comments --- changelogs/unreleased/bitbucket-oauth2.yml | 4 ++++ lib/gitlab/bitbucket_import/importer.rb | 34 +++++++++++++++++------------- 2 files changed, 23 insertions(+), 15 deletions(-) create mode 100644 changelogs/unreleased/bitbucket-oauth2.yml diff --git a/changelogs/unreleased/bitbucket-oauth2.yml b/changelogs/unreleased/bitbucket-oauth2.yml new file mode 100644 index 00000000000..97d82518b7b --- /dev/null +++ b/changelogs/unreleased/bitbucket-oauth2.yml @@ -0,0 +1,4 @@ +--- +title: Refactor Bitbucket importer to use BitBucket API Version 2 +merge_request: +author: diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 63a4407cb78..f3760640655 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -6,7 +6,7 @@ module Gitlab { title: 'proposal', color: '#69D100' }, { title: 'task', color: '#7F8C8D' }].freeze - attr_reader :project, :client, :errors + attr_reader :project, :client, :errors, :users def initialize(project) @project = project @@ -14,6 +14,7 @@ module Gitlab @formatter = Gitlab::ImportFormatter.new @labels = {} @errors = [] + @users = {} end def execute @@ -36,17 +37,18 @@ module Gitlab end def gitlab_user_id(project, username) - user = find_user(username) - user.try(:id) || project.creator_id + find_user_id(username) || project.creator_id end - def find_user(username) + def find_user_id(username) return nil unless username - User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", username) - end - def existing_gitlab_user?(username) - username && find_user(username) + return users[username] if users.key?(username) + + users[username] = User.select(:id) + .joins(:identities) + .find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", username) + .try(:id) end def repo @@ -58,16 +60,18 @@ module Gitlab create_labels + gitlab_issue = nil + client.issues(repo).each do |issue| begin description = '' - description += @formatter.author_line(issue.author) unless existing_gitlab_user?(issue.author) + description += @formatter.author_line(issue.author) unless find_user_id(issue.author) description += issue.description label_name = issue.kind milestone = issue.milestone ? project.milestones.find_or_create_by(title: issue.milestone) : nil - issue = project.issues.create!( + gitlab_issue = project.issues.create!( iid: issue.iid, title: issue.title, description: description, @@ -81,9 +85,9 @@ module Gitlab errors << { type: :issue, iid: issue.iid, errors: e.message } end - issue.labels << @labels[label_name] + gitlab_issue.labels << @labels[label_name] - if issue.persisted? + if gitlab_issue.persisted? client.issue_comments(repo, issue.iid).each do |comment| # The note can be blank for issue service messages like "Changed title: ..." # We would like to import those comments as well but there is no any @@ -93,11 +97,11 @@ module Gitlab next unless comment.note.present? note = '' - note += @formatter.author_line(comment.author) unless existing_gitlab_user?(comment.author) + note += @formatter.author_line(comment.author) unless find_user_id(comment.author) note += comment.note begin - issue.notes.create!( + gitlab_issue.notes.create!( project: project, note: note, author_id: gitlab_user_id(project, comment.author), @@ -124,7 +128,7 @@ module Gitlab pull_requests.each do |pull_request| begin description = '' - description += @formatter.author_line(pull_request.author) unless existing_gitlab_user?(pull_request.author) + description += @formatter.author_line(pull_request.author) unless find_user_id(pull_request.author) description += pull_request.description merge_request = project.merge_requests.create( -- cgit v1.2.1 From 20e472d946d7cc4a2b9dd91264458b1c4ceb5ab6 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Fri, 16 Dec 2016 17:40:29 +0200 Subject: BB importer: Fix documantation --- doc/workflow/importing/import_projects_from_bitbucket.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/doc/workflow/importing/import_projects_from_bitbucket.md b/doc/workflow/importing/import_projects_from_bitbucket.md index f0b73ccbcd2..b6d47e5afa2 100644 --- a/doc/workflow/importing/import_projects_from_bitbucket.md +++ b/doc/workflow/importing/import_projects_from_bitbucket.md @@ -13,16 +13,14 @@ to enable this if not already. - the repository description (GitLab 7.7+) - the Git repository data (GitLab 7.7+) - the issues (GitLab 7.7+) + - the issue comments (GitLab 8.15+) - the pull requests (GitLab 8.4+) - - the wiki pages (GitLab 8.4+) - - the milestones (GitLab 8.7+) - - the labels (GitLab 8.7+) - - the release note descriptions (GitLab 8.12+) + - the pull request comments (GitLab 8.15+) + - the milestones (GitLab 8.15+) - References to pull requests and issues are preserved (GitLab 8.7+) - Repository public access is retained. If a repository is private in Bitbucket it will be created as private in GitLab as well. -Milestones and wiki pages are not imported from Bitbucket. ## How it works -- cgit v1.2.1 From 170efaaba273792ddffc2806ef1501f33d87a5a2 Mon Sep 17 00:00:00 2001 From: Rydkin Maxim <maks.rydkin@gmail.com> Date: Fri, 16 Dec 2016 01:14:20 +0300 Subject: Enable Style/MultilineOperationIndentation in Rubocop, fixes #25741 --- .rubocop.yml | 3 ++- app/controllers/jwt_controller.rb | 2 +- app/controllers/sessions_controller.rb | 2 +- app/helpers/form_helper.rb | 12 ++++++------ app/helpers/nav_helper.rb | 18 +++++++++--------- app/helpers/tab_helper.rb | 6 +++--- app/models/member.rb | 4 ++-- app/models/network/graph.rb | 4 ++-- app/models/project_services/issue_tracker_service.rb | 4 ++-- app/models/user.rb | 2 +- app/policies/note_policy.rb | 2 +- app/policies/project_policy.rb | 12 ++++++------ app/services/groups/update_service.rb | 2 +- app/services/issues/update_service.rb | 2 +- app/services/merge_requests/build_service.rb | 2 +- app/services/merge_requests/update_service.rb | 2 +- app/services/projects/update_service.rb | 2 +- config/initializers/sidekiq.rb | 2 +- lib/gitlab/email/reply_parser.rb | 2 +- spec/lib/gitlab/gfm/reference_rewriter_spec.rb | 2 +- spec/requests/api/projects_spec.rb | 2 +- 21 files changed, 45 insertions(+), 44 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 13df3f99613..80eb4a5c19e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -292,7 +292,8 @@ Style/MultilineMethodDefinitionBraceLayout: # Checks indentation of binary operations that span more than one line. Style/MultilineOperationIndentation: - Enabled: false + Enabled: true + EnforcedStyle: indented # Avoid multi-line `? :` (the ternary operator), use if/unless instead. Style/MultilineTernaryOperator: diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index c736200a104..c2e4d62b50b 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -26,7 +26,7 @@ class JwtController < ApplicationController @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip) render_unauthorized unless @authentication_result.success? && - (@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User)) + (@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User)) end rescue Gitlab::Auth::MissingPersonalTokenError render_missing_personal_token diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 8c698695202..93a180b9036 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -114,7 +114,7 @@ class SessionsController < Devise::SessionsController def valid_otp_attempt?(user) user.validate_and_consume_otp!(user_params[:otp_attempt]) || - user.invalidate_otp_backup_code!(user_params[:otp_attempt]) + user.invalidate_otp_backup_code!(user_params[:otp_attempt]) end def log_audit_event(user, options = {}) diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb index 6a43be2cf3e..1182939f656 100644 --- a/app/helpers/form_helper.rb +++ b/app/helpers/form_helper.rb @@ -7,12 +7,12 @@ module FormHelper content_tag(:div, class: 'alert alert-danger', id: 'error_explanation') do content_tag(:h4, headline) << - content_tag(:ul) do - model.errors.full_messages. - map { |msg| content_tag(:li, msg) }. - join. - html_safe - end + content_tag(:ul) do + model.errors.full_messages. + map { |msg| content_tag(:li, msg) }. + join. + html_safe + end end end end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index a3331dc80cb..e21178c7377 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -7,12 +7,12 @@ module NavHelper def page_gutter_class if current_path?('merge_requests#show') || - current_path?('merge_requests#diffs') || - current_path?('merge_requests#commits') || - current_path?('merge_requests#builds') || - current_path?('merge_requests#conflicts') || - current_path?('merge_requests#pipelines') || - current_path?('issues#show') + current_path?('merge_requests#diffs') || + current_path?('merge_requests#commits') || + current_path?('merge_requests#builds') || + current_path?('merge_requests#conflicts') || + current_path?('merge_requests#pipelines') || + current_path?('issues#show') if cookies[:collapsed_gutter] == 'true' "page-gutter right-sidebar-collapsed" else @@ -21,9 +21,9 @@ module NavHelper elsif current_path?('builds#show') "page-gutter build-sidebar right-sidebar-expanded" elsif current_path?('wikis#show') || - current_path?('wikis#edit') || - current_path?('wikis#history') || - current_path?('wikis#git_access') + current_path?('wikis#edit') || + current_path?('wikis#history') || + current_path?('wikis#git_access') "page-gutter wiki-sidebar right-sidebar-expanded" end end diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index 563ddd2a511..547f6258909 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -106,9 +106,9 @@ module TabHelper def branches_tab_class if current_controller?(:protected_branches) || - current_controller?(:branches) || - current_page?(namespace_project_repository_path(@project.namespace, - @project)) + current_controller?(:branches) || + current_page?(namespace_project_repository_path(@project.namespace, + @project)) 'active' end end diff --git a/app/models/member.rb b/app/models/member.rb index 3b65587c66b..d29b3c68cf6 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -89,8 +89,8 @@ class Member < ActiveRecord::Base member = if user.is_a?(User) source.members.find_by(user_id: user.id) || - source.requesters.find_by(user_id: user.id) || - source.members.build(user_id: user.id) + source.requesters.find_by(user_id: user.id) || + source.members.build(user_id: user.id) else source.members.build(invite_email: user) end diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb index 345041a6ad1..b524ca50ee8 100644 --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb @@ -161,8 +161,8 @@ module Network def is_overlap?(range, overlap_space) range.each do |i| if i != range.first && - i != range.last && - @commits[i].spaces.include?(overlap_space) + i != range.last && + @commits[i].spaces.include?(overlap_space) return true end diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 207bb816ad1..bce2cdd5516 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -85,8 +85,8 @@ class IssueTrackerService < Service def enabled_in_gitlab_config Gitlab.config.issues_tracker && - Gitlab.config.issues_tracker.values.any? && - issues_tracker + Gitlab.config.issues_tracker.values.any? && + issues_tracker end def issues_tracker diff --git a/app/models/user.rb b/app/models/user.rb index 1bd28203523..3f8bbbc425d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -390,7 +390,7 @@ class User < ActiveRecord::Base def namespace_uniq # Return early if username already failed the first uniqueness validation return if errors.key?(:username) && - errors[:username].include?('has already been taken') + errors[:username].include?('has already been taken') existing_namespace = Namespace.by_path(username) if existing_namespace && existing_namespace != namespace diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb index 83847466ee2..5326061bd07 100644 --- a/app/policies/note_policy.rb +++ b/app/policies/note_policy.rb @@ -12,7 +12,7 @@ class NotePolicy < BasePolicy end if @subject.for_merge_request? && - @subject.noteable.author == @user + @subject.noteable.author == @user can! :resolve_note end end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index d5aadfce76a..b5db9c12622 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -3,7 +3,7 @@ class ProjectPolicy < BasePolicy team_access!(user) owner = project.owner == user || - (project.group && project.group.has_owner?(user)) + (project.group && project.group.has_owner?(user)) owner_access! if user.admin? || owner team_member_owner_access! if owner @@ -13,7 +13,7 @@ class ProjectPolicy < BasePolicy public_access! if project.request_access_enabled && - !(owner || user.admin? || project.team.member?(user) || project_group_member?(user)) + !(owner || user.admin? || project.team.member?(user) || project_group_member?(user)) can! :request_access end end @@ -244,10 +244,10 @@ class ProjectPolicy < BasePolicy def project_group_member?(user) project.group && - ( - project.group.members.exists?(user_id: user.id) || - project.group.requesters.exists?(user_id: user.id) - ) + ( + project.group.members.exists?(user_id: user.id) || + project.group.requesters.exists?(user_id: user.id) + ) end def named_abilities(name) diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb index 99ad12b1003..fff2273f402 100644 --- a/app/services/groups/update_service.rb +++ b/app/services/groups/update_service.rb @@ -5,7 +5,7 @@ module Groups new_visibility = params[:visibility_level] if new_visibility && new_visibility.to_i != group.visibility_level unless can?(current_user, :change_visibility_level, group) && - Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) + Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) deny_visibility_level(group, new_visibility) return group diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index a2111b3806b..78cbf94ec69 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -10,7 +10,7 @@ module Issues end if issue.previous_changes.include?('title') || - issue.previous_changes.include?('description') + issue.previous_changes.include?('description') todo_service.update_issue(issue, current_user) end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index bebfca7537b..6a7393a9921 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -42,7 +42,7 @@ module MergeRequests end if merge_request.source_project == merge_request.target_project && - merge_request.target_branch == merge_request.source_branch + merge_request.target_branch == merge_request.source_branch messages << 'You must select different branches' end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index fda0da19d87..ad16ef8c70f 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -25,7 +25,7 @@ module MergeRequests end if merge_request.previous_changes.include?('title') || - merge_request.previous_changes.include?('description') + merge_request.previous_changes.include?('description') todo_service.update_merge_request(merge_request, current_user) end diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 921ca6748d3..8a6af8d8ada 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -6,7 +6,7 @@ module Projects if new_visibility && new_visibility.to_i != project.visibility_level unless can?(current_user, :change_visibility_level, project) && - Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) + Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) deny_visibility_level(project, new_visibility) return project diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 1d7a3f03ace..5a7365bb0f6 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -34,7 +34,7 @@ Sidekiq.configure_server do |config| # Database pool should be at least `sidekiq_concurrency` + 2 # For more info, see: https://github.com/mperham/sidekiq/blob/master/4.0-Upgrade.md config = ActiveRecord::Base.configurations[Rails.env] || - Rails.application.config.database_configuration[Rails.env] + Rails.application.config.database_configuration[Rails.env] config['pool'] = Sidekiq.options[:concurrency] + 2 ActiveRecord::Base.establish_connection(config) Rails.logger.debug("Connection Pool size for Sidekiq Server is now: #{ActiveRecord::Base.connection.pool.instance_variable_get('@size')}") diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb index 85402c2a278..f586c5ab062 100644 --- a/lib/gitlab/email/reply_parser.rb +++ b/lib/gitlab/email/reply_parser.rb @@ -69,7 +69,7 @@ module Gitlab # This one might be controversial but so many reply lines have years, times and end with a colon. # Let's try it and see how well it works. break if (l =~ /\d{4}/ && l =~ /\d:\d\d/ && l =~ /\:$/) || - (l =~ /On \w+ \d+,? \d+,?.*wrote:/) + (l =~ /On \w+ \d+,? \d+,?.*wrote:/) # Headers on subsequent lines break if (0..2).all? { |off| lines[idx + off] =~ REPLYING_HEADER_REGEX } diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb index d619e401897..f4703dc704f 100644 --- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb @@ -29,7 +29,7 @@ describe Gitlab::Gfm::ReferenceRewriter do context 'description with ignored elements' do let(:text) do "Hi. This references #1, but not `#2`\n" + - '<pre>and not !1</pre>' + '<pre>and not !1</pre>' end it { is_expected.to include issue_first.to_reference(new_project) } diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index c5d67a90abc..8304c408064 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -167,7 +167,7 @@ describe API::Projects, api: true do expect(json_response).to satisfy do |response| response.one? do |entry| entry.has_key?('permissions') && - entry['name'] == project.name && + entry['name'] == project.name && entry['owner']['username'] == user.username end end -- cgit v1.2.1 From 88e364f0f6f8d21b73f9eea786c7f8326dff61fe Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Fri, 16 Dec 2016 12:56:38 -0600 Subject: Put back bootstrap wells --- app/assets/stylesheets/framework/tw_bootstrap.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index d998d654aa4..718dbbfea27 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -35,7 +35,7 @@ @import "bootstrap/alerts"; // @import "bootstrap/progress-bars"; @import "bootstrap/list-group"; -// @import "bootstrap/wells"; +@import "bootstrap/wells"; @import "bootstrap/close"; @import "bootstrap/panels"; -- cgit v1.2.1 From dbe2ac8ccc07f53669214eb954489a6cb233d4e9 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Fri, 16 Dec 2016 17:11:04 -0200 Subject: Fix rubucop offenses --- lib/gitlab/bitbucket_import/importer.rb | 54 ++++++++++++------------- spec/lib/bitbucket/representation/issue_spec.rb | 1 - 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index f3760640655..d5287c69e6e 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -60,8 +60,6 @@ module Gitlab create_labels - gitlab_issue = nil - client.issues(repo).each do |issue| begin description = '' @@ -87,31 +85,33 @@ module Gitlab gitlab_issue.labels << @labels[label_name] - if gitlab_issue.persisted? - client.issue_comments(repo, issue.iid).each do |comment| - # The note can be blank for issue service messages like "Changed title: ..." - # We would like to import those comments as well but there is no any - # specific parameter that would allow to process them, it's just an empty comment. - # To prevent our importer from just crashing or from creating useless empty comments - # we do this check. - next unless comment.note.present? - - note = '' - note += @formatter.author_line(comment.author) unless find_user_id(comment.author) - note += comment.note - - begin - gitlab_issue.notes.create!( - project: project, - note: note, - author_id: gitlab_user_id(project, comment.author), - created_at: comment.created_at, - updated_at: comment.updated_at - ) - rescue StandardError => e - errors << { type: :issue_comment, iid: issue.iid, errors: e.message } - end - end + import_issue_comments(issue, gitlab_issue) if gitlab_issue.persisted? + end + end + + def import_issue_comments(issue, gitlab_issue) + client.issue_comments(repo, issue.iid).each do |comment| + # The note can be blank for issue service messages like "Changed title: ..." + # We would like to import those comments as well but there is no any + # specific parameter that would allow to process them, it's just an empty comment. + # To prevent our importer from just crashing or from creating useless empty comments + # we do this check. + next unless comment.note.present? + + note = '' + note += @formatter.author_line(comment.author) unless find_user_id(comment.author) + note += comment.note + + begin + gitlab_issue.notes.create!( + project: project, + note: note, + author_id: gitlab_user_id(project, comment.author), + created_at: comment.created_at, + updated_at: comment.updated_at + ) + rescue StandardError => e + errors << { type: :issue_comment, iid: issue.iid, errors: e.message } end end end diff --git a/spec/lib/bitbucket/representation/issue_spec.rb b/spec/lib/bitbucket/representation/issue_spec.rb index 9a195bebd31..20f47224aa8 100644 --- a/spec/lib/bitbucket/representation/issue_spec.rb +++ b/spec/lib/bitbucket/representation/issue_spec.rb @@ -14,7 +14,6 @@ describe Bitbucket::Representation::Issue do it { expect(described_class.new({}).milestone).to be_nil } end - describe '#author' do it { expect(described_class.new({ 'reporter' => { 'username' => 'Ben' } }).author).to eq('Ben') } it { expect(described_class.new({}).author).to be_nil } -- cgit v1.2.1 From d58fffb27766f49b82866c5460fe218854f9596e Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Fri, 16 Dec 2016 14:30:28 -0600 Subject: fix margin on alert stripes within ":flash_message" block --- app/assets/stylesheets/framework/layout.scss | 8 ++++++++ app/views/layouts/_page.html.haml | 7 ++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 66711aa1804..bef24162924 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -32,6 +32,14 @@ body { } } +.alert-wrapper { + margin-bottom: $gl-padding; + + .alert { + margin-bottom: 0; + } +} + /* The following prevents side effects related to iOS Safari's implementation of -webkit-overflow-scrolling: touch, which is applied to the body by jquery.nicescroling plugin to force hardware acceleration for momentum scrolling. Side diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index a9a0b149049..54d02ee8e4b 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -22,9 +22,10 @@ = render "layouts/nav/#{nav}" .content-wrapper{ class: "#{layout_nav_class}" } = yield :sub_nav - = render "layouts/broadcast" - = render "layouts/flash" - = yield :flash_message + .alert-wrapper + = render "layouts/broadcast" + = render "layouts/flash" + = yield :flash_message %div{ class: "#{(container_class unless @no_container)} #{@content_class}" } .content{ id: "content-body" } = yield -- cgit v1.2.1 From dc9b0913670dc8de5e01a40fd28fa44f48defc9e Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Fri, 16 Dec 2016 14:38:01 -0600 Subject: stripe colors for successive alert-warning blocks --- app/assets/stylesheets/framework/layout.scss | 27 +++++++++++++++++++++++++ app/assets/stylesheets/framework/variables.scss | 1 - app/assets/stylesheets/pages/projects.scss | 6 ------ 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index bef24162924..59fae61a44f 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -38,6 +38,33 @@ body { .alert { margin-bottom: 0; } + + /* Stripe the background colors so that adjacent alert-warnings are distinct from one another */ + .alert-warning { + transition: background-color 0.15s, border-color 0.15s; + background-color: lighten($gl-warning, 4%); + border-color: lighten($gl-warning, 4%); + } + + .alert-warning + .alert-warning { + background-color: $gl-warning; + border-color: $gl-warning; + } + + .alert-warning + .alert-warning + .alert-warning { + background-color: darken($gl-warning, 4%); + border-color: darken($gl-warning, 4%); + } + + .alert-warning + .alert-warning + .alert-warning + .alert-warning { + background-color: darken($gl-warning, 8%); + border-color: darken($gl-warning, 8%); + } + + .alert-warning:only-of-type { + background-color: $gl-warning; + border-color: $gl-warning; + } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 267fcd77b38..0b156d61a23 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -474,7 +474,6 @@ $project-option-descr-color: #54565b; $project-breadcrumb-color: #999; $project-private-forks-notice-odd: #2aa056; $project-network-controls-color: #888; -$project-limit-message-bg: #f28d35; /* * Runners diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 9c3dbc58ae0..3b1b375133d 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -6,12 +6,6 @@ } } -.no-ssh-key-message, -.project-limit-message { - background-color: $project-limit-message-bg; - margin-bottom: 0; -} - .new_project, .edit-project { -- cgit v1.2.1 From ac07ce64a8a62ed4901787063001c7175a5e2cd5 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Fri, 16 Dec 2016 14:41:32 -0600 Subject: add CHANGELOG.md entry for !8151 --- .../25743-clean-up-css-for-project-alerts-and-flash-notifications.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/25743-clean-up-css-for-project-alerts-and-flash-notifications.yml diff --git a/changelogs/unreleased/25743-clean-up-css-for-project-alerts-and-flash-notifications.yml b/changelogs/unreleased/25743-clean-up-css-for-project-alerts-and-flash-notifications.yml new file mode 100644 index 00000000000..0a81124de0d --- /dev/null +++ b/changelogs/unreleased/25743-clean-up-css-for-project-alerts-and-flash-notifications.yml @@ -0,0 +1,4 @@ +--- +title: fix colors and margins for adjacent alert banners +merge_request: 8151 +author: -- cgit v1.2.1 From fe9a372c0b64b47117fc0a64dbdfb514f757ee6e Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Fri, 16 Dec 2016 19:11:48 -0200 Subject: Fix import issues method --- lib/gitlab/bitbucket_import/importer.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index d5287c69e6e..7d2f92d577a 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -79,13 +79,13 @@ module Gitlab created_at: issue.created_at, updated_at: issue.updated_at ) + + gitlab_issue.labels << @labels[label_name] + + import_issue_comments(issue, gitlab_issue) if gitlab_issue.persisted? rescue StandardError => e errors << { type: :issue, iid: issue.iid, errors: e.message } end - - gitlab_issue.labels << @labels[label_name] - - import_issue_comments(issue, gitlab_issue) if gitlab_issue.persisted? end end -- cgit v1.2.1 From a3be4aeb7a71cc940394a5f13d09e79fcafdb1d5 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Fri, 16 Dec 2016 19:51:40 -0200 Subject: Avoid use of Hash#dig to keep compatibility with Ruby 2.1 --- lib/bitbucket/representation/comment.rb | 2 +- lib/bitbucket/representation/issue.rb | 6 +++--- lib/bitbucket/representation/pull_request.rb | 10 +++++----- lib/bitbucket/representation/pull_request_comment.rb | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/bitbucket/representation/comment.rb b/lib/bitbucket/representation/comment.rb index 3c75e9368fa..4937aa9728f 100644 --- a/lib/bitbucket/representation/comment.rb +++ b/lib/bitbucket/representation/comment.rb @@ -6,7 +6,7 @@ module Bitbucket end def note - raw.dig('content', 'raw') + raw.fetch('content', {}).fetch('raw', nil) end def created_at diff --git a/lib/bitbucket/representation/issue.rb b/lib/bitbucket/representation/issue.rb index 3af731753d1..054064395c3 100644 --- a/lib/bitbucket/representation/issue.rb +++ b/lib/bitbucket/representation/issue.rb @@ -12,11 +12,11 @@ module Bitbucket end def author - raw.dig('reporter', 'username') + raw.fetch('reporter', {}).fetch('username', nil) end def description - raw.dig('content', 'raw') + raw.fetch('content', {}).fetch('raw', nil) end def state @@ -28,7 +28,7 @@ module Bitbucket end def milestone - raw.dig('milestone', 'name') + raw['milestone']['name'] if raw['milestone'].present? end def created_at diff --git a/lib/bitbucket/representation/pull_request.rb b/lib/bitbucket/representation/pull_request.rb index e37c9a62c0e..eebf8093380 100644 --- a/lib/bitbucket/representation/pull_request.rb +++ b/lib/bitbucket/representation/pull_request.rb @@ -2,7 +2,7 @@ module Bitbucket module Representation class PullRequest < Representation::Base def author - raw.dig('author', 'username') + raw.fetch('author', {}).fetch('username', nil) end def description @@ -36,19 +36,19 @@ module Bitbucket end def source_branch_name - source_branch.dig('branch', 'name') + source_branch.fetch('branch', {}).fetch('name', nil) end def source_branch_sha - source_branch.dig('commit', 'hash') + source_branch.fetch('commit', {}).fetch('hash', nil) end def target_branch_name - target_branch.dig('branch', 'name') + target_branch.fetch('branch', {}).fetch('name', nil) end def target_branch_sha - target_branch.dig('commit', 'hash') + target_branch.fetch('commit', {}).fetch('hash', nil) end private diff --git a/lib/bitbucket/representation/pull_request_comment.rb b/lib/bitbucket/representation/pull_request_comment.rb index 4f3809fbcea..4f8efe03bae 100644 --- a/lib/bitbucket/representation/pull_request_comment.rb +++ b/lib/bitbucket/representation/pull_request_comment.rb @@ -18,7 +18,7 @@ module Bitbucket end def parent_id - raw.dig('parent', 'id') + raw.fetch('parent', {}).fetch('id', nil) end def inline? -- cgit v1.2.1 From 34ed74fab90904eb77c8b07779401650684af5b1 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Fri, 16 Dec 2016 19:59:53 -0200 Subject: Avoid use of Hash#dig to keep compatibility with Ruby 2.1 --- app/controllers/concerns/oauth_applications.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/concerns/oauth_applications.rb b/app/controllers/concerns/oauth_applications.rb index 7210ed3eb32..9849aa93fa6 100644 --- a/app/controllers/concerns/oauth_applications.rb +++ b/app/controllers/concerns/oauth_applications.rb @@ -6,7 +6,7 @@ module OauthApplications end def prepare_scopes - scopes = params.dig(:doorkeeper_application, :scopes) + scopes = params.fetch(:doorkeeper_application, {}).fetch(:scopes, nil) if scopes params[:doorkeeper_application][:scopes] = scopes.join(' ') -- cgit v1.2.1 From 09388b2021034173156ba8958fa290b01e3a447d Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Tue, 18 Oct 2016 18:22:18 +0600 Subject: Adds sort dropdown for group members --- app/assets/stylesheets/pages/members.scss | 54 +++++++++++++++++----- app/controllers/groups/group_members_controller.rb | 10 ++-- .../projects/project_members_controller.rb | 5 +- app/helpers/members_helper.rb | 30 ++++++++---- app/helpers/sorting_helper.rb | 41 +++++++++++++++- app/views/groups/group_members/index.html.haml | 1 + app/views/projects/project_members/index.html.haml | 1 + app/views/shared/members/_sort_dropdown.html.haml | 11 +++++ 8 files changed, 127 insertions(+), 26 deletions(-) create mode 100644 app/views/shared/members/_sort_dropdown.html.haml diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index 5f3bbb40ba0..b7521133ce5 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -78,6 +78,20 @@ float: right; } + .dropdown { + width: 100%; + margin-top: 5px; + + .dropdown-menu-toggle { + width: 100%; + } + + @media (min-width: $screen-sm-min) { + top: 2.4px; + width: 155px; + } + } + .form-control { width: 100%; padding-right: 35px; @@ -85,18 +99,34 @@ @media (min-width: $screen-sm-min) { width: 350px; } + + &.input-short { + @media (min-width: $screen-md-min) { + width: 170px; + } + + @media (min-width: $screen-lg-min) { + width: 210px; + } + } + } + + .member-search-btn { + position: absolute; + right: 4px; + top: 0; + height: 35px; + padding-left: 10px; + padding-right: 10px; + color: $gray-darkest; + background: transparent; + border: 0; + outline: 0; + + @media (min-width: $screen-sm-min) { + right: 160px; + top: 8px; + } } -} -.member-search-btn { - position: absolute; - right: 0; - top: 0; - height: 35px; - padding-left: 10px; - padding-right: 10px; - color: $gray-darkest; - background: transparent; - border: 0; - outline: 0; } diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 940a3ad20ba..af8abb96d4a 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -10,11 +10,15 @@ class Groups::GroupMembersController < Groups::ApplicationController @members = @members.non_invite unless can?(current_user, :admin_group, @group) if params[:search].present? - users = @group.users.search(params[:search]).to_a - @members = @members.where(user_id: users) + @members = @members.joins(:user).merge(User.search(params[:search])) end - @members = @members.order('access_level DESC').page(params[:page]).per(50) + if params[:sort].present? + @members = @members.joins(:user).merge(User.sort(@sort = params[:sort])) + end + + + @members = @members.page(params[:page]).per(50) @requesters = AccessRequestsFinder.new(@group).execute(current_user) @group_member = @group.group_members.new diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 53308948f62..e4aba4b700e 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -1,10 +1,12 @@ class Projects::ProjectMembersController < Projects::ApplicationController include MembershipActions + include SortingHelper # Authorize before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access] def index + @sort = params[:sort].presence || sort_value_name @group_links = @project.project_group_links @project_members = @project.project_members @@ -40,7 +42,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController @project_members = Member. where(wheres.join(' OR ')). - order(access_level: :desc).page(params[:page]) + sort(@sort). + page(params[:page]) @requesters = AccessRequestsFinder.new(@project).execute(current_user) diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb index 877c77050be..f1b8962eb36 100644 --- a/app/helpers/members_helper.rb +++ b/app/helpers/members_helper.rb @@ -11,17 +11,17 @@ module MembersHelper text = 'Are you sure you want to ' action = - if member.request? - if member.user == user - 'withdraw your access request for' + if member.request? + if member.user == user + 'withdraw your access request for' + else + "deny #{member.user.name}'s request to join" + end + elsif member.invite? + "revoke the invitation for #{member.invite_email} to join" else - "deny #{member.user.name}'s request to join" + "remove #{member.user.name} from" end - elsif member.invite? - "revoke the invitation for #{member.invite_email} to join" - else - "remove #{member.user.name} from" - end text << action << " the #{member.source.human_name} #{member.real_source_type.humanize(capitalize: false)}?" end @@ -36,4 +36,16 @@ module MembersHelper "Are you sure you want to leave the " \ "\"#{member_source.human_name}\" #{member_source.class.to_s.humanize(capitalize: false)}?" end + + def filter_group_project_member_path(options = {}) + exist_opts = { + search: params[:search], + sort: params[:sort] + } + + options = exist_opts.merge(options) + path = request.path + path << "?#{options.to_param}" + path + end end diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 8b138a8e69f..9c6f9f741ed 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -25,7 +25,7 @@ module SortingHelper sort_value_recently_updated => sort_title_recently_updated, sort_value_oldest_updated => sort_title_oldest_updated, sort_value_recently_created => sort_title_recently_created, - sort_value_oldest_created => sort_title_oldest_created, + sort_value_oldest_created => sort_title_oldest_created } if current_controller?('admin/projects') @@ -35,6 +35,17 @@ module SortingHelper options end + def member_sort_options_hash + { + sort_value_last_joined => sort_title_last_joined, + sort_value_oldest_joined => sort_title_oldest_joined, + sort_value_name => sort_title_name_asc, + sort_value_name_desc => sort_title_name_desc, + sort_value_recently_signin => sort_title_recently_signin, + sort_value_oldest_signin => sort_title_oldest_signin + } + end + def sort_title_priority 'Priority' end @@ -95,6 +106,34 @@ module SortingHelper 'Most popular' end + def sort_title_last_joined + 'Last joined' + end + + def sort_title_oldest_joined + 'Oldest joined' + end + + def sort_title_name_asc + 'Name, ascending' + end + + def sort_title_name_desc + 'Name, descending' + end + + def sort_value_last_joined + 'last_joined' + end + + def sort_value_oldest_joined + 'oldest_joined' + end + + def sort_value_name_desc + 'name_desc' + end + def sort_value_priority 'priority' end diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index ebf9aca7700..bc5d3c797ac 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -21,6 +21,7 @@ = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } = icon("search") + = render 'shared/members/sort_dropdown' .panel.panel-default .panel-heading Users with access to diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index bdeb704b6da..4f1cec20f85 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -21,6 +21,7 @@ = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } = icon("search") + = render 'shared/members/sort_dropdown' - if @group_links.any? = render 'groups', group_links: @group_links diff --git a/app/views/shared/members/_sort_dropdown.html.haml b/app/views/shared/members/_sort_dropdown.html.haml new file mode 100644 index 00000000000..42c09636ba3 --- /dev/null +++ b/app/views/shared/members/_sort_dropdown.html.haml @@ -0,0 +1,11 @@ +- @sort ||= sort_value_last_joined + +.dropdown.inline + = dropdown_toggle(member_sort_options_hash[@sort], { toggle: 'dropdown' }, { id: 'sort-members-dropdown' }) + %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable + %li.dropdown-header + Sort by + - member_sort_options_hash.each do |value, title| + %li + = link_to filter_group_project_member_path(sort: value), class: ("is-active" if @sort == value) do + = title -- cgit v1.2.1 From f438a2aabe7f75c0d6ed3a6c1ce2ab5a62e33b31 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Wed, 16 Nov 2016 18:42:39 -0200 Subject: Add CHANGELOG entry --- changelogs/unreleased/23573-sort-functionality-for-project-member.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/23573-sort-functionality-for-project-member.yml diff --git a/changelogs/unreleased/23573-sort-functionality-for-project-member.yml b/changelogs/unreleased/23573-sort-functionality-for-project-member.yml new file mode 100644 index 00000000000..73de0a6351b --- /dev/null +++ b/changelogs/unreleased/23573-sort-functionality-for-project-member.yml @@ -0,0 +1,4 @@ +--- +title: Add sorting functionality for group/project members +merge_request: 7032 +author: -- cgit v1.2.1 From f54ddbf1ecb91ea96b52e18f87ff5ed10ad45747 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Wed, 16 Nov 2016 19:35:33 -0200 Subject: Fix MembersHelper --- app/helpers/members_helper.rb | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb index f1b8962eb36..cf8fdfca1bd 100644 --- a/app/helpers/members_helper.rb +++ b/app/helpers/members_helper.rb @@ -11,17 +11,17 @@ module MembersHelper text = 'Are you sure you want to ' action = - if member.request? - if member.user == user - 'withdraw your access request for' - else - "deny #{member.user.name}'s request to join" - end - elsif member.invite? - "revoke the invitation for #{member.invite_email} to join" + if member.request? + if member.user == user + 'withdraw your access request for' else - "remove #{member.user.name} from" + "deny #{member.user.name}'s request to join" end + elsif member.invite? + "revoke the invitation for #{member.invite_email} to join" + else + "remove #{member.user.name} from" + end text << action << " the #{member.source.human_name} #{member.real_source_type.humanize(capitalize: false)}?" end @@ -39,8 +39,8 @@ module MembersHelper def filter_group_project_member_path(options = {}) exist_opts = { - search: params[:search], - sort: params[:sort] + search: params[:search], + sort: params[:sort] } options = exist_opts.merge(options) -- cgit v1.2.1 From 59d43bea80b56faff54630934694b317cda9f899 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Wed, 16 Nov 2016 19:37:51 -0200 Subject: Fix sort functionality for group/project members --- app/controllers/groups/group_members_controller.rb | 14 ++++---------- app/models/member.rb | 20 ++++++++++++++++++++ app/models/user.rb | 6 ++++-- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index af8abb96d4a..812aaa4c191 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -6,19 +6,13 @@ class Groups::GroupMembersController < Groups::ApplicationController def index @project = @group.projects.find(params[:project_id]) if params[:project_id] + @members = @group.group_members @members = @members.non_invite unless can?(current_user, :admin_group, @group) - - if params[:search].present? - @members = @members.joins(:user).merge(User.search(params[:search])) - end - - if params[:sort].present? - @members = @members.joins(:user).merge(User.sort(@sort = params[:sort])) - end - - + @members = @members.search(params[:search]) if params[:search].present? + @members = @members.sort(@sort = params[:sort]) if params[:sort].present? @members = @members.page(params[:page]).per(50) + @requesters = AccessRequestsFinder.new(@group).execute(current_user) @group_member = @group.group_members.new diff --git a/app/models/member.rb b/app/models/member.rb index 3b65587c66b..b82b16e6f33 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -57,6 +57,11 @@ class Member < ActiveRecord::Base scope :owners, -> { active.where(access_level: OWNER) } scope :owners_and_masters, -> { active.where(access_level: [OWNER, MASTER]) } + scope :order_name_asc, -> { joins(:user).merge(User.order_name_asc) } + scope :order_name_desc, -> { joins(:user).merge(User.order_name_desc) } + scope :order_recent_sign_in, -> { joins(:user).merge(User.order_recent_sign_in) } + scope :order_oldest_sign_in, -> { joins(:user).merge(User.order_oldest_sign_in) } + before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? } after_create :send_invite, if: :invite?, unless: :importing? @@ -72,6 +77,21 @@ class Member < ActiveRecord::Base default_value_for :notification_level, NotificationSetting.levels[:global] class << self + def search(query) + joins(:user).merge(User.search(query)) + end + + def sort(method) + case method.to_s + when 'recent_sign_in' then order_recent_sign_in + when 'oldest_sign_in' then order_oldest_sign_in + when 'last_joined' then order_created_desc + when 'oldest_joined' then order_created_asc + else + order_by(method) + end + end + def access_for_user_ids(user_ids) where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h end diff --git a/app/models/user.rb b/app/models/user.rb index 1bd28203523..a2812d68384 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -178,6 +178,8 @@ class User < ActiveRecord::Base scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all } scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') } scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) } + scope :order_recent_sign_in, -> { reorder(last_sign_in_at: :desc) } + scope :order_oldest_sign_in, -> { reorder(last_sign_in_at: :asc) } def self.with_two_factor joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id"). @@ -205,8 +207,8 @@ class User < ActiveRecord::Base def sort(method) case method.to_s - when 'recent_sign_in' then reorder(last_sign_in_at: :desc) - when 'oldest_sign_in' then reorder(last_sign_in_at: :asc) + when 'recent_sign_in' then order_recent_sign_in + when 'oldest_sign_in' then order_oldest_sign_in else order_by(method) end -- cgit v1.2.1 From 7783267d6cc41b6a5ced907316aefbc71f2a8e7e Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Wed, 16 Nov 2016 19:45:35 -0200 Subject: Add option to sort group/project members by access level --- app/helpers/sorting_helper.rb | 18 ++++++++++++++++++ app/models/member.rb | 2 ++ 2 files changed, 20 insertions(+) diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 9c6f9f741ed..f03c4627050 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -37,6 +37,8 @@ module SortingHelper def member_sort_options_hash { + sort_value_access_level_asc => sort_title_access_level_asc, + sort_value_access_level_desc => sort_title_access_level_desc, sort_value_last_joined => sort_title_last_joined, sort_value_oldest_joined => sort_title_oldest_joined, sort_value_name => sort_title_name_asc, @@ -114,6 +116,14 @@ module SortingHelper 'Oldest joined' end + def sort_title_access_level_asc + 'Access level, ascending' + end + + def sort_title_access_level_desc + 'Access level, descending' + end + def sort_title_name_asc 'Name, ascending' end @@ -130,6 +140,14 @@ module SortingHelper 'oldest_joined' end + def sort_value_access_level_asc + 'access_level_asc' + end + + def sort_value_access_level_desc + 'access_level_desc' + end + def sort_value_name_desc 'name_desc' end diff --git a/app/models/member.rb b/app/models/member.rb index b82b16e6f33..8c36a631ac4 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -83,6 +83,8 @@ class Member < ActiveRecord::Base def sort(method) case method.to_s + when 'access_level_asc' then reorder(access_level: :asc) + when 'access_level_desc' then reorder(access_level: :desc) when 'recent_sign_in' then order_recent_sign_in when 'oldest_sign_in' then order_oldest_sign_in when 'last_joined' then order_created_desc -- cgit v1.2.1 From 4b7a3d0c38126489c42a207411b510b5a7b3264b Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Wed, 16 Nov 2016 20:24:43 -0200 Subject: Add feature spec for sort functionality on group/project members list --- spec/features/groups/members/sorting_spec.rb | 82 ++++++++++++++++++++++++++ spec/features/projects/members/sorting_spec.rb | 82 ++++++++++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 spec/features/groups/members/sorting_spec.rb create mode 100644 spec/features/projects/members/sorting_spec.rb diff --git a/spec/features/groups/members/sorting_spec.rb b/spec/features/groups/members/sorting_spec.rb new file mode 100644 index 00000000000..5990e43c261 --- /dev/null +++ b/spec/features/groups/members/sorting_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' + +feature 'Groups > Members > Sorting', feature: true do + let(:owner) { create(:user, name: 'John Doe') } + let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) } + let(:group) { create(:group) } + + background do + group.add_owner(owner) + group.add_developer(developer) + + login_as(owner) + end + + scenario 'sorts by access level ascending' do + visit_members_list(sort: :access_level_asc) + + expect(first_member).to include(developer.name) + expect(second_member).to include(owner.name) + end + + scenario 'sorts by access level descending' do + visit_members_list(sort: :access_level_desc) + + expect(first_member).to include(owner.name) + expect(second_member).to include(developer.name) + end + + scenario 'sorts by last joined' do + visit_members_list(sort: :last_joined) + + expect(first_member).to include(developer.name) + expect(second_member).to include(owner.name) + end + + scenario 'sorts by oldest joined' do + visit_members_list(sort: :oldest_joined) + + expect(first_member).to include(owner.name) + expect(second_member).to include(developer.name) + end + + scenario 'sorts by name ascending' do + visit_members_list(sort: :name_asc) + + expect(first_member).to include(owner.name) + expect(second_member).to include(developer.name) + end + + scenario 'sorts by name descending' do + visit_members_list(sort: :name_desc) + + expect(first_member).to include(developer.name) + expect(second_member).to include(owner.name) + end + + scenario 'sorts by recent sign in' do + visit_members_list(sort: :recent_sign_in) + + expect(first_member).to include(owner.name) + expect(second_member).to include(developer.name) + end + + scenario 'sorts by oldest sign in' do + visit_members_list(sort: :oldest_sign_in) + + expect(first_member).to include(developer.name) + expect(second_member).to include(owner.name) + end + + def visit_members_list(sort:) + visit group_group_members_path(group.to_param, sort: sort) + end + + def first_member + page.all('ul.content-list > li').first.text + end + + def second_member + page.all('ul.content-list > li').last.text + end +end diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb new file mode 100644 index 00000000000..597e72a5599 --- /dev/null +++ b/spec/features/projects/members/sorting_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' + +feature 'Projects > Members > Sorting', feature: true do + let(:master) { create(:user, name: 'John Doe') } + let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) } + let(:project) { create(:empty_project) } + + background do + project.team << [master, :master] + project.team << [developer, :developer] + + login_as(master) + end + + scenario 'sorts by access level ascending' do + visit_members_list(sort: :access_level_asc) + + expect(first_member).to include(developer.name) + expect(second_member).to include(master.name) + end + + scenario 'sorts by access level descending' do + visit_members_list(sort: :access_level_desc) + + expect(first_member).to include(master.name) + expect(second_member).to include(developer.name) + end + + scenario 'sorts by last joined' do + visit_members_list(sort: :last_joined) + + expect(first_member).to include(developer.name) + expect(second_member).to include(master.name) + end + + scenario 'sorts by oldest joined' do + visit_members_list(sort: :oldest_joined) + + expect(first_member).to include(master.name) + expect(second_member).to include(developer.name) + end + + scenario 'sorts by name ascending' do + visit_members_list(sort: :name_asc) + + expect(first_member).to include(master.name) + expect(second_member).to include(developer.name) + end + + scenario 'sorts by name descending' do + visit_members_list(sort: :name_desc) + + expect(first_member).to include(developer.name) + expect(second_member).to include(master.name) + end + + scenario 'sorts by recent sign in' do + visit_members_list(sort: :recent_sign_in) + + expect(first_member).to include(master.name) + expect(second_member).to include(developer.name) + end + + scenario 'sorts by oldest sign in' do + visit_members_list(sort: :oldest_sign_in) + + expect(first_member).to include(developer.name) + expect(second_member).to include(master.name) + end + + def visit_members_list(sort:) + visit namespace_project_project_members_path(project.namespace.to_param, project.to_param, sort: sort) + end + + def first_member + page.all('ul.content-list > li').first.text + end + + def second_member + page.all('ul.content-list > li').last.text + end +end -- cgit v1.2.1 From 3a2905f5072f451b4b1f284c4383b4054a8892af Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Wed, 16 Nov 2016 20:37:50 -0200 Subject: Sort group/project members alphabetically by default --- app/controllers/groups/group_members_controller.rb | 4 +++- app/views/shared/members/_sort_dropdown.html.haml | 2 -- spec/features/groups/members/sorting_spec.rb | 7 +++++++ spec/features/projects/members/sorting_spec.rb | 7 +++++++ 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 812aaa4c191..4f273a8d4f0 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -1,16 +1,18 @@ class Groups::GroupMembersController < Groups::ApplicationController include MembershipActions + include SortingHelper # Authorize before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access] def index + @sort = params[:sort].presence || sort_value_name @project = @group.projects.find(params[:project_id]) if params[:project_id] @members = @group.group_members @members = @members.non_invite unless can?(current_user, :admin_group, @group) @members = @members.search(params[:search]) if params[:search].present? - @members = @members.sort(@sort = params[:sort]) if params[:sort].present? + @members = @members.sort(@sort) @members = @members.page(params[:page]).per(50) @requesters = AccessRequestsFinder.new(@group).execute(current_user) diff --git a/app/views/shared/members/_sort_dropdown.html.haml b/app/views/shared/members/_sort_dropdown.html.haml index 42c09636ba3..a2b47b605b7 100644 --- a/app/views/shared/members/_sort_dropdown.html.haml +++ b/app/views/shared/members/_sort_dropdown.html.haml @@ -1,5 +1,3 @@ -- @sort ||= sort_value_last_joined - .dropdown.inline = dropdown_toggle(member_sort_options_hash[@sort], { toggle: 'dropdown' }, { id: 'sort-members-dropdown' }) %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable diff --git a/spec/features/groups/members/sorting_spec.rb b/spec/features/groups/members/sorting_spec.rb index 5990e43c261..5904025d978 100644 --- a/spec/features/groups/members/sorting_spec.rb +++ b/spec/features/groups/members/sorting_spec.rb @@ -12,6 +12,13 @@ feature 'Groups > Members > Sorting', feature: true do login_as(owner) end + scenario 'sorts alphabetically by default' do + visit_members_list(sort: nil) + + expect(first_member).to include(owner.name) + expect(second_member).to include(developer.name) + end + scenario 'sorts by access level ascending' do visit_members_list(sort: :access_level_asc) diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb index 597e72a5599..61d3a112b93 100644 --- a/spec/features/projects/members/sorting_spec.rb +++ b/spec/features/projects/members/sorting_spec.rb @@ -12,6 +12,13 @@ feature 'Projects > Members > Sorting', feature: true do login_as(master) end + scenario 'sorts alphabetically by default' do + visit_members_list(sort: nil) + + expect(first_member).to include(master.name) + expect(second_member).to include(developer.name) + end + scenario 'sorts by access level ascending' do visit_members_list(sort: :access_level_asc) -- cgit v1.2.1 From c3af880aeea12565bc33a29284a9836e3b800019 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Thu, 17 Nov 2016 15:51:34 -0200 Subject: Remove unnecessary curly braces from sort dropdown partial --- app/views/shared/members/_sort_dropdown.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/members/_sort_dropdown.html.haml b/app/views/shared/members/_sort_dropdown.html.haml index a2b47b605b7..8f28324c5f6 100644 --- a/app/views/shared/members/_sort_dropdown.html.haml +++ b/app/views/shared/members/_sort_dropdown.html.haml @@ -1,5 +1,5 @@ .dropdown.inline - = dropdown_toggle(member_sort_options_hash[@sort], { toggle: 'dropdown' }, { id: 'sort-members-dropdown' }) + = dropdown_toggle(member_sort_options_hash[@sort], { toggle: 'dropdown' }, id: 'sort-members-dropdown') %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable %li.dropdown-header Sort by -- cgit v1.2.1 From 06f696dd0a6326ca521d81203901618afa0f9a9a Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Thu, 17 Nov 2016 15:52:15 -0200 Subject: Refactor MembersHelper#filter_group_project_member_path --- app/helpers/members_helper.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb index cf8fdfca1bd..41d471cc92f 100644 --- a/app/helpers/members_helper.rb +++ b/app/helpers/members_helper.rb @@ -38,12 +38,8 @@ module MembersHelper end def filter_group_project_member_path(options = {}) - exist_opts = { - search: params[:search], - sort: params[:sort] - } + options = params.slice(:search, :sort).merge(options) - options = exist_opts.merge(options) path = request.path path << "?#{options.to_param}" path -- cgit v1.2.1 From 621b08d8b5cd5ffe22f5bc7fddecc16821c9bd56 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Fri, 18 Nov 2016 15:50:29 -0200 Subject: Fix sort functionality on project/group members to return invited users --- app/models/member.rb | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/app/models/member.rb b/app/models/member.rb index 8c36a631ac4..0312aef32fa 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -57,10 +57,10 @@ class Member < ActiveRecord::Base scope :owners, -> { active.where(access_level: OWNER) } scope :owners_and_masters, -> { active.where(access_level: [OWNER, MASTER]) } - scope :order_name_asc, -> { joins(:user).merge(User.order_name_asc) } - scope :order_name_desc, -> { joins(:user).merge(User.order_name_desc) } - scope :order_recent_sign_in, -> { joins(:user).merge(User.order_recent_sign_in) } - scope :order_oldest_sign_in, -> { joins(:user).merge(User.order_oldest_sign_in) } + scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) } + scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) } + scope :order_recent_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'DESC')) } + scope :order_oldest_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'ASC')) } before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? } @@ -94,6 +94,17 @@ class Member < ActiveRecord::Base end end + def left_join_users + users = User.arel_table + members = Member.arel_table + + member_users = members.join(users, Arel::Nodes::OuterJoin). + on(members[:user_id].eq(users[:id])). + join_sources + + joins(member_users) + end + def access_for_user_ids(user_ids) where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h end -- cgit v1.2.1 From b667d834bc77d29b982c3858d6f788b2a4a34c0d Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Mon, 21 Nov 2016 16:05:11 -0200 Subject: Remove unused id from shared members sort dropdown --- app/views/shared/members/_sort_dropdown.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/members/_sort_dropdown.html.haml b/app/views/shared/members/_sort_dropdown.html.haml index 8f28324c5f6..3fad8406374 100644 --- a/app/views/shared/members/_sort_dropdown.html.haml +++ b/app/views/shared/members/_sort_dropdown.html.haml @@ -1,5 +1,5 @@ .dropdown.inline - = dropdown_toggle(member_sort_options_hash[@sort], { toggle: 'dropdown' }, id: 'sort-members-dropdown') + = dropdown_toggle(member_sort_options_hash[@sort], { toggle: 'dropdown' }) %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable %li.dropdown-header Sort by -- cgit v1.2.1 From 0ef2c8dfbe923fc5e90d2bae306b9ef2aac85112 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Tue, 22 Nov 2016 19:10:31 -0200 Subject: Use factories to create project/group membership on specs --- spec/features/groups/members/sorting_spec.rb | 4 ++-- spec/features/projects/members/sorting_spec.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/features/groups/members/sorting_spec.rb b/spec/features/groups/members/sorting_spec.rb index 5904025d978..42ba8b614a3 100644 --- a/spec/features/groups/members/sorting_spec.rb +++ b/spec/features/groups/members/sorting_spec.rb @@ -6,8 +6,8 @@ feature 'Groups > Members > Sorting', feature: true do let(:group) { create(:group) } background do - group.add_owner(owner) - group.add_developer(developer) + create(:group_member, :owner, user: owner, group: group, created_at: 5.days.ago) + create(:group_member, :developer, user: developer, group: group, created_at: 3.days.ago) login_as(owner) end diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb index 61d3a112b93..3754dfa658d 100644 --- a/spec/features/projects/members/sorting_spec.rb +++ b/spec/features/projects/members/sorting_spec.rb @@ -6,8 +6,8 @@ feature 'Projects > Members > Sorting', feature: true do let(:project) { create(:empty_project) } background do - project.team << [master, :master] - project.team << [developer, :developer] + create(:project_member, :master, user: master, project: project, created_at: 5.days.ago) + create(:project_member, :developer, user: developer, project: project, created_at: 3.days.ago) login_as(master) end -- cgit v1.2.1 From 5479fc9107507fd441de0661dd2c4c0826fb40f0 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Thu, 24 Nov 2016 11:17:51 -0200 Subject: Undo changes on members search button stylesheet --- app/assets/stylesheets/pages/members.scss | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index b7521133ce5..f2417efeebb 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -110,23 +110,22 @@ } } } +} - .member-search-btn { - position: absolute; - right: 4px; - top: 0; - height: 35px; - padding-left: 10px; - padding-right: 10px; - color: $gray-darkest; - background: transparent; - border: 0; - outline: 0; +.member-search-btn { + position: absolute; + right: 4px; + top: 0; + height: 35px; + padding-left: 10px; + padding-right: 10px; + color: $gray-darkest; + background: transparent; + border: 0; + outline: 0; - @media (min-width: $screen-sm-min) { - right: 160px; - top: 8px; - } + @media (min-width: $screen-sm-min) { + right: 160px; + top: 8px; } - } -- cgit v1.2.1 From eac34fd9a3347b873fc963856b2f0e2104fe7a9b Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Fri, 2 Dec 2016 16:24:28 +0600 Subject: Fix sort dropdown alignment --- app/assets/stylesheets/pages/members.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index f2417efeebb..36ee5d17211 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -83,11 +83,12 @@ margin-top: 5px; .dropdown-menu-toggle { + vertical-align: middle; width: 100%; } @media (min-width: $screen-sm-min) { - top: 2.4px; + margin-top: 0; width: 155px; } } @@ -126,6 +127,5 @@ @media (min-width: $screen-sm-min) { right: 160px; - top: 8px; } } -- cgit v1.2.1 From ecea127cd1e2ac382a71e03ebc16de44f762b2dd Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Fri, 2 Dec 2016 17:52:10 +0600 Subject: Improve test for sort dropdown on members page --- app/views/shared/members/_sort_dropdown.html.haml | 2 +- spec/features/groups/members/sorting_spec.rb | 9 +++++++++ spec/features/projects/members/sorting_spec.rb | 9 +++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/app/views/shared/members/_sort_dropdown.html.haml b/app/views/shared/members/_sort_dropdown.html.haml index 3fad8406374..bad0891f9f2 100644 --- a/app/views/shared/members/_sort_dropdown.html.haml +++ b/app/views/shared/members/_sort_dropdown.html.haml @@ -1,4 +1,4 @@ -.dropdown.inline +.dropdown.inline.member-sort-dropdown = dropdown_toggle(member_sort_options_hash[@sort], { toggle: 'dropdown' }) %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable %li.dropdown-header diff --git a/spec/features/groups/members/sorting_spec.rb b/spec/features/groups/members/sorting_spec.rb index 42ba8b614a3..608aedd3471 100644 --- a/spec/features/groups/members/sorting_spec.rb +++ b/spec/features/groups/members/sorting_spec.rb @@ -17,6 +17,7 @@ feature 'Groups > Members > Sorting', feature: true do expect(first_member).to include(owner.name) expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending') end scenario 'sorts by access level ascending' do @@ -24,6 +25,7 @@ feature 'Groups > Members > Sorting', feature: true do expect(first_member).to include(developer.name) expect(second_member).to include(owner.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending') end scenario 'sorts by access level descending' do @@ -31,6 +33,7 @@ feature 'Groups > Members > Sorting', feature: true do expect(first_member).to include(owner.name) expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending') end scenario 'sorts by last joined' do @@ -38,6 +41,7 @@ feature 'Groups > Members > Sorting', feature: true do expect(first_member).to include(developer.name) expect(second_member).to include(owner.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Last joined') end scenario 'sorts by oldest joined' do @@ -45,6 +49,7 @@ feature 'Groups > Members > Sorting', feature: true do expect(first_member).to include(owner.name) expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined') end scenario 'sorts by name ascending' do @@ -52,6 +57,7 @@ feature 'Groups > Members > Sorting', feature: true do expect(first_member).to include(owner.name) expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending') end scenario 'sorts by name descending' do @@ -59,6 +65,7 @@ feature 'Groups > Members > Sorting', feature: true do expect(first_member).to include(developer.name) expect(second_member).to include(owner.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, descending') end scenario 'sorts by recent sign in' do @@ -66,6 +73,7 @@ feature 'Groups > Members > Sorting', feature: true do expect(first_member).to include(owner.name) expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in') end scenario 'sorts by oldest sign in' do @@ -73,6 +81,7 @@ feature 'Groups > Members > Sorting', feature: true do expect(first_member).to include(developer.name) expect(second_member).to include(owner.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in') end def visit_members_list(sort:) diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb index 3754dfa658d..d6ebb523f95 100644 --- a/spec/features/projects/members/sorting_spec.rb +++ b/spec/features/projects/members/sorting_spec.rb @@ -17,6 +17,7 @@ feature 'Projects > Members > Sorting', feature: true do expect(first_member).to include(master.name) expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending') end scenario 'sorts by access level ascending' do @@ -24,6 +25,7 @@ feature 'Projects > Members > Sorting', feature: true do expect(first_member).to include(developer.name) expect(second_member).to include(master.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending') end scenario 'sorts by access level descending' do @@ -31,6 +33,7 @@ feature 'Projects > Members > Sorting', feature: true do expect(first_member).to include(master.name) expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending') end scenario 'sorts by last joined' do @@ -38,6 +41,7 @@ feature 'Projects > Members > Sorting', feature: true do expect(first_member).to include(developer.name) expect(second_member).to include(master.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Last joined') end scenario 'sorts by oldest joined' do @@ -45,6 +49,7 @@ feature 'Projects > Members > Sorting', feature: true do expect(first_member).to include(master.name) expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined') end scenario 'sorts by name ascending' do @@ -52,6 +57,7 @@ feature 'Projects > Members > Sorting', feature: true do expect(first_member).to include(master.name) expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending') end scenario 'sorts by name descending' do @@ -59,6 +65,7 @@ feature 'Projects > Members > Sorting', feature: true do expect(first_member).to include(developer.name) expect(second_member).to include(master.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, descending') end scenario 'sorts by recent sign in' do @@ -66,6 +73,7 @@ feature 'Projects > Members > Sorting', feature: true do expect(first_member).to include(master.name) expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in') end scenario 'sorts by oldest sign in' do @@ -73,6 +81,7 @@ feature 'Projects > Members > Sorting', feature: true do expect(first_member).to include(developer.name) expect(second_member).to include(master.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in') end def visit_members_list(sort:) -- cgit v1.2.1 From e644b8d683d7b9fa2c411fe65e1c828ba9908b57 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Fri, 9 Dec 2016 17:19:47 -0200 Subject: Fix query in Projects::ProjectMembersController to fetch members --- app/controllers/projects/project_members_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index e4aba4b700e..3aec6f18e27 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -37,8 +37,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) end - wheres = ["id IN (#{@project_members.select(:id).to_sql})"] - wheres << "id IN (#{group_members.select(:id).to_sql})" if group_members + wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"] + wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members @project_members = Member. where(wheres.join(' OR ')). -- cgit v1.2.1 From f11caaf4692afdde0a2c458b3682aef3f9658b6a Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Mon, 12 Dec 2016 09:31:48 +0100 Subject: Setup mattermost session --- lib/mattermost/mattermost.rb | 102 +++++++++++++++++++++++++++++++++ spec/lib/mattermost/mattermost_spec.rb | 42 ++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 lib/mattermost/mattermost.rb create mode 100644 spec/lib/mattermost/mattermost_spec.rb diff --git a/lib/mattermost/mattermost.rb b/lib/mattermost/mattermost.rb new file mode 100644 index 00000000000..84d016bb197 --- /dev/null +++ b/lib/mattermost/mattermost.rb @@ -0,0 +1,102 @@ +module Mattermost + class NoSessionError < StandardError; end + # This class' prime objective is to obtain a session token on a Mattermost + # instance with SSO configured where this GitLab instance is the provider. + # + # The process depends on OAuth, but skips a step in the authentication cycle. + # For example, usually a user would click the 'login in GitLab' button on + # Mattermost, which would yield a 302 status code and redirects you to GitLab + # to approve the use of your account on Mattermost. Which would trigger a + # callback so Mattermost knows this request is approved and gets the required + # data to create the user account etc. + # + # This class however skips the button click, and also the approval phase to + # speed up the process and keep it without manual action and get a session + # going. + class Mattermost + include Doorkeeper::Helpers::Controller + include HTTParty + + attr_accessor :current_resource_owner + + def initialize(uri, current_user) + self.class.base_uri(uri) + + @current_resource_owner = current_user + end + + def with_session + raise NoSessionError unless create + yield + destroy + end + + # Next methods are needed for Doorkeeper + def pre_auth + @pre_auth ||= Doorkeeper::OAuth::PreAuthorization.new( + Doorkeeper.configuration, server.client_via_uid, params) + end + + def authorization + @authorization ||= strategy.request + end + + def strategy + @strategy ||= server.authorization_request(pre_auth.response_type) + end + + def request + @request ||= OpenStruct.new(parameters: params) + end + + def params + Rack::Utils.parse_query(@oauth_uri.query).symbolize_keys + end + + private + + def create + return unless oauth_uri + return unless token_uri + + self.class.headers("Cookie" => "MMAUTHTOKEN=#{request_token}") + + request_token + end + + def destroy + post('/api/v3/users/logout') + end + + def oauth_uri + response = get("/api/v3/oauth/gitlab/login", follow_redirects: false) + return unless 300 <= response.code && response.code < 400 + + redirect_uri = response.headers['location'] + return unless redirect_uri + + @oauth_uri ||= URI.parse(redirect_uri) + end + + def token_uri + @token_uri ||= if @oauth_uri + authorization.authorize.redirect_uri if pre_auth.authorizable? + end + end + + def request_token + @request_token ||= if @token_uri + response = get(@token_uri, follow_redirects: false) + response.headers['token'] if 200 <= response.code && response.code < 400 + end + end + + def get(path, options = {}) + self.class.get(path, options) + end + + def post(path, options = {}) + self.class.post(path, options) + end + end +end diff --git a/spec/lib/mattermost/mattermost_spec.rb b/spec/lib/mattermost/mattermost_spec.rb new file mode 100644 index 00000000000..7c99b4df9f3 --- /dev/null +++ b/spec/lib/mattermost/mattermost_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe Mattermost::Mattermost do + let(:user) { create(:user) } + + subject { described_class.new('http://localhost:8065', user) } + + # Needed for doorman to function + it { is_expected.to respond_to(:current_resource_owner) } + it { is_expected.to respond_to(:request) } + it { is_expected.to respond_to(:authorization) } + it { is_expected.to respond_to(:strategy) } + + describe '#with session' do + let!(:stub) do + WebMock.stub_request(:get, 'http://localhost:8065/api/v3/oauth/gitlab/login'). + to_return(headers: { 'location' => 'http://mylocation.com' }, status: 307) + end + + context 'without oauth uri' do + it 'makes a request to the oauth uri' do + expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) + end + + context 'with oauth_uri' do + let!(:doorkeeper) do + Doorkeeper::Application.create(name: "GitLab Mattermost", + redirect_uri: "http://localhost:8065/signup/gitlab/complete\nhttp://localhost:8065/login/gitlab/complete", + scopes: "") + end + + context 'without token_uri' do + it 'can not create a session' do + expect do + subject.with_session + end.to raise_error(Mattermost::NoSessionError) + end + end + end + end + end +end -- cgit v1.2.1 From a31cdb29e49b62f0227963cbc54b6564a3ee9da8 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Thu, 15 Dec 2016 14:32:50 +0100 Subject: Improve session tests --- lib/mattermost/mattermost.rb | 102 -------------------------------- lib/mattermost/session.rb | 105 +++++++++++++++++++++++++++++++++ spec/lib/mattermost/mattermost_spec.rb | 42 ------------- spec/lib/mattermost/session_spec.rb | 68 +++++++++++++++++++++ 4 files changed, 173 insertions(+), 144 deletions(-) delete mode 100644 lib/mattermost/mattermost.rb create mode 100644 lib/mattermost/session.rb delete mode 100644 spec/lib/mattermost/mattermost_spec.rb create mode 100644 spec/lib/mattermost/session_spec.rb diff --git a/lib/mattermost/mattermost.rb b/lib/mattermost/mattermost.rb deleted file mode 100644 index 84d016bb197..00000000000 --- a/lib/mattermost/mattermost.rb +++ /dev/null @@ -1,102 +0,0 @@ -module Mattermost - class NoSessionError < StandardError; end - # This class' prime objective is to obtain a session token on a Mattermost - # instance with SSO configured where this GitLab instance is the provider. - # - # The process depends on OAuth, but skips a step in the authentication cycle. - # For example, usually a user would click the 'login in GitLab' button on - # Mattermost, which would yield a 302 status code and redirects you to GitLab - # to approve the use of your account on Mattermost. Which would trigger a - # callback so Mattermost knows this request is approved and gets the required - # data to create the user account etc. - # - # This class however skips the button click, and also the approval phase to - # speed up the process and keep it without manual action and get a session - # going. - class Mattermost - include Doorkeeper::Helpers::Controller - include HTTParty - - attr_accessor :current_resource_owner - - def initialize(uri, current_user) - self.class.base_uri(uri) - - @current_resource_owner = current_user - end - - def with_session - raise NoSessionError unless create - yield - destroy - end - - # Next methods are needed for Doorkeeper - def pre_auth - @pre_auth ||= Doorkeeper::OAuth::PreAuthorization.new( - Doorkeeper.configuration, server.client_via_uid, params) - end - - def authorization - @authorization ||= strategy.request - end - - def strategy - @strategy ||= server.authorization_request(pre_auth.response_type) - end - - def request - @request ||= OpenStruct.new(parameters: params) - end - - def params - Rack::Utils.parse_query(@oauth_uri.query).symbolize_keys - end - - private - - def create - return unless oauth_uri - return unless token_uri - - self.class.headers("Cookie" => "MMAUTHTOKEN=#{request_token}") - - request_token - end - - def destroy - post('/api/v3/users/logout') - end - - def oauth_uri - response = get("/api/v3/oauth/gitlab/login", follow_redirects: false) - return unless 300 <= response.code && response.code < 400 - - redirect_uri = response.headers['location'] - return unless redirect_uri - - @oauth_uri ||= URI.parse(redirect_uri) - end - - def token_uri - @token_uri ||= if @oauth_uri - authorization.authorize.redirect_uri if pre_auth.authorizable? - end - end - - def request_token - @request_token ||= if @token_uri - response = get(@token_uri, follow_redirects: false) - response.headers['token'] if 200 <= response.code && response.code < 400 - end - end - - def get(path, options = {}) - self.class.get(path, options) - end - - def post(path, options = {}) - self.class.post(path, options) - end - end -end diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb new file mode 100644 index 00000000000..d14121c91a0 --- /dev/null +++ b/lib/mattermost/session.rb @@ -0,0 +1,105 @@ +module Mattermost + class NoSessionError < StandardError; end + # This class' prime objective is to obtain a session token on a Mattermost + # instance with SSO configured where this GitLab instance is the provider. + # + # The process depends on OAuth, but skips a step in the authentication cycle. + # For example, usually a user would click the 'login in GitLab' button on + # Mattermost, which would yield a 302 status code and redirects you to GitLab + # to approve the use of your account on Mattermost. Which would trigger a + # callback so Mattermost knows this request is approved and gets the required + # data to create the user account etc. + # + # This class however skips the button click, and also the approval phase to + # speed up the process and keep it without manual action and get a session + # going. + class Session + include Doorkeeper::Helpers::Controller + include HTTParty + + attr_accessor :current_resource_owner + + def initialize(uri, current_user) + # Sets the base uri for HTTParty, so we can use paths + self.class.base_uri(uri) + + @current_resource_owner = current_user + end + + def with_session + raise NoSessionError unless create + result = yield + destroy + + result + end + + # Next methods are needed for Doorkeeper + def pre_auth + @pre_auth ||= Doorkeeper::OAuth::PreAuthorization.new( + Doorkeeper.configuration, server.client_via_uid, params) + end + + def authorization + @authorization ||= strategy.request + end + + def strategy + @strategy ||= server.authorization_request(pre_auth.response_type) + end + + def request + @request ||= OpenStruct.new(parameters: params) + end + + def params + Rack::Utils.parse_query(@oauth_uri.query).symbolize_keys + end + + private + + def create + return unless oauth_uri + return unless token_uri + + self.class.headers("Cookie" => "MMAUTHTOKEN=#{request_token}") + + request_token + end + + def destroy + post('/api/v3/users/logout') + end + + def oauth_uri + response = get("/api/v3/oauth/gitlab/login", follow_redirects: false) + return unless 300 <= response.code && response.code < 400 + + redirect_uri = response.headers['location'] + return unless redirect_uri + + @oauth_uri ||= URI.parse(redirect_uri) + end + + def token_uri + @token_uri ||= if @oauth_uri + authorization.authorize.redirect_uri if pre_auth.authorizable? + end + end + + def request_token + @request_token ||= begin + response = get(@token_uri, follow_redirects: false) + response.headers['token'] if 200 <= response.code && response.code < 400 + end + end + + def get(path, options = {}) + self.class.get(path, options) + end + + def post(path, options = {}) + self.class.post(path, options) + end + end +end diff --git a/spec/lib/mattermost/mattermost_spec.rb b/spec/lib/mattermost/mattermost_spec.rb deleted file mode 100644 index 7c99b4df9f3..00000000000 --- a/spec/lib/mattermost/mattermost_spec.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'spec_helper' - -describe Mattermost::Mattermost do - let(:user) { create(:user) } - - subject { described_class.new('http://localhost:8065', user) } - - # Needed for doorman to function - it { is_expected.to respond_to(:current_resource_owner) } - it { is_expected.to respond_to(:request) } - it { is_expected.to respond_to(:authorization) } - it { is_expected.to respond_to(:strategy) } - - describe '#with session' do - let!(:stub) do - WebMock.stub_request(:get, 'http://localhost:8065/api/v3/oauth/gitlab/login'). - to_return(headers: { 'location' => 'http://mylocation.com' }, status: 307) - end - - context 'without oauth uri' do - it 'makes a request to the oauth uri' do - expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) - end - - context 'with oauth_uri' do - let!(:doorkeeper) do - Doorkeeper::Application.create(name: "GitLab Mattermost", - redirect_uri: "http://localhost:8065/signup/gitlab/complete\nhttp://localhost:8065/login/gitlab/complete", - scopes: "") - end - - context 'without token_uri' do - it 'can not create a session' do - expect do - subject.with_session - end.to raise_error(Mattermost::NoSessionError) - end - end - end - end - end -end diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb new file mode 100644 index 00000000000..a93bab877da --- /dev/null +++ b/spec/lib/mattermost/session_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +describe Mattermost::Session do + let(:user) { create(:user) } + + subject { described_class.new('http://localhost:8065', user) } + + # Needed for doorkeeper to function + it { is_expected.to respond_to(:current_resource_owner) } + it { is_expected.to respond_to(:request) } + it { is_expected.to respond_to(:authorization) } + it { is_expected.to respond_to(:strategy) } + + describe '#with session' do + let(:location) { 'http://location.tld' } + let!(:stub) do + WebMock.stub_request(:get, 'http://localhost:8065/api/v3/oauth/gitlab/login'). + to_return(headers: { 'location' => location }, status: 307) + end + + context 'without oauth uri' do + it 'makes a request to the oauth uri' do + expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) + end + end + + context 'with oauth_uri' do + let!(:doorkeeper) do + Doorkeeper::Application.create(name: "GitLab Mattermost", + redirect_uri: "http://localhost:8065/signup/gitlab/complete\nhttp://localhost:8065/login/gitlab/complete", + scopes: "") + end + + context 'without token_uri' do + it 'can not create a session' do + expect do + subject.with_session + end.to raise_error(Mattermost::NoSessionError) + end + end + + context 'with token_uri' do + let(:state) { "eyJhY3Rpb24iOiJsb2dpbiIsImhhc2giOiIkMmEkMTAkVC9wYVlEaTdIUS8vcWdKRmdOOUllZUptaUNJWUlvNVNtNEcwU2NBMXFqelNOVmVPZ1cxWUsifQ%3D%3D" } + let(:location) { "http://locahost:8065/oauth/authorize?response_type=code&client_id=#{doorkeeper.uid}&redirect_uri=http%3A%2F%2Flocalhost:8065%2Fsignup%2Fgitlab%2Fcomplete&state=#{state}" } + + before do + WebMock.stub_request(:get, /http:\/\/localhost:8065\/signup\/gitlab\/complete*/). + to_return(headers: { 'token' => 'thisworksnow' }, status: 202) + end + + it 'can setup a session' do + expect(subject).to receive(:destroy) + + subject.with_session { 1 + 1 } + end + + it 'returns the value of the block' do + WebMock.stub_request(:post, "http://localhost:8065/api/v3/users/logout"). + to_return(headers: { 'token' => 'thisworksnow' }, status: 200) + + value = subject.with_session { 1 + 1 } + + expect(value).to be(2) + end + end + end + end +end -- cgit v1.2.1 From 9bcc4d4de5510a14ae891105645b4d59891ba78d Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Thu, 15 Dec 2016 21:06:17 +0100 Subject: Ensure the session is destroyed --- lib/mattermost/session.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index d14121c91a0..f4629585da7 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -28,10 +28,12 @@ module Mattermost def with_session raise NoSessionError unless create - result = yield - destroy - result + begin + yield + ensure + destroy + end end # Next methods are needed for Doorkeeper -- cgit v1.2.1 From 48ebfaa49146b8f6fcb24b063f22d553b2f20395 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Fri, 16 Dec 2016 11:31:26 +0100 Subject: Improve Mattermost Session specs --- lib/mattermost/session.rb | 23 ++++++++-------- spec/lib/mattermost/session_spec.rb | 53 ++++++++++++++++++++++++++----------- 2 files changed, 50 insertions(+), 26 deletions(-) diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index f4629585da7..7d0290be5a1 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -17,7 +17,7 @@ module Mattermost include Doorkeeper::Helpers::Controller include HTTParty - attr_accessor :current_resource_owner + attr_accessor :current_resource_owner, :token def initialize(uri, current_user) # Sets the base uri for HTTParty, so we can use paths @@ -64,9 +64,9 @@ module Mattermost return unless oauth_uri return unless token_uri - self.class.headers("Cookie" => "MMAUTHTOKEN=#{request_token}") - - request_token + self.token = request_token + self.class.headers("Cookie" => "MMAUTHTOKEN=#{self.token}") + self.token end def destroy @@ -84,16 +84,17 @@ module Mattermost end def token_uri - @token_uri ||= if @oauth_uri - authorization.authorize.redirect_uri if pre_auth.authorizable? - end + @token_uri ||= + if @oauth_uri + authorization.authorize.redirect_uri if pre_auth.authorizable? + end end def request_token - @request_token ||= begin - response = get(@token_uri, follow_redirects: false) - response.headers['token'] if 200 <= response.code && response.code < 400 - end + response = get(@token_uri, follow_redirects: false) + if 200 <= response.code && response.code < 400 + response.headers['token'] + end end def get(path, options = {}) diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb index a93bab877da..69d677930bc 100644 --- a/spec/lib/mattermost/session_spec.rb +++ b/spec/lib/mattermost/session_spec.rb @@ -1,9 +1,12 @@ require 'spec_helper' -describe Mattermost::Session do +describe Mattermost::Session, type: :request do let(:user) { create(:user) } - subject { described_class.new('http://localhost:8065', user) } + let(:gitlab_url) { "http://gitlab.com" } + let(:mattermost_url) { "http://mattermost.com" } + + subject { described_class.new(mattermost_url, user) } # Needed for doorkeeper to function it { is_expected.to respond_to(:current_resource_owner) } @@ -14,7 +17,7 @@ describe Mattermost::Session do describe '#with session' do let(:location) { 'http://location.tld' } let!(:stub) do - WebMock.stub_request(:get, 'http://localhost:8065/api/v3/oauth/gitlab/login'). + WebMock.stub_request(:get, "#{mattermost_url}/api/v3/oauth/gitlab/login"). to_return(headers: { 'location' => location }, status: 307) end @@ -26,9 +29,10 @@ describe Mattermost::Session do context 'with oauth_uri' do let!(:doorkeeper) do - Doorkeeper::Application.create(name: "GitLab Mattermost", - redirect_uri: "http://localhost:8065/signup/gitlab/complete\nhttp://localhost:8065/login/gitlab/complete", - scopes: "") + Doorkeeper::Application.create( + name: "GitLab Mattermost", + redirect_uri: "#{mattermost_url}/signup/gitlab/complete\n#{mattermost_url}/login/gitlab/complete", + scopes: "") end context 'without token_uri' do @@ -40,24 +44,43 @@ describe Mattermost::Session do end context 'with token_uri' do - let(:state) { "eyJhY3Rpb24iOiJsb2dpbiIsImhhc2giOiIkMmEkMTAkVC9wYVlEaTdIUS8vcWdKRmdOOUllZUptaUNJWUlvNVNtNEcwU2NBMXFqelNOVmVPZ1cxWUsifQ%3D%3D" } - let(:location) { "http://locahost:8065/oauth/authorize?response_type=code&client_id=#{doorkeeper.uid}&redirect_uri=http%3A%2F%2Flocalhost:8065%2Fsignup%2Fgitlab%2Fcomplete&state=#{state}" } + let(:state) { "state" } + let(:params) do + { response_type: "code", + client_id: doorkeeper.uid, + redirect_uri: "#{mattermost_url}/signup/gitlab/complete", + state: state } + end + let(:location) do + "#{gitlab_url}/oauth/authorize?#{URI.encode_www_form(params)}" + end before do - WebMock.stub_request(:get, /http:\/\/localhost:8065\/signup\/gitlab\/complete*/). - to_return(headers: { 'token' => 'thisworksnow' }, status: 202) + WebMock.stub_request(:get, "#{mattermost_url}/signup/gitlab/complete"). + with(query: hash_including({ 'state' => state })). + to_return do |request| + post "/oauth/token", + client_id: doorkeeper.uid, + client_secret: doorkeeper.secret, + redirect_uri: params[:redirect_uri], + grant_type: 'authorization_code', + code: request.uri.query_values['code'] + + if response.status == 200 + { headers: { 'token' => 'thisworksnow' }, status: 202 } + end + end + + WebMock.stub_request(:post, "#{mattermost_url}/api/v3/users/logout"). + to_return(headers: { Cookie: 'MMAUTHTOKEN=thisworksnow' }, status: 200) end it 'can setup a session' do - expect(subject).to receive(:destroy) - subject.with_session { 1 + 1 } + expect(subject.token).not_to be_nil end it 'returns the value of the block' do - WebMock.stub_request(:post, "http://localhost:8065/api/v3/users/logout"). - to_return(headers: { 'token' => 'thisworksnow' }, status: 200) - value = subject.with_session { 1 + 1 } expect(value).to be(2) -- cgit v1.2.1 From e663725961de66ac838d0a5a85978656938e74f4 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Fri, 16 Dec 2016 12:20:42 +0100 Subject: Store mattermost_url in settings --- config/gitlab.yml.example | 6 ++++++ config/initializers/1_settings.rb | 7 +++++++ lib/mattermost/session.rb | 17 +++++++++-------- spec/lib/mattermost/session_spec.rb | 18 +++++++++++++----- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 327e4a7937c..b8b41a0d86c 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -153,6 +153,12 @@ production: &base # The location where LFS objects are stored (default: shared/lfs-objects). # storage_path: shared/lfs-objects + ## Mattermost + ## For enabling Add to Mattermost button + mattermost: + enabled: false + host: 'https://mattermost.example.com' + ## Gravatar ## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html gravatar: diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 0ee1b1ec634..45404e579ae 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -261,6 +261,13 @@ Settings['lfs'] ||= Settingslogic.new({}) Settings.lfs['enabled'] = true if Settings.lfs['enabled'].nil? Settings.lfs['storage_path'] = File.expand_path(Settings.lfs['storage_path'] || File.join(Settings.shared['path'], "lfs-objects"), Rails.root) +# +# Mattermost +# +Settings['mattermost'] ||= Settingslogic.new({}) +Settings.mattermost['enabled'] = false if Settings.mattermost['enabled'].nil? +Settings.mattermost['host'] = nil unless Settings.mattermost.enabled + # # Gravatar # diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index 7d0290be5a1..a3715bed482 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -17,12 +17,11 @@ module Mattermost include Doorkeeper::Helpers::Controller include HTTParty - attr_accessor :current_resource_owner, :token + base_uri Settings.mattermost.host - def initialize(uri, current_user) - # Sets the base uri for HTTParty, so we can use paths - self.class.base_uri(uri) + attr_accessor :current_resource_owner, :token + def initialize(current_user) @current_resource_owner = current_user end @@ -30,7 +29,7 @@ module Mattermost raise NoSessionError unless create begin - yield + yield self ensure destroy end @@ -65,7 +64,9 @@ module Mattermost return unless token_uri self.token = request_token - self.class.headers("Cookie" => "MMAUTHTOKEN=#{self.token}") + @headers = { + "Authorization": "Bearer #{self.token}" + } self.token end @@ -98,11 +99,11 @@ module Mattermost end def get(path, options = {}) - self.class.get(path, options) + self.class.get(path, options.merge(headers: @headers)) end def post(path, options = {}) - self.class.post(path, options) + self.class.post(path, options.merge(headers: @headers)) end end end diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb index 69d677930bc..3c2eddbd221 100644 --- a/spec/lib/mattermost/session_spec.rb +++ b/spec/lib/mattermost/session_spec.rb @@ -6,7 +6,7 @@ describe Mattermost::Session, type: :request do let(:gitlab_url) { "http://gitlab.com" } let(:mattermost_url) { "http://mattermost.com" } - subject { described_class.new(mattermost_url, user) } + subject { described_class.new(user) } # Needed for doorkeeper to function it { is_expected.to respond_to(:current_resource_owner) } @@ -14,6 +14,10 @@ describe Mattermost::Session, type: :request do it { is_expected.to respond_to(:authorization) } it { is_expected.to respond_to(:strategy) } + before do + described_class.base_uri(mattermost_url) + end + describe '#with session' do let(:location) { 'http://location.tld' } let!(:stub) do @@ -72,18 +76,22 @@ describe Mattermost::Session, type: :request do end WebMock.stub_request(:post, "#{mattermost_url}/api/v3/users/logout"). - to_return(headers: { Cookie: 'MMAUTHTOKEN=thisworksnow' }, status: 200) + to_return(headers: { Authorization: 'token thisworksnow' }, status: 200) end it 'can setup a session' do - subject.with_session { 1 + 1 } + subject.with_session do |session| + end + expect(subject.token).not_to be_nil end it 'returns the value of the block' do - value = subject.with_session { 1 + 1 } + result = subject.with_session do |session| + "value" + end - expect(value).to be(2) + expect(result).to eq("value") end end end -- cgit v1.2.1 From c9610e0a052526adb3138dccf6114d710979a0b7 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Fri, 16 Dec 2016 13:43:01 +0100 Subject: Fix rubocop failures --- config/initializers/1_settings.rb | 4 ++-- lib/mattermost/session.rb | 36 +++++++++++++++++++++--------------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 45404e579ae..ddea325c6ca 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -265,8 +265,8 @@ Settings.lfs['storage_path'] = File.expand_path(Settings.lfs['storage_path'] || # Mattermost # Settings['mattermost'] ||= Settingslogic.new({}) -Settings.mattermost['enabled'] = false if Settings.mattermost['enabled'].nil? -Settings.mattermost['host'] = nil unless Settings.mattermost.enabled +Settings.mattermost['enabled'] = false if Settings.mattermost['enabled'].nil? +Settings.mattermost['host'] = nil unless Settings.mattermost.enabled # # Gravatar diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index a3715bed482..fb8d7d97f8a 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -54,7 +54,15 @@ module Mattermost end def params - Rack::Utils.parse_query(@oauth_uri.query).symbolize_keys + Rack::Utils.parse_query(oauth_uri.query).symbolize_keys + end + + def get(path, options = {}) + self.class.get(path, options.merge(headers: @headers)) + end + + def post(path, options = {}) + self.class.post(path, options.merge(headers: @headers)) end private @@ -63,11 +71,12 @@ module Mattermost return unless oauth_uri return unless token_uri - self.token = request_token + @token = request_token @headers = { - "Authorization": "Bearer #{self.token}" + Authorization: "Bearer #{@token}" } - self.token + + @token end def destroy @@ -75,35 +84,32 @@ module Mattermost end def oauth_uri + return @oauth_uri if defined?(@oauth_uri) + + @oauth_uri = nil + response = get("/api/v3/oauth/gitlab/login", follow_redirects: false) return unless 300 <= response.code && response.code < 400 redirect_uri = response.headers['location'] return unless redirect_uri - @oauth_uri ||= URI.parse(redirect_uri) + @oauth_uri = URI.parse(redirect_uri) end def token_uri @token_uri ||= - if @oauth_uri + if oauth_uri authorization.authorize.redirect_uri if pre_auth.authorizable? end end def request_token - response = get(@token_uri, follow_redirects: false) + response = get(token_uri, follow_redirects: false) + if 200 <= response.code && response.code < 400 response.headers['token'] end end - - def get(path, options = {}) - self.class.get(path, options.merge(headers: @headers)) - end - - def post(path, options = {}) - self.class.post(path, options.merge(headers: @headers)) - end end end -- cgit v1.2.1