From 2c0f375f13cafa305237c306f423b1dd9ea70a23 Mon Sep 17 00:00:00 2001 From: Jared Deckard Date: Sat, 13 Aug 2016 00:13:39 -0500 Subject: Fix inline comment images by removing wrapper #20890 --- lib/banzai/filter/image_link_filter.rb | 9 +-------- spec/features/atom/users_spec.rb | 2 +- spec/lib/banzai/filter/image_link_filter_spec.rb | 6 +++--- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/lib/banzai/filter/image_link_filter.rb b/lib/banzai/filter/image_link_filter.rb index f0fb6084a35..651b55523c0 100644 --- a/lib/banzai/filter/image_link_filter.rb +++ b/lib/banzai/filter/image_link_filter.rb @@ -8,11 +8,6 @@ module Banzai # of the anchor, and then replace the img with the link-wrapped version. def call doc.xpath('descendant-or-self::img[not(ancestor::a)]').each do |img| - div = doc.document.create_element( - 'div', - class: 'image-container' - ) - link = doc.document.create_element( 'a', class: 'no-attachment-icon', @@ -22,9 +17,7 @@ module Banzai link.children = img.clone - div.children = link - - img.replace(div) + img.replace(link) end doc diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb index a8833194421..5a6f241c102 100644 --- a/spec/features/atom/users_spec.rb +++ b/spec/features/atom/users_spec.rb @@ -61,7 +61,7 @@ describe "User Feed", feature: true do end it 'has XHTML summaries in merge request descriptions' do - expect(body).to match /Here is the fix: <\/p>]*>]*>]*\/><\/a><\/div>/ + expect(body).to match /Here is the fix: ]*>]*\/><\/a>/ end end end diff --git a/spec/lib/banzai/filter/image_link_filter_spec.rb b/spec/lib/banzai/filter/image_link_filter_spec.rb index a2a1ed58d1b..d2d519113df 100644 --- a/spec/lib/banzai/filter/image_link_filter_spec.rb +++ b/spec/lib/banzai/filter/image_link_filter_spec.rb @@ -22,8 +22,8 @@ describe Banzai::Filter::ImageLinkFilter, lib: true do expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href'] end - it 'wraps the image with a link and a div' do - doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) - expect(doc.to_html).to include('
') + it 'works with inline images' do + doc = filter(%Q(

test #{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')} inline

)) + expect(doc.to_html).to match /^

test ]*>]*><\/a> inline<\/p>$/ end end -- cgit v1.2.3 From b8cab61ff016156e8c527322bb72d4185d752e44 Mon Sep 17 00:00:00 2001 From: Jared Deckard Date: Sat, 13 Aug 2016 14:01:28 -0500 Subject: Fix false positive caused by non-interpolated string use --- spec/lib/banzai/filter/image_link_filter_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/lib/banzai/filter/image_link_filter_spec.rb b/spec/lib/banzai/filter/image_link_filter_spec.rb index d2d519113df..294558b3db2 100644 --- a/spec/lib/banzai/filter/image_link_filter_spec.rb +++ b/spec/lib/banzai/filter/image_link_filter_spec.rb @@ -13,8 +13,8 @@ describe Banzai::Filter::ImageLinkFilter, lib: true do end it 'does not wrap a duplicate link' do - exp = act = %q(#{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')}) - expect(filter(act).to_html).to eq exp + doc = filter(%Q(#{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')})) + expect(doc.to_html).to match /^]*><\/a>$/ end it 'works with external images' do -- cgit v1.2.3 From 893ad50b9cce75842a6593411003efaec63aacbd Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Wed, 19 Oct 2016 22:43:05 +1100 Subject: Updated schema.rb --- db/schema.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index 5ce855fe08f..21fd8494ccc 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -832,7 +832,7 @@ ActiveRecord::Schema.define(version: 20161017095000) do t.integer "builds_access_level" t.datetime "created_at" t.datetime "updated_at" - t.integer "repository_access_level", default: 20, null: false + t.integer "repository_access_level", default: 20, null: false end add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", using: :btree -- cgit v1.2.3 From ccdaa8abc8ef6033148b6a0151d5d13e4df8a9d4 Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Thu, 20 Oct 2016 15:44:10 +1100 Subject: Add hover to trash icon in notes --- 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 73fe6a715fa..ab719e38904 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -57,7 +57,7 @@ = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = icon('pencil', class: 'link-highlight') = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do - = icon('trash-o') + = icon('trash-o', class: 'danger-highlight') .note-body{class: note_editable ? 'js-task-list-container' : ''} .note-text.md = preserve do -- cgit v1.2.3 From 5b2200777231367cb402898af4f31cef2e63b1a1 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Fri, 14 Oct 2016 10:57:10 -0500 Subject: Smaller min-width for MR pipeline table --- app/assets/stylesheets/pages/pipelines.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 247339648fa..dfaf1ab732d 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -643,6 +643,10 @@ &.pipelines { + .ci-table { + min-width: 900px; + } + .content-list.pipelines { overflow: auto; } -- cgit v1.2.3 From 16fffa9061f72971f37ef13ed0656712e20084f4 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 18 Oct 2016 17:03:36 +0100 Subject: Fixed height of issue board blank state --- app/assets/stylesheets/pages/boards.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 6e81c12aa55..d8fabbdcebe 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -1,4 +1,3 @@ -lex [v-cloak] { display: none; } @@ -132,7 +131,7 @@ lex } .board-blank-state { - height: 100%; + height: calc(100% - 49px); padding: $gl-padding; background-color: #fff; } -- cgit v1.2.3 From f488b9f765346d9f7e918c31cb5bd07717b5989a Mon Sep 17 00:00:00 2001 From: Callum Dryden Date: Thu, 6 Oct 2016 15:19:27 +0000 Subject: Differentiate the expire from leave event At the moment we cannot see weather a user left a project due to their membership expiring of if they themselves opted to leave the project. This adds a new event type that allows us to make this differentiation. Note that is not really feasable to go back and reliably fix up the previous events. As a result the events for previous expire removals will remain the same however events of this nature going forward will be correctly represented. --- CHANGELOG.md | 1 + app/models/concerns/expirable.rb | 6 +++- app/models/event.rb | 9 ++++- app/models/members/project_member.rb | 6 +++- app/services/event_create_service.rb | 4 +++ features/dashboard/dashboard.feature | 13 ------- features/steps/dashboard/dashboard.rb | 27 -------------- lib/event_filter.rb | 11 ++++-- .../project_member_activity_index_spec.rb | 41 ++++++++++++++++++++++ spec/models/concerns/expirable_spec.rb | 31 ++++++++++++++++ spec/models/event_spec.rb | 27 ++++++++++++++ spec/models/members/project_member_spec.rb | 11 ++++++ spec/services/event_create_service_spec.rb | 19 ++++++++++ 13 files changed, 161 insertions(+), 45 deletions(-) create mode 100644 spec/features/dashboard/project_member_activity_index_spec.rb create mode 100644 spec/models/concerns/expirable_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b018fc0d57..8cb848c3957 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. ## 8.14.0 (2016-11-22) + - Adds user project membership expired event to clarify why user was removed (Callum Dryden) ## 8.13.0 (2016-10-22) diff --git a/app/models/concerns/expirable.rb b/app/models/concerns/expirable.rb index be93435453b..b66ba08dc59 100644 --- a/app/models/concerns/expirable.rb +++ b/app/models/concerns/expirable.rb @@ -5,11 +5,15 @@ module Expirable scope :expired, -> { where('expires_at <= ?', Time.current) } end + def expired? + expires? && expires_at <= Time.current + end + def expires? expires_at.present? end def expires_soon? - expires_at < 7.days.from_now + expires? && expires_at < 7.days.from_now end end diff --git a/app/models/event.rb b/app/models/event.rb index 0764cb8cabd..3993b35f96d 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -12,6 +12,7 @@ class Event < ActiveRecord::Base JOINED = 8 # User joined project LEFT = 9 # User left project DESTROYED = 10 + EXPIRED = 11 # User left project due to expiry RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour @@ -115,6 +116,10 @@ class Event < ActiveRecord::Base action == LEFT end + def expired? + action == EXPIRED + end + def destroyed? action == DESTROYED end @@ -124,7 +129,7 @@ class Event < ActiveRecord::Base end def membership_changed? - joined? || left? + joined? || left? || expired? end def created_project? @@ -184,6 +189,8 @@ class Event < ActiveRecord::Base 'joined' elsif left? 'left' + elsif expired? + 'removed due to membership expiration from' elsif destroyed? 'destroyed' elsif commented? diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 125f26369d7..e4880973117 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -121,7 +121,11 @@ class ProjectMember < Member end def post_destroy_hook - event_service.leave_project(self.project, self.user) + if expired? + event_service.expired_leave_project(self.project, self.user) + else + event_service.leave_project(self.project, self.user) + end super end diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb index 07fc77001a5..e24cc66e0fe 100644 --- a/app/services/event_create_service.rb +++ b/app/services/event_create_service.rb @@ -62,6 +62,10 @@ class EventCreateService create_event(project, current_user, Event::LEFT) end + def expired_leave_project(project, current_user) + create_event(project, current_user, Event::EXPIRED) + end + def create_project(project, current_user) create_event(project, current_user, Event::CREATED) end diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature index 1f4c9020731..b1d5e4a7acb 100644 --- a/features/dashboard/dashboard.feature +++ b/features/dashboard/dashboard.feature @@ -31,19 +31,6 @@ Feature: Dashboard And I click "Create Merge Request" link Then I see prefilled new Merge Request page - @javascript - Scenario: I should see User joined Project event - Given user with name "John Doe" joined project "Shop" - When I visit dashboard activity page - Then I should see "John Doe joined project Shop" event - - @javascript - Scenario: I should see User left Project event - Given user with name "John Doe" joined project "Shop" - And user with name "John Doe" left project "Shop" - When I visit dashboard activity page - Then I should see "John Doe left project Shop" event - @javascript Scenario: Sorting Issues Given I visit dashboard issues page diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index a7d61bc28e0..b2bec369e0f 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -33,33 +33,6 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps expect(find("input#merge_request_target_branch").value).to eq "master" end - step 'user with name "John Doe" joined project "Shop"' do - user = create(:user, { name: "John Doe" }) - project.team << [user, :master] - Event.create( - project: project, - author_id: user.id, - action: Event::JOINED - ) - end - - step 'I should see "John Doe joined project Shop" event' do - expect(page).to have_content "John Doe joined project #{project.name_with_namespace}" - end - - step 'user with name "John Doe" left project "Shop"' do - user = User.find_by(name: "John Doe") - Event.create( - project: project, - author_id: user.id, - action: Event::LEFT - ) - end - - step 'I should see "John Doe left project Shop" event' do - expect(page).to have_content "John Doe left project #{project.name_with_namespace}" - end - step 'I have group with projects' do @group = create(:group) @project = create(:project, namespace: @group) diff --git a/lib/event_filter.rb b/lib/event_filter.rb index 96e70e37e8f..21f6a9a762b 100644 --- a/lib/event_filter.rb +++ b/lib/event_filter.rb @@ -45,9 +45,16 @@ class EventFilter when EventFilter.comments actions = [Event::COMMENTED] when EventFilter.team - actions = [Event::JOINED, Event::LEFT] + actions = [Event::JOINED, Event::LEFT, Event::EXPIRED] when EventFilter.all - actions = [Event::PUSHED, Event::MERGED, Event::COMMENTED, Event::JOINED, Event::LEFT] + actions = [ + Event::PUSHED, + Event::MERGED, + Event::COMMENTED, + Event::JOINED, + Event::LEFT, + Event::EXPIRED + ] end events.where(action: actions) diff --git a/spec/features/dashboard/project_member_activity_index_spec.rb b/spec/features/dashboard/project_member_activity_index_spec.rb new file mode 100644 index 00000000000..ba77093a6d4 --- /dev/null +++ b/spec/features/dashboard/project_member_activity_index_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +feature 'Project member activity', feature: true, js: true do + include WaitForAjax + + let(:user) { create(:user) } + let(:project) { create(:empty_project, :public, name: 'x', namespace: user.namespace) } + + before do + project.team << [user, :master] + end + + def visit_activities_and_wait_with_event(event_type) + Event.create(project: project, author_id: user.id, action: event_type) + visit activity_namespace_project_path(project.namespace.path, project.path) + wait_for_ajax + end + + subject { page.find(".event-title").text } + + context 'when a user joins the project' do + before { visit_activities_and_wait_with_event(Event::JOINED) } + + it { is_expected.to eq("#{user.name} joined project") } + end + + context 'when a user leaves the project' do + before { visit_activities_and_wait_with_event(Event::LEFT) } + + it { is_expected.to eq("#{user.name} left project") } + end + + context 'when a users membership expires for the project' do + before { visit_activities_and_wait_with_event(Event::EXPIRED) } + + it "presents the correct message" do + message = "#{user.name} removed due to membership expiration from project" + is_expected.to eq(message) + end + end +end diff --git a/spec/models/concerns/expirable_spec.rb b/spec/models/concerns/expirable_spec.rb new file mode 100644 index 00000000000..f7b436f32e6 --- /dev/null +++ b/spec/models/concerns/expirable_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Expirable do + describe 'ProjectMember' do + let(:no_expire) { create(:project_member) } + let(:expire_later) { create(:project_member, expires_at: Time.current + 6.days) } + let(:expired) { create(:project_member, expires_at: Time.current - 6.days) } + + describe '.expired' do + it { expect(ProjectMember.expired).to match_array([expired]) } + end + + describe '#expired?' do + it { expect(no_expire.expired?).to eq(false) } + it { expect(expire_later.expired?).to eq(false) } + it { expect(expired.expired?).to eq(true) } + end + + describe '#expires?' do + it { expect(no_expire.expires?).to eq(false) } + it { expect(expire_later.expires?).to eq(true) } + it { expect(expired.expires?).to eq(true) } + end + + describe '#expires_soon?' do + it { expect(no_expire.expires_soon?).to eq(false) } + it { expect(expire_later.expires_soon?).to eq(true) } + it { expect(expired.expires_soon?).to eq(true) } + end + end +end diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 733b79079ed..aca49be2942 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -40,6 +40,33 @@ describe Event, models: true do end end + describe '#membership_changed?' do + context "created" do + subject { build(:event, action: Event::CREATED).membership_changed? } + it { is_expected.to be_falsey } + end + + context "updated" do + subject { build(:event, action: Event::UPDATED).membership_changed? } + it { is_expected.to be_falsey } + end + + context "expired" do + subject { build(:event, action: Event::EXPIRED).membership_changed? } + it { is_expected.to be_truthy } + end + + context "left" do + subject { build(:event, action: Event::LEFT).membership_changed? } + it { is_expected.to be_truthy } + end + + context "joined" do + subject { build(:event, action: Event::JOINED).membership_changed? } + it { is_expected.to be_truthy } + end + end + describe '#note?' do subject { Event.new(project: target.project, target: target) } diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index d85a1c1e3b2..b2fe96e2e02 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -54,6 +54,17 @@ describe ProjectMember, models: true do master_todos end + it "creates an expired event when left due to expiry" do + expired = create(:project_member, project: project, expires_at: Time.now - 6.days) + expired.destroy + expect(Event.first.action).to eq(Event::EXPIRED) + end + + it "creates a left event when left due to leave" do + master.destroy + expect(Event.first.action).to eq(Event::LEFT) + end + it "destroys itself and delete associated todos" do expect(owner.user.todos.size).to eq(2) expect(master.user.todos.size).to eq(3) diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index 16a9956fe7f..b7dc99ed887 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -110,4 +110,23 @@ describe EventCreateService, services: true do end end end + + describe 'Project' do + let(:user) { create :user } + let(:project) { create(:empty_project) } + + describe '#join_project' do + subject { service.join_project(project, user) } + + it { is_expected.to be_truthy } + it { expect { subject }.to change { Event.count }.from(0).to(1) } + end + + describe '#expired_leave_project' do + subject { service.expired_leave_project(project, user) } + + it { is_expected.to be_truthy } + it { expect { subject }.to change { Event.count }.from(0).to(1) } + end + end end -- cgit v1.2.3 From 7077162af28b589c00bed5603cc4f18653cc5678 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Wed, 19 Oct 2016 21:58:12 +0200 Subject: Simpler arguments passed to named_route on toggle_award_url helper method --- CHANGELOG.md | 1 + app/helpers/award_emoji_helper.rb | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cb848c3957..dfde2cc81e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. ## 8.14.0 (2016-11-22) - Adds user project membership expired event to clarify why user was removed (Callum Dryden) + - Simpler arguments passed to named_route on toggle_award_url helper method ## 8.13.0 (2016-10-22) diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb index 592ffe7b89f..167b09e678f 100644 --- a/app/helpers/award_emoji_helper.rb +++ b/app/helpers/award_emoji_helper.rb @@ -3,8 +3,8 @@ module AwardEmojiHelper return url_for([:toggle_award_emoji, awardable]) unless @project if awardable.is_a?(Note) - # We render a list of notes very frequently and calling the specific method is a lot faster than the generic one (6.5x) - toggle_award_emoji_namespace_project_note_url(namespace_id: @project.namespace, project_id: @project, id: awardable.id) + # We render a list of notes very frequently and calling the specific method is a lot faster than the generic one (4.5x) + toggle_award_emoji_namespace_project_note_url(@project.namespace, @project, awardable.id) else url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) end -- cgit v1.2.3 From 6278ee5ab0c4c48d3c8a12ade85c263fefb935d2 Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Thu, 20 Oct 2016 08:57:23 +0900 Subject: Fix a broken table in Project API doc --- CHANGELOG.md | 1 + doc/api/projects.md | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfde2cc81e5..399a7f65267 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -135,6 +135,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Delete dynamic environments - Fix buggy iOS tooltip layering behavior. - Make guests unable to view MRs on private projects + - Fix broken Project API docs (Takuya Noguchi) ## 8.12.7 diff --git a/doc/api/projects.md b/doc/api/projects.md index b7791b4748a..77d6bd6b5c2 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1333,8 +1333,8 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `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 -| `order_by` (optional) - Return requests ordered by `id`, `name`, `created_at` or `last_activity_at` fields +| `query` | string | yes | A string contained in the project name | +| `per_page` | integer | no | number of projects to return per page | +| `page` | integer | no | the page to retrieve | +| `order_by` | string | no | Return requests ordered by `id`, `name`, `created_at` or `last_activity_at` fields | | `sort` | string | no | Return requests sorted in `asc` or `desc` order | -- cgit v1.2.3 From 6fd561d282098e71a523cf1b73396b440ef13e63 Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Thu, 20 Oct 2016 08:58:35 +0900 Subject: Remove pagination description from individual doc --- doc/api/projects.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/doc/api/projects.md b/doc/api/projects.md index 77d6bd6b5c2..b69db90e70d 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1334,7 +1334,5 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `query` | string | yes | A string contained in the project name | -| `per_page` | integer | no | number of projects to return per page | -| `page` | integer | no | the page to retrieve | | `order_by` | string | no | Return requests ordered by `id`, `name`, `created_at` or `last_activity_at` fields | | `sort` | string | no | Return requests sorted in `asc` or `desc` order | -- cgit v1.2.3 From 7a184617199fa01c040857ed3a5d2a071944760a Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Wed, 19 Oct 2016 19:43:04 +0300 Subject: Refactoring find_commits functionality --- app/controllers/projects/commits_controller.rb | 2 +- app/models/repository.rb | 9 ++++++--- lib/gitlab/project_search_results.rb | 6 +----- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index a52c614b259..c2e7bf1ffec 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -13,7 +13,7 @@ class Projects::CommitsController < Projects::ApplicationController @commits = if search.present? - @repository.find_commits_by_message(search, @ref, @path, @limit, @offset).compact + @repository.find_commits_by_message(search, @ref, @path, @limit, @offset) else @repository.commits(@ref, path: @path, limit: @limit, offset: @offset) end diff --git a/app/models/repository.rb b/app/models/repository.rb index 72e473871fa..1b7f20a2134 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -109,6 +109,10 @@ class Repository end def find_commits_by_message(query, ref = nil, path = nil, limit = 1000, offset = 0) + unless exists? && has_visible_content? && query.present? + return [] + end + ref ||= root_ref args = %W( @@ -117,9 +121,8 @@ class Repository ) args = args.concat(%W(-- #{path})) if path.present? - git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:chomp) - commits = git_log_results.map { |c| commit(c) } - commits + git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines + git_log_results.map { |c| commit(c.chomp) }.compact end def find_branch(name, fresh_repo: true) diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 5b9cfaeb2f8..24733435a5a 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -73,11 +73,7 @@ module Gitlab end def commits - if project.empty_repo? || query.blank? - [] - else - project.repository.find_commits_by_message(query).compact - end + project.repository.find_commits_by_message(query) end def project_ids_relation -- cgit v1.2.3 From 17ae807ae3e03fcc67f557c4ad7d5c6e0502065e Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 14 Oct 2016 19:38:41 -0300 Subject: Create project feature when project is created --- CHANGELOG.md | 1 + app/models/project.rb | 7 +----- ...213545_generate_project_feature_for_projects.rb | 28 ++++++++++++++++++++++ db/schema.rb | 2 +- spec/models/project_spec.rb | 14 ++++++++--- 5 files changed, 42 insertions(+), 10 deletions(-) create mode 100644 db/migrate/20161019213545_generate_project_feature_for_projects.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 399a7f65267..670404e4fce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun) - Cancelled pipelines could be retried. !6927 - Updating verbiage on git basics to be more intuitive + - Fix project_feature record not generated on project creation - Clarify documentation for Runners API (Gennady Trafimenkov) - The instrumentation for Banzai::Renderer has been restored - Change user & group landing page routing from /u/:username to /:username diff --git a/app/models/project.rb b/app/models/project.rb index 653c38322c5..6685baab699 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -32,8 +32,8 @@ class Project < ActiveRecord::Base default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } after_create :ensure_dir_exist + after_create :create_project_feature, unless: :project_feature after_save :ensure_dir_exist, if: :namespace_id_changed? - after_initialize :setup_project_feature # set last_activity_at to the same as created_at after_create :set_last_activity_at @@ -1310,11 +1310,6 @@ class Project < ActiveRecord::Base "projects/#{id}/pushes_since_gc" end - # Prevents the creation of project_feature record for every project - def setup_project_feature - build_project_feature unless project_feature - 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/db/migrate/20161019213545_generate_project_feature_for_projects.rb b/db/migrate/20161019213545_generate_project_feature_for_projects.rb new file mode 100644 index 00000000000..4554e14b0df --- /dev/null +++ b/db/migrate/20161019213545_generate_project_feature_for_projects.rb @@ -0,0 +1,28 @@ +class GenerateProjectFeatureForProjects < ActiveRecord::Migration + DOWNTIME = true + + DOWNTIME_REASON = <<-HEREDOC + Application was eager loading project_feature for all projects generating an extra query + everytime a project was fetched. We removed that behavior to avoid the extra query, this migration + makes sure all projects have a project_feature record associated. + HEREDOC + + def up + # Generate enabled values for each project feature 20, 20, 20, 20, 20 + # All features are enabled by default + enabled_values = [ProjectFeature::ENABLED] * 5 + + execute <<-EOF.strip_heredoc + INSERT INTO project_features + (project_id, merge_requests_access_level, builds_access_level, + issues_access_level, snippets_access_level, wiki_access_level) + (SELECT projects.id, #{enabled_values.join(',')} FROM projects LEFT OUTER JOIN project_features + ON project_features.project_id = projects.id + WHERE project_features.id IS NULL) + EOF + end + + def down + "Not needed" + end +end diff --git a/db/schema.rb b/db/schema.rb index f777ed39378..f5c01511195 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: 20161018024550) do +ActiveRecord::Schema.define(version: 20161019213545) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index e6d98e25d0b..f4dda1ee558 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -67,6 +67,14 @@ describe Project, models: true do it { is_expected.to have_many(:notification_settings).dependent(:destroy) } it { is_expected.to have_many(:forks).through(:forked_project_links) } + context 'after create' do + it "creates project feature" do + project = FactoryGirl.build(:project) + + expect { project.save }.to change{ project.project_feature.present? }.from(false).to(true) + end + end + describe '#members & #requesters' do let(:project) { create(:project, :public) } let(:requester) { create(:user) } @@ -531,9 +539,9 @@ describe Project, models: true do end describe '#has_wiki?' do - let(:no_wiki_project) { build(:project, wiki_enabled: false, has_external_wiki: false) } - let(:wiki_enabled_project) { build(:project) } - let(:external_wiki_project) { build(:project, has_external_wiki: true) } + let(:no_wiki_project) { create(:project, wiki_access_level: ProjectFeature::DISABLED, has_external_wiki: false) } + let(:wiki_enabled_project) { create(:project) } + let(:external_wiki_project) { create(:project, has_external_wiki: true) } it 'returns true if project is wiki enabled or has external wiki' do expect(wiki_enabled_project).to have_wiki -- cgit v1.2.3 From 89408aed1f32d760a879c9e26563b956f498793d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 20 Oct 2016 12:10:27 +0200 Subject: Make label API spec independent of order --- spec/requests/api/labels_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index 1da9988978b..867bc615b97 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -22,8 +22,7 @@ describe API::API, api: true do expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.size).to eq(2) - expect(json_response.first['name']).to eq(group_label.name) - expect(json_response.second['name']).to eq(label1.name) + expect(json_response.map { |l| l['name'] }).to match_array([group_label.name, label1.name]) end end -- cgit v1.2.3 From ae0a9336f106b14a7abc39e5a32fe8491dd9b544 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 20 Oct 2016 10:27:30 +0100 Subject: Removed code from project members controller This code was meant to be added to another branch as an expirement, but instead was commited to wrong branch --- app/controllers/projects/project_members_controller.rb | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 37a86ed0523..2a07d154853 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -32,21 +32,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController current_user: current_user ) - if params[:group_ids].present? - group_ids = params[:group_ids].split(',') - groups = Group.where(id: group_ids) - - groups.each do |group| - next unless can?(current_user, :read_group, group) - - project.project_group_links.create( - group: group, - group_access: params[:access_level], - expires_at: params[:expires_at] - ) - end - end - redirect_to namespace_project_project_members_path(@project.namespace, @project) end -- cgit v1.2.3 From 583b79a46875b14405061f3036cf041c44241c62 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 20 Oct 2016 12:59:39 +0200 Subject: Restrict ProjectCacheWorker jobs to one per 15 min This ensures ProjectCacheWorker jobs for a given project are performed at most once per 15 minutes. This should reduce disk load a bit in cases where there are multiple pushes happening (which should schedule multiple ProjectCacheWorker jobs). --- CHANGELOG.md | 3 ++- app/workers/project_cache_worker.rb | 27 ++++++++++++++++++++++ spec/workers/project_cache_worker_spec.rb | 38 +++++++++++++++++++++++-------- 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 670404e4fce..98cffab8c03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. ## 8.14.0 (2016-11-22) - Adds user project membership expired event to clarify why user was removed (Callum Dryden) - - Simpler arguments passed to named_route on toggle_award_url helper method + - Simpler arguments passed to named_route on toggle_award_url helper method ## 8.13.0 (2016-10-22) @@ -36,6 +36,7 @@ Please view this file on the master branch, on stable branches it's out of date. - AbstractReferenceFilter caches project_refs on RequestStore when active - Replaced the check sign to arrow in the show build view. !6501 - Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar) + - ProjectCacheWorker updates caches at most once per 15 minutes per project - Fix Error 500 when viewing old merge requests with bad diff data - Create a new /templates namespace for the /licenses, /gitignores and /gitlab_ci_ymls API endpoints. !5717 (tbalthazar) - Speed-up group milestones show page diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb index ccefd0f71a0..0d524e88dc3 100644 --- a/app/workers/project_cache_worker.rb +++ b/app/workers/project_cache_worker.rb @@ -1,9 +1,30 @@ +# Worker for updating any project specific caches. +# +# This worker runs at most once every 15 minutes per project. This is to ensure +# that multiple instances of jobs for this worker don't hammer the underlying +# storage engine as much. class ProjectCacheWorker include Sidekiq::Worker sidekiq_options queue: :default + LEASE_TIMEOUT = 15.minutes.to_i + def perform(project_id) + if try_obtain_lease_for(project_id) + Rails.logger. + info("Obtained ProjectCacheWorker lease for project #{project_id}") + else + Rails.logger. + info("Could not obtain ProjectCacheWorker lease for project #{project_id}") + + return + end + + update_caches(project_id) + end + + def update_caches(project_id) project = Project.find(project_id) return unless project.repository.exists? @@ -15,4 +36,10 @@ class ProjectCacheWorker project.repository.build_cache end end + + def try_obtain_lease_for(project_id) + Gitlab::ExclusiveLease. + new("project_cache_worker:#{project_id}", timeout: LEASE_TIMEOUT). + try_obtain + end end diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb index 5785a6a06ff..f5b60b90d11 100644 --- a/spec/workers/project_cache_worker_spec.rb +++ b/spec/workers/project_cache_worker_spec.rb @@ -6,21 +6,39 @@ describe ProjectCacheWorker do subject { described_class.new } describe '#perform' do - it 'updates project cache data' do - expect_any_instance_of(Repository).to receive(:size) - expect_any_instance_of(Repository).to receive(:commit_count) + context 'when an exclusive lease can be obtained' do + before do + allow(subject).to receive(:try_obtain_lease_for).with(project.id). + and_return(true) + end - expect_any_instance_of(Project).to receive(:update_repository_size) - expect_any_instance_of(Project).to receive(:update_commit_count) + it 'updates project cache data' do + expect_any_instance_of(Repository).to receive(:size) + expect_any_instance_of(Repository).to receive(:commit_count) - subject.perform(project.id) + expect_any_instance_of(Project).to receive(:update_repository_size) + expect_any_instance_of(Project).to receive(:update_commit_count) + + subject.perform(project.id) + end + + it 'handles missing repository data' do + expect_any_instance_of(Repository).to receive(:exists?).and_return(false) + expect_any_instance_of(Repository).not_to receive(:size) + + subject.perform(project.id) + end end - it 'handles missing repository data' do - expect_any_instance_of(Repository).to receive(:exists?).and_return(false) - expect_any_instance_of(Repository).not_to receive(:size) + context 'when an exclusive lease can not be obtained' do + it 'does nothing' do + allow(subject).to receive(:try_obtain_lease_for).with(project.id). + and_return(false) + + expect(subject).not_to receive(:update_caches) - subject.perform(project.id) + subject.perform(project.id) + end end end end -- cgit v1.2.3 From bd3548c19e00cfadbc5d2c122b968090a160afbc Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 13 Oct 2016 13:58:31 -0500 Subject: Change input order on Sign In form for better tabbing. This *unreverts* 8751491b, which was mistakenly reverted in !6328. It also changes the implementation of the original commit to work with the new login styling and markup. cc: @ClemMakesApps --- app/assets/stylesheets/pages/login.scss | 17 +++++++++++++++++ app/views/devise/sessions/_new_base.html.haml | 4 ++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index e6d9be5185d..bdb13bee178 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -53,6 +53,7 @@ margin: 0 0 10px; } + .login-footer { margin-top: 10px; @@ -246,3 +247,19 @@ padding: 65px; // height of footer + bottom padding of email confirmation link } } + +// For sign in pane only, to improve tab order, the following removes the submit button from +// normal document flow and pins it to the bottom of the form. For context, see !6867 & !6928 + +.login-box { + .new_user { + position: relative; + padding-bottom: 35px; + } + + .move-submit-down { + position: absolute; + width: 100%; + bottom: 0; + } +} diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index a96b579c593..525e7d99d71 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -5,6 +5,8 @@ %div.form-group = f.label :password = f.password_field :password, class: "form-control bottom", required: true, title: "This field is required." + %div.submit-container.move-submit-down + = f.submit "Sign in", class: "btn btn-save" - if devise_mapping.rememberable? .remember-me.checkbox %label{for: "user_remember_me"} @@ -12,5 +14,3 @@ %span Remember me .pull-right = link_to "Forgot your password?", new_password_path(resource_name) - %div.submit-container - = f.submit "Sign in", class: "btn btn-save" -- cgit v1.2.3 From a2f64619af8da5c5b09ead7554e5613368f80dae Mon Sep 17 00:00:00 2001 From: Luke Bennett Date: Sun, 18 Sep 2016 17:20:40 +0100 Subject: Added tooltip with jobs full name to build items in graph Added status to build tooltip and removed status icon tooltip Added Pipelines class to force tooltips ontop of the dropdown, we cannot do this with data attributes because dropdown toggle is already set Corrected dispatcher invocation --- app/assets/javascripts/pipelines.js.es6 | 11 +++++++++++ app/helpers/ci_status_helper.rb | 4 ++-- app/views/projects/commit/_pipeline_stage.html.haml | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 index a7624de6089..dd320c52e44 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 @@ -4,6 +4,17 @@ constructor() { $(document).off('click', '.toggle-pipeline-btn').on('click', '.toggle-pipeline-btn', this.toggleGraph); this.addMarginToBuildColumns(); + this.initGroupedPipelineTooltips(); + } + + initGroupedPipelineTooltips() { + $('.dropdown-menu-toggle', $('.grouped-pipeline-dropdown').parent()).each(function() { + const $this = $(this); + $this.tooltip({ + title: $this.data('tooltip-title'), + placement: 'bottom' + }); + }); } toggleGraph() { diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index b7f48630bd4..0e727f3dcf0 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -71,10 +71,10 @@ module CiStatusHelper Ci::Runner.shared.blank? end - def render_status_with_link(type, status, path = nil, tooltip_placement: 'auto left', cssclass: '', container: 'body') + def render_status_with_link(type, status, path = nil, tooltip_placement: 'auto left', cssclass: '', container: 'body', show_tooltip: true) klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}" title = "#{type.titleize}: #{ci_label_for_status(status)}" - data = { toggle: 'tooltip', placement: tooltip_placement, container: container } + data = { toggle: 'tooltip', placement: tooltip_placement, container: container } if show_tooltip if path link_to ci_icon_for_status(status), path, diff --git a/app/views/projects/commit/_pipeline_stage.html.haml b/app/views/projects/commit/_pipeline_stage.html.haml index 289aa5178b1..993fc89d23d 100644 --- a/app/views/projects/commit/_pipeline_stage.html.haml +++ b/app/views/projects/commit/_pipeline_stage.html.haml @@ -5,7 +5,7 @@ - is_playable = status.playable? && can?(current_user, :update_build, @project) %li.build{ class: ("playable" if is_playable) } .curve - .build-content + .build-content{ { data: { toggle: 'tooltip', title: "#{group_name} - #{status.status}", placement: 'bottom' } } } = render "projects/#{status.to_partial_path}_pipeline", subject: status - else %li.build -- cgit v1.2.3 From 33f3c3792abe2cbf1b68c0ce3414edb8e199be1e Mon Sep 17 00:00:00 2001 From: Luke Bennett Date: Fri, 7 Oct 2016 19:30:34 +0100 Subject: Added dyanmic position adjustment Added tooltips for dropdown items Reverted pretty much everything in favour of a DOM approach Simplified JS --- app/assets/javascripts/pipelines.js.es6 | 11 ----------- app/assets/stylesheets/pages/pipelines.scss | 14 ++++++++++---- app/helpers/ci_status_helper.rb | 4 ++-- app/views/projects/ci/builds/_build_pipeline.html.haml | 4 ++-- app/views/projects/commit/_pipeline_stage.html.haml | 2 +- app/views/projects/commit/_pipeline_status_group.html.haml | 2 +- .../_generic_commit_status_pipeline.html.haml | 13 +++++++------ 7 files changed, 23 insertions(+), 27 deletions(-) diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 index dd320c52e44..a7624de6089 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 @@ -4,17 +4,6 @@ constructor() { $(document).off('click', '.toggle-pipeline-btn').on('click', '.toggle-pipeline-btn', this.toggleGraph); this.addMarginToBuildColumns(); - this.initGroupedPipelineTooltips(); - } - - initGroupedPipelineTooltips() { - $('.dropdown-menu-toggle', $('.grouped-pipeline-dropdown').parent()).each(function() { - const $this = $(this); - $this.tooltip({ - title: $this.data('tooltip-title'), - placement: 'bottom' - }); - }); } toggleGraph() { diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index dfaf1ab732d..5b8dc7f8c40 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -369,10 +369,6 @@ &:hover { background-color: $gray-lighter; - - .dropdown-menu-toggle { - background-color: transparent; - } } &.playable { @@ -402,6 +398,15 @@ } } + .tooltip { + white-space: nowrap; + + .tooltip-inner { + overflow: hidden; + text-overflow: ellipsis; + } + } + .ci-status-text { width: 135px; white-space: nowrap; @@ -419,6 +424,7 @@ } .dropdown-menu-toggle { + background-color: transparent; border: none; width: auto; padding: 0; diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 0e727f3dcf0..b7f48630bd4 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -71,10 +71,10 @@ module CiStatusHelper Ci::Runner.shared.blank? end - def render_status_with_link(type, status, path = nil, tooltip_placement: 'auto left', cssclass: '', container: 'body', show_tooltip: true) + def render_status_with_link(type, status, path = nil, tooltip_placement: 'auto left', cssclass: '', container: 'body') klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}" title = "#{type.titleize}: #{ci_label_for_status(status)}" - data = { toggle: 'tooltip', placement: tooltip_placement, container: container } if show_tooltip + data = { toggle: 'tooltip', placement: tooltip_placement, container: container } if path link_to ci_icon_for_status(status), path, diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml index 017d3ff6af2..55965172d3f 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, title: 'Play' 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: '.pipeline-graph', placement: 'bottom' } do = render_status_with_link('build', '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) do + = 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 = render_status_with_link('build', subject.status) .ci-status-text= subject.name diff --git a/app/views/projects/commit/_pipeline_stage.html.haml b/app/views/projects/commit/_pipeline_stage.html.haml index 993fc89d23d..289aa5178b1 100644 --- a/app/views/projects/commit/_pipeline_stage.html.haml +++ b/app/views/projects/commit/_pipeline_stage.html.haml @@ -5,7 +5,7 @@ - is_playable = status.playable? && can?(current_user, :update_build, @project) %li.build{ class: ("playable" if is_playable) } .curve - .build-content{ { data: { toggle: 'tooltip', title: "#{group_name} - #{status.status}", placement: 'bottom' } } } + .build-content = render "projects/#{status.to_partial_path}_pipeline", subject: status - else %li.build diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml index 5d0d5ba0262..f2d71fa6989 100644 --- a/app/views/projects/commit/_pipeline_status_group.html.haml +++ b/app/views/projects/commit/_pipeline_status_group.html.haml @@ -1,5 +1,5 @@ - group_status = CommitStatus.where(id: subject).status -%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } +%button.dropdown-menu-toggle.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } } %span.ci-status-icon = render_status_with_link('build', group_status) %span.ci-status-text 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 0a66d60accc..c45b73e4225 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,9 +1,10 @@ -- if subject.target_url - = link_to subject.target_url do +%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 + = render_status_with_link('commit status', subject.status) + %span.ci-status-text= subject.name + - else %span.ci-status-icon = render_status_with_link('commit status', subject.status) %span.ci-status-text= subject.name -- else - %span.ci-status-icon - = render_status_with_link('commit status', subject.status) - %span.ci-status-text= subject.name -- cgit v1.2.3 From b6c859974307b71a82544cb137196b1bcd7ef7ef Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Wed, 19 Oct 2016 16:53:12 +0100 Subject: Revert "Add #closed_without_source_project?" This reverts commit 31c37c6c38258684fc92e0d91119c33872e39034. See #23341 --- app/models/merge_request.rb | 10 ++----- app/views/projects/merge_requests/_show.html.haml | 31 ++++++++++------------ spec/models/merge_request_spec.rb | 32 ----------------------- 3 files changed, 16 insertions(+), 57 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 0cc0b3c2a0e..d32bc9f882f 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -333,11 +333,7 @@ class MergeRequest < ActiveRecord::Base end def closed_without_fork? - closed? && forked_source_project_missing? - end - - def closed_without_source_project? - closed? && !source_project + closed? && (forked_source_project_missing? || !source_project) end def forked_source_project_missing? @@ -348,9 +344,7 @@ class MergeRequest < ActiveRecord::Base end def reopenable? - return false if closed_without_fork? || closed_without_source_project? || merged? - - closed? + source_branch_exists? && closed? end def ensure_merge_request_diff diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index cd98aaf8d75..fe90383b00f 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -26,19 +26,17 @@ %ul.dropdown-menu.dropdown-menu-align-right %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) - - unless @merge_request.closed_without_fork? - .normal - %span Request to merge - %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) - - if @merge_request.open? && @merge_request.diverged_from_target_branch? - %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind) + .normal + %span Request to merge + %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) + - if @merge_request.open? && @merge_request.diverged_from_target_branch? + %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind) - - unless @merge_request.closed_without_source_project? - = render "projects/merge_requests/show/how_to_merge" - = render "projects/merge_requests/widget/show.html.haml" + = render "projects/merge_requests/show/how_to_merge" + = 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 @@ -52,11 +50,10 @@ = 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 - - unless @merge_request.closed_without_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 - Commits - %span.badge= @commits_count + %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 + Commits + %span.badge= @commits_count - if @pipeline %li.pipelines-tab = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 6db5e7f7d80..ee003a9d18f 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1274,38 +1274,6 @@ describe MergeRequest, models: true do end end - describe '#closed_without_source_project?' do - let(:project) { create(:project) } - let(:user) { create(:user) } - let(:fork_project) { create(:project, forked_from_project: project, namespace: user.namespace) } - let(:destroy_service) { Projects::DestroyService.new(fork_project, user) } - - context 'when the merge request is closed' do - let(:closed_merge_request) do - create(:closed_merge_request, - source_project: fork_project, - target_project: project) - end - - it 'returns false if the source project exists' do - expect(closed_merge_request.closed_without_source_project?).to be_falsey - end - - it 'returns true if the source project does not exist' do - destroy_service.execute - closed_merge_request.reload - - expect(closed_merge_request.closed_without_source_project?).to be_truthy - end - end - - context 'when the merge request is open' do - it 'returns false' do - expect(subject.closed_without_source_project?).to be_falsey - end - end - end - describe '#reopenable?' do context 'when the merge request is closed' do it 'returns true' do -- cgit v1.2.3 From f00ee5c2ed9a0812b6337e52814756c560c879c8 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Wed, 19 Oct 2016 17:13:04 +0100 Subject: Fix the merge request view when source projects or branches are removed --- app/controllers/projects/merge_requests_controller.rb | 2 +- app/helpers/merge_requests_helper.rb | 10 +++++++--- app/models/merge_request.rb | 15 ++++++--------- app/views/projects/merge_requests/_show.html.haml | 13 ++++++++----- spec/features/merge_requests/created_from_fork_spec.rb | 14 ++++++++++++++ 5 files changed, 36 insertions(+), 18 deletions(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 0f593d1a936..55ea31e48a0 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -554,7 +554,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def define_pipelines_vars @pipelines = @merge_request.all_pipelines - if @pipelines.any? + if @pipelines.present? @pipeline = @pipelines.first @statuses = @pipeline.statuses.relevant end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 249cb44e9d5..a6659ea2fd1 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -86,11 +86,15 @@ module MergeRequestsHelper end def source_branch_with_namespace(merge_request) - branch = link_to(merge_request.source_branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch)) + namespace = merge_request.source_project_namespace + branch = merge_request.source_branch + + if merge_request.source_branch_exists? + namespace = link_to(namespace, project_path(merge_request.source_project)) + branch = link_to(branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch)) + end if merge_request.for_fork? - namespace = link_to(merge_request.source_project_namespace, - project_path(merge_request.source_project)) namespace + ":" + branch else branch diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index d32bc9f882f..45fa8af60e5 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -333,7 +333,7 @@ class MergeRequest < ActiveRecord::Base end def closed_without_fork? - closed? && (forked_source_project_missing? || !source_project) + closed? && forked_source_project_missing? end def forked_source_project_missing? @@ -344,7 +344,7 @@ class MergeRequest < ActiveRecord::Base end def reopenable? - source_branch_exists? && closed? + closed? && !source_project_missing? && source_branch_exists? end def ensure_merge_request_diff @@ -656,7 +656,7 @@ class MergeRequest < ActiveRecord::Base end def has_ci? - source_project.ci_service && commits.any? + source_project.try(:ci_service) && commits.any? end def branch_missing? @@ -688,12 +688,9 @@ class MergeRequest < ActiveRecord::Base @environments ||= begin - environments = source_project.environments_for( - source_branch, diff_head_commit) - environments += target_project.environments_for( - target_branch, diff_head_commit, with_tags: true) - - environments.uniq + envs = target_project.environments_for(target_branch, diff_head_commit, with_tags: true) + envs.concat(source_project.environments_for(source_branch, diff_head_commit)) if source_project + envs.uniq end end diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index fe90383b00f..0e19d224fcd 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -35,7 +35,9 @@ - if @merge_request.open? && @merge_request.diverged_from_target_branch? %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind) - = render "projects/merge_requests/show/how_to_merge" + - if @merge_request.source_branch_exists? + = render "projects/merge_requests/show/how_to_merge" + = 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) @@ -50,10 +52,11 @@ = 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 - %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 - Commits - %span.badge= @commits_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 + Commits + %span.badge= @commits_count - if @pipeline %li.pipelines-tab = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb index a506624b30d..cfc1244429f 100644 --- a/spec/features/merge_requests/created_from_fork_spec.rb +++ b/spec/features/merge_requests/created_from_fork_spec.rb @@ -25,6 +25,20 @@ feature 'Merge request created from fork' do expect(page).to have_content 'Test merge request' end + context 'source project is deleted' do + background do + MergeRequests::MergeService.new(project, user).execute(merge_request) + fork_project.destroy! + end + + scenario 'user can access merge request' do + visit_merge_request(merge_request) + + expect(page).to have_content 'Test merge request' + expect(page).to have_content "(removed):#{merge_request.source_branch}" + end + end + context 'pipeline present in source project' do include WaitForAjax -- cgit v1.2.3 From f118d2fc7394cbbd71a8c97e0c186581c045fb47 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Wed, 19 Oct 2016 17:48:30 +0100 Subject: Fix two CI endpoints for MRs where the source project is deleted --- app/controllers/projects/merge_requests_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 55ea31e48a0..2ee53f7ceda 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -398,7 +398,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController status ||= "preparing" else - ci_service = @merge_request.source_project.ci_service + ci_service = @merge_request.source_project.try(:ci_service) status = ci_service.commit_status(merge_request.diff_head_sha, merge_request.source_branch) if ci_service if ci_service.respond_to?(:commit_coverage) -- cgit v1.2.3 From 51fbb9bfa90f293447cf3f338bdc0a189b601d0e Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Wed, 19 Oct 2016 19:33:51 +0100 Subject: Rename forked_source_project_missing? to source_project_missing? --- app/models/merge_request.rb | 6 +++--- spec/models/merge_request_spec.rb | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 45fa8af60e5..c476a3bb14e 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -326,17 +326,17 @@ class MergeRequest < ActiveRecord::Base def validate_fork return true unless target_project && source_project return true if target_project == source_project - return true unless forked_source_project_missing? + return true unless source_project_missing? errors.add :validate_fork, 'Source project is not a fork of the target project' end def closed_without_fork? - closed? && forked_source_project_missing? + closed? && source_project_missing? end - def forked_source_project_missing? + def source_project_missing? return false unless for_fork? return true unless source_project diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index ee003a9d18f..6e5137602aa 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1198,7 +1198,7 @@ describe MergeRequest, models: true do end end - describe "#forked_source_project_missing?" do + describe "#source_project_missing?" do let(:project) { create(:project) } let(:fork_project) { create(:project, forked_from_project: project) } let(:user) { create(:user) } @@ -1211,13 +1211,13 @@ describe MergeRequest, models: true do target_project: project) end - it { expect(merge_request.forked_source_project_missing?).to be_falsey } + it { expect(merge_request.source_project_missing?).to be_falsey } end context "when the source project is the same as the target project" do let(:merge_request) { create(:merge_request, source_project: project) } - it { expect(merge_request.forked_source_project_missing?).to be_falsey } + it { expect(merge_request.source_project_missing?).to be_falsey } end context "when the fork does not exist" do @@ -1231,7 +1231,7 @@ describe MergeRequest, models: true do unlink_project.execute merge_request.reload - expect(merge_request.forked_source_project_missing?).to be_truthy + expect(merge_request.source_project_missing?).to be_truthy end end end -- cgit v1.2.3 From 25447b46c6adf62a0aafc0da38d456ef80e489f3 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Wed, 19 Oct 2016 18:09:33 +0100 Subject: Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98cffab8c03..4f163f323e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ Please view this file on the master branch, on stable branches it's out of date. - ProjectCacheWorker updates caches at most once per 15 minutes per project - Fix Error 500 when viewing old merge requests with bad diff data - Create a new /templates namespace for the /licenses, /gitignores and /gitlab_ci_ymls API endpoints. !5717 (tbalthazar) + - Fix viewing merged MRs when the source project has been removed !6991 - Speed-up group milestones show page - Fix inconsistent options dropdown caret on mobile viewports (ClemMakesApps) - Extract project#update_merge_requests and SystemHooks to its own worker from GitPushService -- cgit v1.2.3 From 3ab461dc64d8cadd61a67e632fde309fb887d1e8 Mon Sep 17 00:00:00 2001 From: Airat Shigapov Date: Thu, 15 Sep 2016 18:45:22 +0300 Subject: Render hipchat notification descriptions as HTML instead of raw markdown --- app/models/project_services/hipchat_service.rb | 9 +++------ spec/models/project_services/hipchat_service_spec.rb | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index afebd3b6a12..98f0312d84e 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -144,8 +144,7 @@ class HipchatService < Service message = "#{user_name} #{state} #{issue_link} in #{project_link}: #{title}" if description - description = format_body(description) - message << description + message << format_body(Banzai::Filter::MarkdownFilter.renderer.render(description)) end message @@ -167,8 +166,7 @@ class HipchatService < Service "#{project_link}: #{title}" if description - description = format_body(description) - message << description + message << format_body(Banzai::Filter::MarkdownFilter.renderer.render(description)) end message @@ -219,8 +217,7 @@ class HipchatService < Service message << title if note - note = format_body(note) - message << note + message << format_body(Banzai::Filter::MarkdownFilter.renderer.render(note)) end message diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 26dd95bdfec..90066f01f6f 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -117,7 +117,7 @@ describe HipchatService, models: true do end context 'issue events' do - let(:issue) { create(:issue, title: 'Awesome issue', description: 'please fix') } + let(:issue) { create(:issue, title: 'Awesome issue', description: '**please** fix') } let(:issue_service) { Issues::CreateService.new(project, user) } let(:issues_sample_data) { issue_service.hook_data(issue, 'open') } @@ -135,12 +135,12 @@ describe HipchatService, models: true do "issue ##{obj_attr["iid"]} in " \ "#{project_name}: " \ "Awesome issue" \ - "

please fix
") + "

please fix

\n
") end end context 'merge request events' do - let(:merge_request) { create(:merge_request, description: 'please fix', title: 'Awesome merge request', target_project: project, source_project: project) } + let(:merge_request) { create(:merge_request, description: '**please** fix', title: 'Awesome merge request', target_project: project, source_project: project) } let(:merge_service) { MergeRequests::CreateService.new(project, user) } let(:merge_sample_data) { merge_service.hook_data(merge_request, 'open') } @@ -159,7 +159,7 @@ describe HipchatService, models: true do "merge request !#{obj_attr["iid"]} in " \ "#{project_name}: " \ "Awesome merge request" \ - "
please fix
") + "

please fix

\n
") end end @@ -190,7 +190,7 @@ describe HipchatService, models: true do "commit #{commit_id} in " \ "#{project_name}: " \ "#{title}" \ - "
a comment on a commit
") + "

a comment on a commit

\n
") end end @@ -203,7 +203,7 @@ describe HipchatService, models: true do let(:merge_request_note) do create(:note_on_merge_request, noteable: merge_request, project: project, - note: "merge request note") + note: "merge request **note**") end it "calls Hipchat API for merge request comment events" do @@ -222,7 +222,7 @@ describe HipchatService, models: true do "merge request !#{merge_id} in " \ "#{project_name}: " \ "#{title}" \ - "
merge request note
") + "

merge request note

\n
") end end @@ -230,7 +230,7 @@ describe HipchatService, models: true do let(:issue) { create(:issue, project: project) } let(:issue_note) do create(:note_on_issue, noteable: issue, project: project, - note: "issue note") + note: "issue **note**") end it "calls Hipchat API for issue comment events" do @@ -247,7 +247,7 @@ describe HipchatService, models: true do "issue ##{issue_id} in " \ "#{project_name}: " \ "#{title}" \ - "
issue note
") + "

issue note

\n
") end end @@ -275,7 +275,7 @@ describe HipchatService, models: true do "snippet ##{snippet_id} in " \ "#{project_name}: " \ "#{title}" \ - "
snippet note
") + "

snippet note

\n
") end end end -- cgit v1.2.3 From 0090116d87585ab6e433ba7c04198271848c43c0 Mon Sep 17 00:00:00 2001 From: David Eisner Date: Tue, 4 Oct 2016 12:28:42 +0100 Subject: Full Banzai rendering for HipChat notifications --- app/models/project_services/hipchat_service.rb | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 98f0312d84e..f4edbbbceb2 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -121,14 +121,6 @@ class HipchatService < Service message end - def format_body(body) - if body - body = body.truncate(200, separator: ' ', omission: '...') - end - - "
#{body}
" - end - def create_issue_message(data) user_name = data[:user][:name] @@ -144,7 +136,7 @@ class HipchatService < Service message = "#{user_name} #{state} #{issue_link} in #{project_link}: #{title}" if description - message << format_body(Banzai::Filter::MarkdownFilter.renderer.render(description)) + message << Banzai.render(note, project: project) end message @@ -166,7 +158,7 @@ class HipchatService < Service "#{project_link}: #{title}" if description - message << format_body(Banzai::Filter::MarkdownFilter.renderer.render(description)) + message << Banzai.render(note, project: project) end message @@ -217,7 +209,7 @@ class HipchatService < Service message << title if note - message << format_body(Banzai::Filter::MarkdownFilter.renderer.render(note)) + message << Banzai.render(note, project: project) end message -- cgit v1.2.3 From ad28b3989dece0d3495e08cf545708caf9642cfc Mon Sep 17 00:00:00 2001 From: David Eisner Date: Tue, 4 Oct 2016 12:38:31 +0100 Subject: Also render commit titles in HipChat notifications --- app/models/project_services/hipchat_service.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index f4edbbbceb2..7d70452a70b 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -88,6 +88,10 @@ class HipchatService < Service end end + def render_line(text) + Banzai.render(text.lines.first.chomp, project: project, pipeline: :single_line) if text + end + def create_push_message(push) ref_type = Gitlab::Git.tag_ref?(push[:ref]) ? 'tag' : 'branch' ref = Gitlab::Git.ref_name(push[:ref]) @@ -110,7 +114,7 @@ class HipchatService < Service message << "(Compare changes)" push[:commits].take(MAX_COMMITS).each do |commit| - message << "
- #{commit[:message].lines.first} (#{commit[:id][0..5]})" + message << "
- #{render_line(commit[:message])} (#{commit[:id][0..5]})" end if push[:commits].count > MAX_COMMITS @@ -126,7 +130,7 @@ class HipchatService < Service obj_attr = data[:object_attributes] obj_attr = HashWithIndifferentAccess.new(obj_attr) - title = obj_attr[:title] + title = render_line(obj_attr[:title]) state = obj_attr[:state] issue_iid = obj_attr[:iid] issue_url = obj_attr[:url] @@ -150,7 +154,7 @@ class HipchatService < Service merge_request_id = obj_attr[:iid] state = obj_attr[:state] description = obj_attr[:description] - title = obj_attr[:title] + title = render_line(obj_attr[:title]) merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}" merge_request_link = "merge request !#{merge_request_id}" @@ -165,7 +169,7 @@ class HipchatService < Service end def format_title(title) - "" + title.lines.first.chomp + "" + "#{render_line(title)}" end def create_note_message(data) -- cgit v1.2.3 From 59b2770a035489db9995b8638792ed5c92242035 Mon Sep 17 00:00:00 2001 From: David Eisner Date: Tue, 4 Oct 2016 16:23:16 +0100 Subject: Absolute URLs for Banzai HTML for HipChat Using "pipeline: :email" gets "only_path: false" into the context to produce full URLs instead of /namespace/project/... --- app/models/project_services/hipchat_service.rb | 32 +++++++++++++++++--------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 7d70452a70b..9d52a1423b7 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -89,7 +89,7 @@ class HipchatService < Service end def render_line(text) - Banzai.render(text.lines.first.chomp, project: project, pipeline: :single_line) if text + markdown(text.lines.first.chomp, pipeline: :single_line) if text end def create_push_message(push) @@ -125,6 +125,20 @@ class HipchatService < Service message end + def markdown(text, context = {}) + if text + context = ({ + project: project, + pipeline: :email + }).merge(context) + + html = Banzai.render(text, context) + html = Banzai.post_process(html, context) + else + "" + end + end + def create_issue_message(data) user_name = data[:user][:name] @@ -139,9 +153,7 @@ class HipchatService < Service issue_link = "issue ##{issue_iid}" message = "#{user_name} #{state} #{issue_link} in #{project_link}: #{title}" - if description - message << Banzai.render(note, project: project) - end + message << markdown(description) message end @@ -161,9 +173,7 @@ class HipchatService < Service message = "#{user_name} #{state} #{merge_request_link} in " \ "#{project_link}: #{title}" - if description - message << Banzai.render(note, project: project) - end + message << markdown(description) message end @@ -180,11 +190,13 @@ class HipchatService < Service note = obj_attr[:note] note_url = obj_attr[:url] noteable_type = obj_attr[:noteable_type] + commit_id = nil case noteable_type when "Commit" commit_attr = HashWithIndifferentAccess.new(data[:commit]) - subject_desc = commit_attr[:id] + commit_id = commit_attr[:id] + subject_desc = commit_id subject_desc = Commit.truncate_sha(subject_desc) subject_type = "commit" title = format_title(commit_attr[:message]) @@ -212,9 +224,7 @@ class HipchatService < Service message = "#{user_name} commented on #{subject_html} in #{project_link}: " message << title - if note - message << Banzai.render(note, project: project) - end + message << markdown(note, ref: commit_id) message end -- cgit v1.2.3 From 73f13526d6ef3da1289074e2503bf2764b41b137 Mon Sep 17 00:00:00 2001 From: David Eisner Date: Tue, 4 Oct 2016 16:25:45 +0100 Subject: Ensure absolute URLs for single lines from Banzai for HipChat "pipeline: :single_line" leaves the protocol/host part out of the URLs and caches them that way. To avoid giving those out to HipChat, markdown is always rendered with "pipeline: :email" first. There must be a better way to do this, but I can't see how to avoid the link caching. --- app/models/project_services/hipchat_service.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 9d52a1423b7..ce4a2a96015 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -125,12 +125,16 @@ class HipchatService < Service message end - def markdown(text, context = {}) + def markdown(text, options = {}) if text - context = ({ + context = { project: project, pipeline: :email - }).merge(context) + } + + Banzai.render(text, context) + + context.merge!(options) html = Banzai.render(text, context) html = Banzai.post_process(html, context) -- cgit v1.2.3 From 551c74edf75e3fa89ea57a45b217a3a34f8c2fc1 Mon Sep 17 00:00:00 2001 From: David Eisner Date: Tue, 4 Oct 2016 16:27:40 +0100 Subject: Clean up Banzai HTML for HipChat The `class` and `data-*` attributes are meaningless in HipChat, and it would probably be better to limit the tags, too. For example, we could avoid block-level elements in `render_line`. --- app/models/project_services/hipchat_service.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index ce4a2a96015..8988a7b905e 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -1,4 +1,6 @@ class HipchatService < Service + include ActionView::Helpers::SanitizeHelper + MAX_COMMITS = 3 prop_accessor :token, :room, :server, :notify, :color, :api_version @@ -138,6 +140,7 @@ class HipchatService < Service html = Banzai.render(text, context) html = Banzai.post_process(html, context) + sanitize html, attributes: %w(href title alt) else "" end -- cgit v1.2.3 From f6a97e6c0bf4d0f699ded24983c6be1ca4b5d6cc Mon Sep 17 00:00:00 2001 From: David Eisner Date: Wed, 5 Oct 2016 13:38:08 +0100 Subject: Tests for markdown HipChat notifications --- spec/models/project_services/hipchat_service_spec.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 90066f01f6f..1029b6d2459 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -135,7 +135,7 @@ describe HipchatService, models: true do "issue ##{obj_attr["iid"]} in " \ "#{project_name}: " \ "Awesome issue" \ - "

please fix

\n
") + "

please fix

") end end @@ -159,7 +159,7 @@ describe HipchatService, models: true do "merge request !#{obj_attr["iid"]} in " \ "#{project_name}: " \ "Awesome merge request" \ - "

please fix

\n
") + "

please fix

") end end @@ -190,7 +190,7 @@ describe HipchatService, models: true do "commit #{commit_id} in " \ "#{project_name}: " \ "#{title}" \ - "

a comment on a commit

\n
") + "

a comment on a commit

") end end @@ -222,7 +222,7 @@ describe HipchatService, models: true do "merge request !#{merge_id} in " \ "#{project_name}: " \ "#{title}" \ - "

merge request note

\n
") + "

merge request note

") end end @@ -247,7 +247,7 @@ describe HipchatService, models: true do "issue ##{issue_id} in " \ "#{project_name}: " \ "#{title}" \ - "

issue note

\n
") + "

issue note

") end end @@ -275,7 +275,7 @@ describe HipchatService, models: true do "snippet ##{snippet_id} in " \ "#{project_name}: " \ "#{title}" \ - "

snippet note

\n
") + "

snippet note

") end end end -- cgit v1.2.3 From 1c7807925aaa3efcc85275273cf65c574985dd61 Mon Sep 17 00:00:00 2001 From: Airat Shigapov Date: Wed, 12 Oct 2016 09:51:02 +0300 Subject: Use guard clause instead of if-else statement --- app/models/project_services/hipchat_service.rb | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 8988a7b905e..e7a77070b9f 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -128,22 +128,21 @@ class HipchatService < Service end def markdown(text, options = {}) - if text - context = { - project: project, - pipeline: :email - } + return "" unless text - Banzai.render(text, context) + context = { + project: project, + pipeline: :email + } - context.merge!(options) + Banzai.render(text, context) - html = Banzai.render(text, context) - html = Banzai.post_process(html, context) - sanitize html, attributes: %w(href title alt) - else - "" - end + context.merge!(options) + + html = Banzai.render(text, context) + html = Banzai.post_process(html, context) + + sanitize html, attributes: %w(href title alt) end def create_issue_message(data) -- cgit v1.2.3 From 5d608ceb4f433a0d3f196a2a5cb11cf6a535baf2 Mon Sep 17 00:00:00 2001 From: Airat Shigapov Date: Wed, 19 Oct 2016 22:51:15 +0300 Subject: Return truncation for notification descriptions, fix minor bugs with rendering --- app/models/project_services/hipchat_service.rb | 17 +++++++++++------ spec/models/project_services/hipchat_service_spec.rb | 12 ++++++------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index e7a77070b9f..660a8ae3421 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -2,6 +2,11 @@ class HipchatService < Service include ActionView::Helpers::SanitizeHelper MAX_COMMITS = 3 + HIPCHAT_ALLOWED_TAGS = %w[ + a b i strong em br img pre code + table th tr td caption colgroup col thead tbody tfoot + ul ol li dl dt dd + ] prop_accessor :token, :room, :server, :notify, :color, :api_version boolean_accessor :notify_only_broken_builds @@ -139,10 +144,10 @@ class HipchatService < Service context.merge!(options) - html = Banzai.render(text, context) - html = Banzai.post_process(html, context) + html = Banzai.post_process(Banzai.render(text, context), context) + sanitized_html = sanitize(html, tags: HIPCHAT_ALLOWED_TAGS, attributes: %w[href title alt]) - sanitize html, attributes: %w(href title alt) + sanitized_html.truncate(200, separator: ' ', omission: '...') end def create_issue_message(data) @@ -159,7 +164,7 @@ class HipchatService < Service issue_link = "issue ##{issue_iid}" message = "#{user_name} #{state} #{issue_link} in #{project_link}: #{title}" - message << markdown(description) + message << "
#{markdown(description)}
" message end @@ -179,7 +184,7 @@ class HipchatService < Service message = "#{user_name} #{state} #{merge_request_link} in " \ "#{project_link}: #{title}" - message << markdown(description) + message << "
#{markdown(description)}
" message end @@ -230,7 +235,7 @@ class HipchatService < Service message = "#{user_name} commented on #{subject_html} in #{project_link}: " message << title - message << markdown(note, ref: commit_id) + message << "
#{markdown(note, ref: commit_id)}
" message end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 1029b6d2459..2da3a9cb09f 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -135,7 +135,7 @@ describe HipchatService, models: true do "issue ##{obj_attr["iid"]} in " \ "#{project_name}: " \ "Awesome issue" \ - "

please fix

") + "
please fix
") end end @@ -159,7 +159,7 @@ describe HipchatService, models: true do "merge request !#{obj_attr["iid"]} in " \ "#{project_name}: " \ "Awesome merge request" \ - "

please fix

") + "
please fix
") end end @@ -190,7 +190,7 @@ describe HipchatService, models: true do "commit #{commit_id} in " \ "#{project_name}: " \ "#{title}" \ - "

a comment on a commit

") + "
a comment on a commit
") end end @@ -222,7 +222,7 @@ describe HipchatService, models: true do "merge request !#{merge_id} in " \ "#{project_name}: " \ "#{title}" \ - "

merge request note

") + "
merge request note
") end end @@ -247,7 +247,7 @@ describe HipchatService, models: true do "issue ##{issue_id} in " \ "#{project_name}: " \ "#{title}" \ - "

issue note

") + "
issue note
") end end @@ -275,7 +275,7 @@ describe HipchatService, models: true do "snippet ##{snippet_id} in " \ "#{project_name}: " \ "#{title}" \ - "

snippet note

") + "
snippet note
") end end end -- cgit v1.2.3 From 435678d58828db76c3ef5f95643dac0e3ae979b3 Mon Sep 17 00:00:00 2001 From: Airat Shigapov Date: Thu, 20 Oct 2016 15:12:39 +0300 Subject: Add CHANGELOG.md entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f163f323e6..0bfdf23b959 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. ## 8.14.0 (2016-11-22) - Adds user project membership expired event to clarify why user was removed (Callum Dryden) + - Fix HipChat notifications rendering (airatshigapov, eisnerd) - Simpler arguments passed to named_route on toggle_award_url helper method ## 8.13.0 (2016-10-22) -- cgit v1.2.3 From ea704005c7bdca55e2782a2c24fe0c5e89cad179 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 19 Oct 2016 14:48:37 +0200 Subject: Update duration at the end of pipeline --- CHANGELOG.md | 1 + app/models/ci/pipeline.rb | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bfdf23b959..de24e08c52f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Add `/projects/visible` API endpoint (Ben Boeckel) - Fix centering of custom header logos (Ashley Dumaine) - Keep around commits only pipeline creation as pipeline data doesn't change over time + - Update duration at the end of pipeline - ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup - Add group level labels. (!6425) - Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index e84c91b417d..d5c1e03b461 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -59,9 +59,6 @@ module Ci before_transition any => [:success, :failed, :canceled] do |pipeline| pipeline.finished_at = Time.now - end - - before_transition do |pipeline| pipeline.update_duration end -- cgit v1.2.3 From 523ee4a5f4c28aab29310a742f4798bc77f20ccf Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 20 Oct 2016 16:16:57 +0800 Subject: Preserve note_type and position for notes from emails Closes #23208 --- lib/gitlab/email/handler/create_note_handler.rb | 4 +++- spec/fixtures/emails/commands_in_reply.eml | 2 -- spec/fixtures/emails/commands_only_reply.eml | 2 -- .../email/handler/create_note_handler_spec.rb | 24 ++++++++++++---------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/gitlab/email/handler/create_note_handler.rb b/lib/gitlab/email/handler/create_note_handler.rb index 06dae31cc27..447c7a6a6b9 100644 --- a/lib/gitlab/email/handler/create_note_handler.rb +++ b/lib/gitlab/email/handler/create_note_handler.rb @@ -46,7 +46,9 @@ module Gitlab noteable_type: sent_notification.noteable_type, noteable_id: sent_notification.noteable_id, commit_id: sent_notification.commit_id, - line_code: sent_notification.line_code + line_code: sent_notification.line_code, + position: sent_notification.position, + type: sent_notification.note_type ).execute end end diff --git a/spec/fixtures/emails/commands_in_reply.eml b/spec/fixtures/emails/commands_in_reply.eml index 06bf60ab734..712f6f797b4 100644 --- a/spec/fixtures/emails/commands_in_reply.eml +++ b/spec/fixtures/emails/commands_in_reply.eml @@ -23,8 +23,6 @@ Cool! /close /todo -/due tomorrow - On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta wrote: diff --git a/spec/fixtures/emails/commands_only_reply.eml b/spec/fixtures/emails/commands_only_reply.eml index aed64224b06..2d2e2f94290 100644 --- a/spec/fixtures/emails/commands_only_reply.eml +++ b/spec/fixtures/emails/commands_only_reply.eml @@ -21,8 +21,6 @@ X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1 /close /todo -/due tomorrow - On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta wrote: diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb index 4909fed6b77..48660d1dd1b 100644 --- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb @@ -12,10 +12,13 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do let(:email_raw) { fixture_file('emails/valid_reply.eml') } let(:project) { create(:project, :public) } - let(:noteable) { create(:issue, project: project) } let(:user) { create(:user) } + let(:note) { create(:diff_note_on_merge_request, project: project) } + let(:noteable) { note.noteable } - let!(:sent_notification) { SentNotification.record(noteable, user.id, mail_key) } + let!(:sent_notification) do + SentNotification.record_note(note, user.id, mail_key) + end context "when the recipient address doesn't include a mail key" do let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "") } @@ -82,7 +85,6 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do expect { receiver.execute }.to change { noteable.notes.count }.by(1) expect(noteable.reload).to be_closed - expect(noteable.due_date).to eq(Date.tomorrow) expect(TodoService.new.todo_exist?(noteable, user)).to be_truthy end end @@ -100,7 +102,6 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do expect { receiver.execute }.to change { noteable.notes.count }.by(1) expect(noteable.reload).to be_open - expect(noteable.due_date).to be_nil expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy end end @@ -117,7 +118,6 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do expect { receiver.execute }.to change { noteable.notes.count }.by(2) expect(noteable.reload).to be_closed - expect(noteable.due_date).to eq(Date.tomorrow) expect(TodoService.new.todo_exist?(noteable, user)).to be_truthy end end @@ -138,10 +138,11 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do it "creates a comment" do expect { receiver.execute }.to change { noteable.notes.count }.by(1) - note = noteable.notes.last + new_note = noteable.notes.last - expect(note.author).to eq(sent_notification.recipient) - expect(note.note).to include("I could not disagree more.") + expect(new_note.author).to eq(sent_notification.recipient) + expect(new_note.position).to eq(note.position) + expect(new_note.note).to include("I could not disagree more.") end it "adds all attachments" do @@ -160,10 +161,11 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do shared_examples 'an email that contains a mail key' do |header| it "fetches the mail key from the #{header} header and creates a comment" do expect { receiver.execute }.to change { noteable.notes.count }.by(1) - note = noteable.notes.last + new_note = noteable.notes.last - expect(note.author).to eq(sent_notification.recipient) - expect(note.note).to include('I could not disagree more.') + expect(new_note.author).to eq(sent_notification.recipient) + expect(new_note.position).to eq(note.position) + expect(new_note.note).to include('I could not disagree more.') end end -- cgit v1.2.3 From cbb65aae395821c596746d2b347fc3a7de1b3da8 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 20 Oct 2016 16:24:37 +0800 Subject: Add CHANGELOG entry [ci skip] --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index de24e08c52f..98d95e275e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ Please view this file on the master branch, on stable branches it's out of date. - Fix HipChat notifications rendering (airatshigapov, eisnerd) - Simpler arguments passed to named_route on toggle_award_url helper method + - Fix discussion thread from emails for merge requests. !7010 + ## 8.13.0 (2016-10-22) - Fix save button on project pipeline settings page. (!6955) -- cgit v1.2.3 From 25d00ea871a970fb82d79067d97c84fdcb5264c5 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 20 Oct 2016 20:47:21 +0800 Subject: We want to release this in 8.13.0 --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98d95e275e2..73dc323e02c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,6 @@ Please view this file on the master branch, on stable branches it's out of date. - Fix HipChat notifications rendering (airatshigapov, eisnerd) - Simpler arguments passed to named_route on toggle_award_url helper method - - Fix discussion thread from emails for merge requests. !7010 - ## 8.13.0 (2016-10-22) - Fix save button on project pipeline settings page. (!6955) @@ -47,6 +45,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Speed-up group milestones show page - Fix inconsistent options dropdown caret on mobile viewports (ClemMakesApps) - Extract project#update_merge_requests and SystemHooks to its own worker from GitPushService + - Fix discussion thread from emails for merge requests. !7010 - Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs) - Add tag shortcut from the Commit page. !6543 - Keep refs for each deployment -- cgit v1.2.3 From 8979567e439c624145ffef7ebc255b7936b5d05e Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Wed, 19 Oct 2016 14:49:09 +0200 Subject: Use deployment IID when saving refs --- app/models/deployment.rb | 2 +- app/models/environment.rb | 4 ++-- spec/controllers/projects/merge_requests_controller_spec.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 1f8c5fb3d85..c843903877b 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -102,6 +102,6 @@ class Deployment < ActiveRecord::Base private def ref_path - File.join(environment.ref_path, 'deployments', id.to_s) + File.join(environment.ref_path, 'deployments', iid.to_s) end end diff --git a/app/models/environment.rb b/app/models/environment.rb index d575f1dc73a..73f415c0ef0 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -71,8 +71,8 @@ class Environment < ActiveRecord::Base return nil unless ref - deployment_id = ref.split('/').last - deployments.find(deployment_id) + deployment_iid = ref.split('/').last + deployments.find_by(iid: deployment_iid) end def ref_path diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index d6980471ea4..940d54f8686 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -913,7 +913,7 @@ describe Projects::MergeRequestsController do end describe 'GET ci_environments_status' do - context 'when the environment is from a forked project' do + context 'the environment is from a forked project' do let!(:forked) { create(:project) } let!(:environment) { create(:environment, project: forked) } let!(:deployment) { create(:deployment, environment: environment, sha: forked.commit.id, ref: 'master') } -- cgit v1.2.3 From 1cb75998992745216cb634537c281561559d1964 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Thu, 20 Oct 2016 14:21:40 +0200 Subject: Only create refs for new deployments This patch makes sure GitLab does not save the refs to the filesystem each time the deployment is updated. This will save some IO although I expect the impact to be minimal. --- app/models/deployment.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/deployment.rb b/app/models/deployment.rb index c843903877b..91d85c2279b 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -11,7 +11,7 @@ class Deployment < ActiveRecord::Base delegate :name, to: :environment, prefix: true - after_save :create_ref + after_create :create_ref def commit project.commit(sha) -- cgit v1.2.3 From 3974c2e9e18d80a9b5a49bd16ea3fc0d157dd919 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 14 Oct 2016 17:25:12 -0500 Subject: Create protected branches bundle --- .../protected_branch_access_dropdown.js.es6 | 28 -------- .../javascripts/protected_branch_create.js.es6 | 54 --------------- .../javascripts/protected_branch_dropdown.js.es6 | 76 ---------------------- .../javascripts/protected_branch_edit.js.es6 | 65 ------------------ .../javascripts/protected_branch_edit_list.js.es6 | 17 ----- .../protected_branch_access_dropdown.js.es6 | 28 ++++++++ .../protected_branch_create.js.es6 | 54 +++++++++++++++ .../protected_branch_dropdown.js.es6 | 76 ++++++++++++++++++++++ .../protected_branch_edit.js.es6 | 65 ++++++++++++++++++ .../protected_branch_edit_list.js.es6 | 17 +++++ .../protected_branches_bundle.js | 1 + .../projects/protected_branches/index.html.haml | 2 + config/application.rb | 1 + 13 files changed, 244 insertions(+), 240 deletions(-) delete mode 100644 app/assets/javascripts/protected_branch_access_dropdown.js.es6 delete mode 100644 app/assets/javascripts/protected_branch_create.js.es6 delete mode 100644 app/assets/javascripts/protected_branch_dropdown.js.es6 delete mode 100644 app/assets/javascripts/protected_branch_edit.js.es6 delete mode 100644 app/assets/javascripts/protected_branch_edit_list.js.es6 create mode 100644 app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 create mode 100644 app/assets/javascripts/protected_branches/protected_branch_create.js.es6 create mode 100644 app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 create mode 100644 app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 create mode 100644 app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 create mode 100644 app/assets/javascripts/protected_branches/protected_branches_bundle.js diff --git a/app/assets/javascripts/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branch_access_dropdown.js.es6 deleted file mode 100644 index 7aeb5f92514..00000000000 --- a/app/assets/javascripts/protected_branch_access_dropdown.js.es6 +++ /dev/null @@ -1,28 +0,0 @@ -(global => { - global.gl = global.gl || {}; - - gl.ProtectedBranchAccessDropdown = class { - constructor(options) { - const { $dropdown, data, onSelect } = options; - - $dropdown.glDropdown({ - data: data, - selectable: true, - inputId: $dropdown.data('input-id'), - fieldName: $dropdown.data('field-name'), - toggleLabel(item, el) { - if (el.is('.is-active')) { - return item.text; - } else { - return 'Select'; - } - }, - clicked(item, $el, e) { - e.preventDefault(); - onSelect(); - } - }); - } - } - -})(window); diff --git a/app/assets/javascripts/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branch_create.js.es6 deleted file mode 100644 index 46beca469b9..00000000000 --- a/app/assets/javascripts/protected_branch_create.js.es6 +++ /dev/null @@ -1,54 +0,0 @@ -(global => { - global.gl = global.gl || {}; - - gl.ProtectedBranchCreate = class { - constructor() { - this.$wrap = this.$form = $('#new_protected_branch'); - this.buildDropdowns(); - } - - buildDropdowns() { - const $allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); - const $allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); - - // Cache callback - this.onSelectCallback = this.onSelect.bind(this); - - // Allowed to Merge dropdown - new gl.ProtectedBranchAccessDropdown({ - $dropdown: $allowedToMergeDropdown, - data: gon.merge_access_levels, - onSelect: this.onSelectCallback - }); - - // Allowed to Push dropdown - new gl.ProtectedBranchAccessDropdown({ - $dropdown: $allowedToPushDropdown, - data: gon.push_access_levels, - onSelect: this.onSelectCallback - }); - - // Select default - $allowedToPushDropdown.data('glDropdown').selectRowAtIndex(0); - $allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(0); - - // Protected branch dropdown - new ProtectedBranchDropdown({ - $dropdown: this.$wrap.find('.js-protected-branch-select'), - onSelect: this.onSelectCallback - }); - } - - // This will run after clicked callback - onSelect() { - - // Enable submit button - const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]'); - const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]'); - const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]'); - - this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length)); - } - } - -})(window); diff --git a/app/assets/javascripts/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branch_dropdown.js.es6 deleted file mode 100644 index 983322cbecc..00000000000 --- a/app/assets/javascripts/protected_branch_dropdown.js.es6 +++ /dev/null @@ -1,76 +0,0 @@ -class ProtectedBranchDropdown { - constructor(options) { - this.onSelect = options.onSelect; - this.$dropdown = options.$dropdown; - this.$dropdownContainer = this.$dropdown.parent(); - this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer'); - this.$protectedBranch = this.$dropdownContainer.find('.create-new-protected-branch'); - - this.buildDropdown(); - this.bindEvents(); - - // Hide footer - this.$dropdownFooter.addClass('hidden'); - } - - buildDropdown() { - this.$dropdown.glDropdown({ - data: this.getProtectedBranches.bind(this), - filterable: true, - remote: false, - search: { - fields: ['title'] - }, - selectable: true, - toggleLabel(selected) { - return (selected && 'id' in selected) ? selected.title : 'Protected Branch'; - }, - fieldName: 'protected_branch[name]', - text(protectedBranch) { - return _.escape(protectedBranch.title); - }, - id(protectedBranch) { - return _.escape(protectedBranch.id); - }, - onFilter: this.toggleCreateNewButton.bind(this), - clicked: (item, $el, e) => { - e.preventDefault(); - this.onSelect(); - } - }); - } - - bindEvents() { - this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this)); - } - - onClickCreateWildcard() { - // Refresh the dropdown's data, which ends up calling `getProtectedBranches` - this.$dropdown.data('glDropdown').remote.execute(); - this.$dropdown.data('glDropdown').selectRowAtIndex(0); - } - - getProtectedBranches(term, callback) { - if (this.selectedBranch) { - callback(gon.open_branches.concat(this.selectedBranch)); - } else { - callback(gon.open_branches); - } - } - - toggleCreateNewButton(branchName) { - this.selectedBranch = { - title: branchName, - id: branchName, - text: branchName - }; - - if (branchName) { - this.$dropdownContainer - .find('.create-new-protected-branch code') - .text(branchName); - } - - this.$dropdownFooter.toggleClass('hidden', !branchName); - } -} diff --git a/app/assets/javascripts/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branch_edit.js.es6 deleted file mode 100644 index 15a6dca2875..00000000000 --- a/app/assets/javascripts/protected_branch_edit.js.es6 +++ /dev/null @@ -1,65 +0,0 @@ -(global => { - global.gl = global.gl || {}; - - gl.ProtectedBranchEdit = class { - constructor(options) { - this.$wrap = options.$wrap; - this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); - this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); - - this.buildDropdowns(); - } - - buildDropdowns() { - - // Allowed to merge dropdown - new gl.ProtectedBranchAccessDropdown({ - $dropdown: this.$allowedToMergeDropdown, - data: gon.merge_access_levels, - onSelect: this.onSelect.bind(this) - }); - - // Allowed to push dropdown - new gl.ProtectedBranchAccessDropdown({ - $dropdown: this.$allowedToPushDropdown, - data: gon.push_access_levels, - onSelect: this.onSelect.bind(this) - }); - } - - onSelect() { - const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`); - 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; - - $.ajax({ - type: 'POST', - url: this.$wrap.data('url'), - dataType: 'json', - data: { - _method: 'PATCH', - protected_branch: { - merge_access_levels_attributes: [{ - id: this.$allowedToMergeDropdown.data('access-level-id'), - access_level: $allowedToMergeInput.val() - }], - push_access_levels_attributes: [{ - id: this.$allowedToPushDropdown.data('access-level-id'), - access_level: $allowedToPushInput.val() - }] - } - }, - success: () => { - this.$wrap.effect('highlight'); - }, - error() { - $.scrollTo(0); - new Flash('Failed to update branch!'); - } - }); - } - } - -})(window); diff --git a/app/assets/javascripts/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branch_edit_list.js.es6 deleted file mode 100644 index 9ff0fd12c76..00000000000 --- a/app/assets/javascripts/protected_branch_edit_list.js.es6 +++ /dev/null @@ -1,17 +0,0 @@ -(global => { - global.gl = global.gl || {}; - - gl.ProtectedBranchEditList = class { - constructor() { - this.$wrap = $('.protected-branches-list'); - - // Build edit forms - this.$wrap.find('.js-protected-branch-edit-form').each((i, el) => { - new gl.ProtectedBranchEdit({ - $wrap: $(el) - }); - }); - } - } - -})(window); 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 new file mode 100644 index 00000000000..7aeb5f92514 --- /dev/null +++ b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 @@ -0,0 +1,28 @@ +(global => { + global.gl = global.gl || {}; + + gl.ProtectedBranchAccessDropdown = class { + constructor(options) { + const { $dropdown, data, onSelect } = options; + + $dropdown.glDropdown({ + data: data, + selectable: true, + inputId: $dropdown.data('input-id'), + fieldName: $dropdown.data('field-name'), + toggleLabel(item, el) { + if (el.is('.is-active')) { + return item.text; + } else { + return 'Select'; + } + }, + clicked(item, $el, e) { + e.preventDefault(); + onSelect(); + } + }); + } + } + +})(window); diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 new file mode 100644 index 00000000000..46beca469b9 --- /dev/null +++ b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 @@ -0,0 +1,54 @@ +(global => { + global.gl = global.gl || {}; + + gl.ProtectedBranchCreate = class { + constructor() { + this.$wrap = this.$form = $('#new_protected_branch'); + this.buildDropdowns(); + } + + buildDropdowns() { + const $allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); + const $allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); + + // Cache callback + this.onSelectCallback = this.onSelect.bind(this); + + // Allowed to Merge dropdown + new gl.ProtectedBranchAccessDropdown({ + $dropdown: $allowedToMergeDropdown, + data: gon.merge_access_levels, + onSelect: this.onSelectCallback + }); + + // Allowed to Push dropdown + new gl.ProtectedBranchAccessDropdown({ + $dropdown: $allowedToPushDropdown, + data: gon.push_access_levels, + onSelect: this.onSelectCallback + }); + + // Select default + $allowedToPushDropdown.data('glDropdown').selectRowAtIndex(0); + $allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(0); + + // Protected branch dropdown + new ProtectedBranchDropdown({ + $dropdown: this.$wrap.find('.js-protected-branch-select'), + onSelect: this.onSelectCallback + }); + } + + // This will run after clicked callback + onSelect() { + + // Enable submit button + const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]'); + const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]'); + const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]'); + + this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length)); + } + } + +})(window); diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 new file mode 100644 index 00000000000..983322cbecc --- /dev/null +++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 @@ -0,0 +1,76 @@ +class ProtectedBranchDropdown { + constructor(options) { + this.onSelect = options.onSelect; + this.$dropdown = options.$dropdown; + this.$dropdownContainer = this.$dropdown.parent(); + this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer'); + this.$protectedBranch = this.$dropdownContainer.find('.create-new-protected-branch'); + + this.buildDropdown(); + this.bindEvents(); + + // Hide footer + this.$dropdownFooter.addClass('hidden'); + } + + buildDropdown() { + this.$dropdown.glDropdown({ + data: this.getProtectedBranches.bind(this), + filterable: true, + remote: false, + search: { + fields: ['title'] + }, + selectable: true, + toggleLabel(selected) { + return (selected && 'id' in selected) ? selected.title : 'Protected Branch'; + }, + fieldName: 'protected_branch[name]', + text(protectedBranch) { + return _.escape(protectedBranch.title); + }, + id(protectedBranch) { + return _.escape(protectedBranch.id); + }, + onFilter: this.toggleCreateNewButton.bind(this), + clicked: (item, $el, e) => { + e.preventDefault(); + this.onSelect(); + } + }); + } + + bindEvents() { + this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this)); + } + + onClickCreateWildcard() { + // Refresh the dropdown's data, which ends up calling `getProtectedBranches` + this.$dropdown.data('glDropdown').remote.execute(); + this.$dropdown.data('glDropdown').selectRowAtIndex(0); + } + + getProtectedBranches(term, callback) { + if (this.selectedBranch) { + callback(gon.open_branches.concat(this.selectedBranch)); + } else { + callback(gon.open_branches); + } + } + + toggleCreateNewButton(branchName) { + this.selectedBranch = { + title: branchName, + id: branchName, + text: branchName + }; + + if (branchName) { + this.$dropdownContainer + .find('.create-new-protected-branch code') + .text(branchName); + } + + this.$dropdownFooter.toggleClass('hidden', !branchName); + } +} diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 new file mode 100644 index 00000000000..15a6dca2875 --- /dev/null +++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 @@ -0,0 +1,65 @@ +(global => { + global.gl = global.gl || {}; + + gl.ProtectedBranchEdit = class { + constructor(options) { + this.$wrap = options.$wrap; + this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); + this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); + + this.buildDropdowns(); + } + + buildDropdowns() { + + // Allowed to merge dropdown + new gl.ProtectedBranchAccessDropdown({ + $dropdown: this.$allowedToMergeDropdown, + data: gon.merge_access_levels, + onSelect: this.onSelect.bind(this) + }); + + // Allowed to push dropdown + new gl.ProtectedBranchAccessDropdown({ + $dropdown: this.$allowedToPushDropdown, + data: gon.push_access_levels, + onSelect: this.onSelect.bind(this) + }); + } + + onSelect() { + const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`); + 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; + + $.ajax({ + type: 'POST', + url: this.$wrap.data('url'), + dataType: 'json', + data: { + _method: 'PATCH', + protected_branch: { + merge_access_levels_attributes: [{ + id: this.$allowedToMergeDropdown.data('access-level-id'), + access_level: $allowedToMergeInput.val() + }], + push_access_levels_attributes: [{ + id: this.$allowedToPushDropdown.data('access-level-id'), + access_level: $allowedToPushInput.val() + }] + } + }, + success: () => { + this.$wrap.effect('highlight'); + }, + error() { + $.scrollTo(0); + new Flash('Failed to update branch!'); + } + }); + } + } + +})(window); 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 new file mode 100644 index 00000000000..9ff0fd12c76 --- /dev/null +++ b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 @@ -0,0 +1,17 @@ +(global => { + global.gl = global.gl || {}; + + gl.ProtectedBranchEditList = class { + constructor() { + this.$wrap = $('.protected-branches-list'); + + // Build edit forms + this.$wrap.find('.js-protected-branch-edit-form').each((i, el) => { + new gl.ProtectedBranchEdit({ + $wrap: $(el) + }); + }); + } + } + +})(window); diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js new file mode 100644 index 00000000000..15b3affd469 --- /dev/null +++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js @@ -0,0 +1 @@ +/*= require_tree . */ diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 49dcc9a6ba4..42e9bdbd30e 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -1,4 +1,6 @@ - page_title "Protected branches" +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('protected_branches/protected_branches_bundle.js') .row.prepend-top-default.append-bottom-default .col-lg-3 diff --git a/config/application.rb b/config/application.rb index 8a9c539cb43..f3337b00dc6 100644 --- a/config/application.rb +++ b/config/application.rb @@ -87,6 +87,7 @@ module Gitlab config.assets.precompile << "users/users_bundle.js" config.assets.precompile << "network/network_bundle.js" config.assets.precompile << "profile/profile_bundle.js" + config.assets.precompile << "protected_branches/protected_branches_bundle.js" config.assets.precompile << "diff_notes/diff_notes_bundle.js" config.assets.precompile << "boards/boards_bundle.js" config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js" -- cgit v1.2.3 From a2aa1abbc272b285dab12f03465f13e71fc91e15 Mon Sep 17 00:00:00 2001 From: Adam Niedzielski Date: Wed, 19 Oct 2016 15:35:38 +0200 Subject: Test GitLab project import for a user with only their default namespace. Refactor the spec file: - remove hardcoded record IDs - avoid top-level let if not used in all scenarios - prefer expect { ... }.to change { ... }.from(0).to(1) over checking that there are no records at the beginning of the test --- .../projects/import_export/import_file_spec.rb | 53 ++++++++++++---------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index f32834801a0..3015576f6f8 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -3,13 +3,8 @@ require 'spec_helper' feature 'Import/Export - project import integration test', feature: true, js: true do include Select2Helper - let(:admin) { create(:admin) } - let(:normal_user) { create(:user) } - let!(:namespace) { create(:namespace, name: "asd", owner: admin) } let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') } let(:export_path) { "#{Dir::tmpdir}/import_file_spec" } - let(:project) { Project.last } - let(:project_hook) { Gitlab::Git::Hook.new('post-receive', project.repository.path) } background do allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) @@ -19,41 +14,43 @@ feature 'Import/Export - project import integration test', feature: true, js: tr FileUtils.rm_rf(export_path, secure: true) end - context 'admin user' do + context 'when selecting the namespace' do + let(:user) { create(:admin) } + let!(:namespace) { create(:namespace, name: "asd", owner: user) } + before do - login_as(admin) + login_as(user) end scenario 'user imports an exported project successfully' do - expect(Project.all.count).to be_zero - visit new_project_path - select2('2', from: '#project_namespace_id') + select2(namespace.id, from: '#project_namespace_id') fill_in :project_path, with: 'test-project-path', visible: true click_link 'GitLab export' expect(page).to have_content('GitLab project export') - expect(URI.parse(current_url).query).to eq('namespace_id=2&path=test-project-path') + expect(URI.parse(current_url).query).to eq("namespace_id=#{namespace.id}&path=test-project-path") attach_file('file', file) - click_on 'Import project' # import starts + expect { click_on 'Import project' }.to change { Project.count }.from(0).to(1) + project = Project.last expect(project).not_to be_nil expect(project.issues).not_to be_empty expect(project.merge_requests).not_to be_empty - expect(project_hook).to exist - expect(wiki_exists?).to be true + expect(project_hook_exists?(project)).to be true + expect(wiki_exists?(project)).to be true expect(project.import_status).to eq('finished') end scenario 'invalid project' do - project = create(:project, namespace_id: 2) + project = create(:project, namespace: namespace) visit new_project_path - select2('2', from: '#project_namespace_id') + select2(namespace.id, from: '#project_namespace_id') fill_in :project_path, with: project.name, visible: true click_link 'GitLab export' @@ -66,11 +63,11 @@ feature 'Import/Export - project import integration test', feature: true, js: tr end scenario 'project with no name' do - create(:project, namespace_id: 2) + create(:project, namespace: namespace) visit new_project_path - select2('2', from: '#project_namespace_id') + select2(namespace.id, from: '#project_namespace_id') # click on disabled element find(:link, 'GitLab export').trigger('click') @@ -81,24 +78,30 @@ feature 'Import/Export - project import integration test', feature: true, js: tr end end - context 'normal user' do + context 'when limited to the default user namespace' do + let(:user) { create(:user) } before do - login_as(normal_user) + login_as(user) end - scenario 'non-admin user is allowed to import a project' do - expect(Project.all.count).to be_zero - + scenario 'passes correct namespace ID in the URL' do visit new_project_path fill_in :project_path, with: 'test-project-path', visible: true - expect(page).to have_content('GitLab export') + click_link 'GitLab export' + + expect(page).to have_content('GitLab project export') + expect(URI.parse(current_url).query).to eq("namespace_id=#{user.namespace.id}&path=test-project-path") end end - def wiki_exists? + def wiki_exists?(project) wiki = ProjectWiki.new(project) File.exist?(wiki.repository.path_to_repo) && !wiki.repository.empty? end + + def project_hook_exists?(project) + Gitlab::Git::Hook.new('post-receive', project.repository.path).exists? + end end -- cgit v1.2.3 From beddc37478113771785385a645fecf50f743d2ec Mon Sep 17 00:00:00 2001 From: Adam Niedzielski Date: Wed, 19 Oct 2016 20:42:35 +0200 Subject: Fix GitLab project import when a user has access only to their default namespace. Render a hidden field with namespace ID so it can be read by JavaScript and passed to "/import/gitlab_project/new" screen. --- app/views/projects/new.html.haml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index cc8cb134fb8..399ccf15b7f 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -27,6 +27,7 @@ - else .input-group-addon.static-namespace #{root_url}#{current_user.username}/ + = f.hidden_field :namespace_id, value: current_user.namespace_id .form-group.col-xs-12.col-sm-6.project-path = f.label :namespace_id, class: 'label-light' do %span -- cgit v1.2.3 From 1bdda800b6a18237921dd0cdec60c36c0fa0153f Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Thu, 20 Oct 2016 14:15:39 -0200 Subject: [ci skip] Add a comment explaining validate_board_limit callback Callback associations are not common to see around. We want to make clear that the `before_add` callback uses the number before the addition, in this particular case 1. --- app/models/project.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/models/project.rb b/app/models/project.rb index 6685baab699..af117f0acb0 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1339,6 +1339,13 @@ class Project < ActiveRecord::Base shared_projects.any? end + # Similar to the normal callbacks that hook into the life cycle of an + # Active Record object, you can also define callbacks that get triggered + # when you add an object to an association collection. If any of these + # callbacks throw an exception, the object will not be added to the + # collection. Before you add a new board to the boards collection if you + # already have 1, 2, or n it will fail, but it if you have 0 that is lower + # than the number of permitted boards per project it won't fail. def validate_board_limit(board) raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS end -- cgit v1.2.3 From 327ae90c6bcbbc3cc08895ece2f6cc641983a84c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 20 Oct 2016 18:51:03 +0200 Subject: Don't use Hash#slice since it's not supported in Ruby 2.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- lib/api/commit_statuses.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index f282a3b9cd6..f54d4f06627 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -67,9 +67,14 @@ module API pipeline = @project.ensure_pipeline(ref, commit.sha, current_user) status = GenericCommitStatus.running_or_pending.find_or_initialize_by( - project: @project, pipeline: pipeline, - user: current_user, name: name, ref: ref) - status.attributes = declared(params).slice(:target_url, :description) + project: @project, + pipeline: pipeline, + user: current_user, + name: name, + ref: ref, + target_url: params[:target_url], + description: params[:description] + ) begin case params[:state].to_s -- cgit v1.2.3 From 813286e5ea6712039fd868310160eef8acb1c012 Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Fri, 21 Oct 2016 08:39:37 +1100 Subject: Added item to CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73dc323e02c..128f5dec039 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Please view this file on the master branch, on stable branches it's out of date. ## 8.14.0 (2016-11-22) - Adds user project membership expired event to clarify why user was removed (Callum Dryden) - Fix HipChat notifications rendering (airatshigapov, eisnerd) + - Add hover to trash icon in notes !7008 (blackst0ne) - Simpler arguments passed to named_route on toggle_award_url helper method ## 8.13.0 (2016-10-22) -- cgit v1.2.3 From b2592f01da50367f1c6a2acf4ef701d3a7661d36 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 8 Dec 2016 14:39:39 +1100 Subject: Store capybara-screenshots folder as artifacts for RSpec and Spinach --- .gitlab-ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e522d47d19d..475346dcd34 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -69,9 +69,11 @@ stages: - knapsack rspec "--color --format documentation" artifacts: expire_in: 31d + when: always paths: - - knapsack/ - coverage/ + - knapsack/ + - tmp/capybara/ .spinach-knapsack: &spinach-knapsack stage: test @@ -87,9 +89,11 @@ stages: - knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)' artifacts: expire_in: 31d + when: always paths: - - knapsack/ - coverage/ + - knapsack/ + - tmp/capybara/ # Prepare and merge knapsack tests -- cgit v1.2.3 From a61c19778180a8316321ac9ca6f84de76a6c23b3 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 8 Dec 2016 14:45:34 +1100 Subject: Don't disable capybara-screenshot in CI environment --- features/support/capybara.rb | 9 +++------ spec/support/capybara.rb | 9 +++------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/features/support/capybara.rb b/features/support/capybara.rb index 47372df152d..0b6a0981a3c 100644 --- a/features/support/capybara.rb +++ b/features/support/capybara.rb @@ -1,5 +1,6 @@ require 'spinach/capybara' require 'capybara/poltergeist' +require 'capybara-screenshot/spinach' # Give CI some extra time timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 15 @@ -20,12 +21,8 @@ end Capybara.default_max_wait_time = timeout Capybara.ignore_hidden_elements = false -unless ENV['CI'] || ENV['CI_SERVER'] - require 'capybara-screenshot/spinach' - - # Keep only the screenshots generated from the last failing test suite - Capybara::Screenshot.prune_strategy = :keep_last_run -end +# Keep only the screenshots generated from the last failing test suite +Capybara::Screenshot.prune_strategy = :keep_last_run Spinach.hooks.before_run do TestEnv.warm_asset_cache unless ENV['CI'] || ENV['CI_SERVER'] diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 16d5f2bf0b8..ebb1f30f090 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -1,6 +1,7 @@ require 'capybara/rails' require 'capybara/rspec' require 'capybara/poltergeist' +require 'capybara-screenshot/rspec' # Give CI some extra time timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10 @@ -21,12 +22,8 @@ end Capybara.default_max_wait_time = timeout Capybara.ignore_hidden_elements = true -unless ENV['CI'] || ENV['CI_SERVER'] - require 'capybara-screenshot/rspec' - - # Keep only the screenshots generated from the last failing test suite - Capybara::Screenshot.prune_strategy = :keep_last_run -end +# Keep only the screenshots generated from the last failing test suite +Capybara::Screenshot.prune_strategy = :keep_last_run RSpec.configure do |config| config.before(:suite) do -- cgit v1.2.3 From 2e3cbb6965ecfd6e5da43b83cc4e351edfc83fbb Mon Sep 17 00:00:00 2001 From: Balasankar C Date: Mon, 9 Jan 2017 11:19:17 +0530 Subject: Update Registry administration doc - external registry --- doc/administration/container_registry.md | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index d7cfb464f74..90da88d5d49 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -462,6 +462,46 @@ If Registry is enabled in your GitLab instance, but you don't need it for your project, you can disable it from your project's settings. Read the user guide on how to achieve that. +## Disable Container Registry but use GitLab as an auth endpoint + +You can disable the embedded Container Registry to use an external one, but +still use GitLab as an auth endpoint. + +**Omnibus GitLab** +1. Open `/etc/gitlab/gitlab.rb` and set necessary configurations: + + ```ruby + registry['enable'] = false + gitlab_rails['registry_enabled'] = true + gitlab_rails['registry_host'] = "registry.gitlab.example.com" + gitlab_rails['registry_port'] = "5005" + gitlab_rails['registry_api_url'] = "http://localhost:5000" + gitlab_rails['registry_key_path'] = "/var/opt/gitlab/gitlab-rails/certificate.key" + gitlab_rails['registry_path'] = "/var/opt/gitlab/gitlab-rails/shared/registry" + gitlab_rails['registry_issuer'] = "omnibus-gitlab-issuer" + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +**Installations from source** + +1. Open `/home/git/gitlab/config/gitlab.yml`, and edit the configuration settings under `registry`: + + ``` + ## Container Registry + + registry: + enabled: true + host: "registry.gitlab.example.com" + port: "5005" + api_url: "http://localhost:5000" + path: /var/opt/gitlab/gitlab-rails/shared/registry + key: /var/opt/gitlab/gitlab-rails/certificate.key + issuer: omnibus-gitlab-issuer + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. + ## Storage limitations Currently, there is no storage limitation, which means a user can upload an -- cgit v1.2.3 From 7a399b7061d4d374f01ddaa75ae7fba53ca4cb6b Mon Sep 17 00:00:00 2001 From: Matthieu Tardy Date: Mon, 9 Jan 2017 07:38:13 +0100 Subject: Strip reference prefixes on branch creation Signed-off-by: Matthieu Tardy --- ...ranch-names-with-reference-prefixes-results-in-buggy-branches.yml | 4 ++++ lib/gitlab/git_ref_validator.rb | 3 +++ spec/lib/git_ref_validator_spec.rb | 5 +++++ 3 files changed, 12 insertions(+) create mode 100644 changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml diff --git a/changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml b/changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml new file mode 100644 index 00000000000..e82cbf00cfb --- /dev/null +++ b/changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml @@ -0,0 +1,4 @@ +--- +title: Strip reference prefixes on branch creation +merge_request: 8498 +author: Matthieu Tardy diff --git a/lib/gitlab/git_ref_validator.rb b/lib/gitlab/git_ref_validator.rb index 4d83d8e72a8..0e87ee30c98 100644 --- a/lib/gitlab/git_ref_validator.rb +++ b/lib/gitlab/git_ref_validator.rb @@ -5,6 +5,9 @@ module Gitlab # # Returns true for a valid reference name, false otherwise def validate(ref_name) + return false if ref_name.start_with?('refs/heads/') + return false if ref_name.start_with?('refs/remotes/') + Gitlab::Utils.system_silent( %W(#{Gitlab.config.git.bin_path} check-ref-format refs/#{ref_name})) end diff --git a/spec/lib/git_ref_validator_spec.rb b/spec/lib/git_ref_validator_spec.rb index dc57e94f193..cc8daa535d6 100644 --- a/spec/lib/git_ref_validator_spec.rb +++ b/spec/lib/git_ref_validator_spec.rb @@ -5,6 +5,7 @@ describe Gitlab::GitRefValidator, lib: true do it { expect(Gitlab::GitRefValidator.validate('implement_@all')).to be_truthy } it { expect(Gitlab::GitRefValidator.validate('my_new_feature')).to be_truthy } it { expect(Gitlab::GitRefValidator.validate('#1')).to be_truthy } + it { expect(Gitlab::GitRefValidator.validate('feature/refs/heads/foo')).to be_truthy } it { expect(Gitlab::GitRefValidator.validate('feature/~new/')).to be_falsey } it { expect(Gitlab::GitRefValidator.validate('feature/^new/')).to be_falsey } it { expect(Gitlab::GitRefValidator.validate('feature/:new/')).to be_falsey } @@ -17,4 +18,8 @@ describe Gitlab::GitRefValidator, lib: true do it { expect(Gitlab::GitRefValidator.validate('feature\new')).to be_falsey } it { expect(Gitlab::GitRefValidator.validate('feature//new')).to be_falsey } it { expect(Gitlab::GitRefValidator.validate('feature new')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('refs/heads/')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('refs/remotes/')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('refs/heads/feature')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('refs/remotes/origin')).to be_falsey } end -- cgit v1.2.3 From ed0aa3f61c295d4d38ee643a4b3257f890f50686 Mon Sep 17 00:00:00 2001 From: Pedro Moreira da Silva Date: Thu, 26 Jan 2017 17:54:50 +0000 Subject: Format milestone header as issues and MRs * Enclose the identifier in strong * Use the % character as in GFM * Remove spaces around en dash for date range --- app/helpers/milestones_helper.rb | 2 +- app/views/projects/milestones/show.html.haml | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 729928ce1dd..7011e670cee 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -97,7 +97,7 @@ module MilestonesHelper def milestone_date_range(milestone) if milestone.start_date && milestone.due_date - "#{milestone.start_date.to_s(:medium)} - #{milestone.due_date.to_s(:medium)}" + "#{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)}" diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index c3a6096aa54..07592a169b9 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -16,10 +16,9 @@ Open .header-text-content %span.identifier - Milestone ##{@milestone.iid} + %strong + Milestone %#{@milestone.iid} - if @milestone.due_date || @milestone.start_date - %span.creator - · = milestone_date_range(@milestone) .milestone-buttons - if can?(current_user, :admin_milestone, @project) -- cgit v1.2.3 From ae1aee24b226bd182b9b9894b4f80ad3283afbcf Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Thu, 2 Feb 2017 15:04:02 +0100 Subject: Create MM team for GitLab group --- app/models/group.rb | 1 + app/services/groups/create_service.rb | 5 ++++ app/views/groups/new.html.haml | 5 ++++ app/workers/mattermost/create_team_worker.rb | 28 ++++++++++++++++++++++ db/migrate/20170120131253_create_chat_teams.rb | 17 +++++++++++++ db/schema.rb | 11 +++++++++ lib/mattermost/session.rb | 2 +- lib/mattermost/team.rb | 5 ++++ spec/models/chat_team_spec.rb | 10 ++++++++ spec/models/group_spec.rb | 1 + spec/workers/mattermost/create_team_worker_spec.rb | 21 ++++++++++++++++ 11 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 app/workers/mattermost/create_team_worker.rb create mode 100644 db/migrate/20170120131253_create_chat_teams.rb create mode 100644 spec/models/chat_team_spec.rb create mode 100644 spec/workers/mattermost/create_team_worker_spec.rb diff --git a/app/models/group.rb b/app/models/group.rb index 4cdfd022094..76c52481f4f 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -21,6 +21,7 @@ class Group < Namespace has_many :shared_projects, through: :project_group_links, source: :project has_many :notification_settings, dependent: :destroy, as: :source has_many :labels, class_name: 'GroupLabel' + has_one :chat_team, dependent: :destroy validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :visibility_level_allowed_by_projects diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index febeb661fb5..209fd15f481 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -22,6 +22,11 @@ module Groups @group.name ||= @group.path.dup @group.save @group.add_owner(current_user) + + if params[:create_chat_team] && Gitlab.config.mattermost.enabled + Mattermost::CreateTeamWorker.perform_async(@group.id, current_user.id) + end + @group end end diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 38d63fd9acc..8e1513b151b 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -16,6 +16,11 @@ = render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group + .form-group + = f.label :create_chat_team, "Create Mattermost Team", class: 'control-label' + .col-sm-10 + = f.check_box :chat_team + .form-group .col-sm-offset-2.col-sm-10 = render 'shared/group_tips' diff --git a/app/workers/mattermost/create_team_worker.rb b/app/workers/mattermost/create_team_worker.rb new file mode 100644 index 00000000000..69486569cbf --- /dev/null +++ b/app/workers/mattermost/create_team_worker.rb @@ -0,0 +1,28 @@ +module Mattermost + class CreateTeamWorker + include Sidekiq::Worker + include DedicatedSidekiqQueue + + def perform(group_id, current_user_id, options = {}) + @group = Group.find(group_id) + current_user = User.find(current_user_id) + + options = team_params.merge(options) + + # The user that creates the team will be Team Admin + response = Mattermost::Team.new(current_user).create(options) + + ChatTeam.create!(namespace: @group, name: response['name'], team_id: response['id']) + end + + private + + def team_params + { + name: @group.path[0..59], + display_name: @group.name[0..59], + type: @group.public? ? 'O' : 'I' # Open vs Invite-only + } + end + end +end diff --git a/db/migrate/20170120131253_create_chat_teams.rb b/db/migrate/20170120131253_create_chat_teams.rb new file mode 100644 index 00000000000..6476c239152 --- /dev/null +++ b/db/migrate/20170120131253_create_chat_teams.rb @@ -0,0 +1,17 @@ +class CreateChatTeams < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + create_table :chat_teams do |t| + t.integer :namespace_id, index: true + t.string :team_id + t.string :name + + t.timestamps null: false + end + + add_foreign_key :chat_teams, :namespaces, on_delete: :cascade + end +end diff --git a/db/schema.rb b/db/schema.rb index 5efb4f6595c..8850f1fbc7f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -169,6 +169,16 @@ ActiveRecord::Schema.define(version: 20170130204620) do add_index "chat_names", ["service_id", "team_id", "chat_id"], name: "index_chat_names_on_service_id_and_team_id_and_chat_id", unique: true, using: :btree add_index "chat_names", ["user_id", "service_id"], name: "index_chat_names_on_user_id_and_service_id", unique: true, using: :btree + create_table "chat_teams", force: :cascade do |t| + t.integer "namespace_id" + t.string "team_id" + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "chat_teams", ["namespace_id"], name: "index_chat_teams_on_namespace_id", using: :btree + create_table "ci_application_settings", force: :cascade do |t| t.boolean "all_broken_builds" t.boolean "add_pusher" @@ -1307,6 +1317,7 @@ ActiveRecord::Schema.define(version: 20170130204620) do add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree add_foreign_key "boards", "projects" + add_foreign_key "chat_teams", "namespaces", on_delete: :cascade add_foreign_key "issue_metrics", "issues", on_delete: :cascade add_foreign_key "label_priorities", "labels", on_delete: :cascade add_foreign_key "label_priorities", "projects", on_delete: :cascade diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index 377cb7b1021..d4b4ba97f8c 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -153,7 +153,7 @@ module Mattermost yield rescue HTTParty::Error => e raise Mattermost::ConnectionError.new(e.message) - rescue Errno::ECONNREFUSED + rescue Errno::ECONNREFUSED => e raise Mattermost::ConnectionError.new(e.message) end end diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb index 09dfd082b3a..9e80f7f8571 100644 --- a/lib/mattermost/team.rb +++ b/lib/mattermost/team.rb @@ -1,7 +1,12 @@ module Mattermost class Team < Client + # Returns **all** teams for an admin def all session_get('/api/v3/teams/all') end + + def create(params) + session_post('/api/v3/teams/create', body: params.to_json) + end end end diff --git a/spec/models/chat_team_spec.rb b/spec/models/chat_team_spec.rb new file mode 100644 index 00000000000..37a22f5cd6d --- /dev/null +++ b/spec/models/chat_team_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper' + +describe ChatTeam, type: :model do + # Associations + it { is_expected.to belong_to(:group) } + + # Fields + it { is_expected.to respond_to(:name) } + it { is_expected.to respond_to(:team_id) } +end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 9ca50555191..e6acb34a884 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -13,6 +13,7 @@ describe Group, models: true do it { is_expected.to have_many(:shared_projects).through(:project_group_links) } it { is_expected.to have_many(:notification_settings).dependent(:destroy) } it { is_expected.to have_many(:labels).class_name('GroupLabel') } + it { is_expected.to have_one(:chat_team) } describe '#members & #requesters' do let(:requester) { create(:user) } diff --git a/spec/workers/mattermost/create_team_worker_spec.rb b/spec/workers/mattermost/create_team_worker_spec.rb new file mode 100644 index 00000000000..a6b87967ca2 --- /dev/null +++ b/spec/workers/mattermost/create_team_worker_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Mattermost::CreateTeamWorker do + let(:group) { create(:group, path: 'path', name: 'name') } + let(:admin) { create(:admin) } + + describe '.perform' do + subject { described_class.new.perform(group.id, admin.id) } + + before do + allow_any_instance_of(Mattermost::Team). + to receive(:create). + with(name: "path", display_name: "name", type: "O"). + and_return('name' => 'my team', 'id' => 'sjfkdlwkdjfwlkfjwf') + end + + it 'creates a new chat team' do + expect { subject }.to change { ChatTeam.count }.from(0).to(1) + end + end +end -- cgit v1.2.3 From 6c873b11b5d0c582a9613008bc4f28600bbd6579 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Fri, 3 Feb 2017 11:29:56 +0100 Subject: Add tests for Mattermost team creation --- app/controllers/groups_controller.rb | 3 ++- app/models/chat_team.rb | 5 ++++ app/services/groups/create_service.rb | 4 ++- app/workers/mattermost/create_team_worker.rb | 25 ++++++++----------- lib/mattermost/client.rb | 2 +- lib/mattermost/team.rb | 18 ++++++++++++-- spec/models/chat_team_spec.rb | 2 +- spec/services/groups/create_service_spec.rb | 17 ++++++++++--- spec/workers/mattermost/create_team_worker_spec.rb | 29 ++++++++++++++++------ 9 files changed, 73 insertions(+), 32 deletions(-) create mode 100644 app/models/chat_team.rb diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index f81237db991..6f6821f544a 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -138,7 +138,8 @@ class GroupsController < Groups::ApplicationController :public, :request_access_enabled, :share_with_group_lock, - :visibility_level + :visibility_level, + :create_chat_team ] end diff --git a/app/models/chat_team.rb b/app/models/chat_team.rb new file mode 100644 index 00000000000..7952141a0d6 --- /dev/null +++ b/app/models/chat_team.rb @@ -0,0 +1,5 @@ +class ChatTeam < ActiveRecord::Base + validates :team_id, presence: true + + belongs_to :namespace +end diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index 209fd15f481..aabd3c4bd05 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -5,6 +5,8 @@ module Groups end def execute + create_chat_team = params.delete(:create_chat_team) + @group = Group.new(params) unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level]) @@ -23,7 +25,7 @@ module Groups @group.save @group.add_owner(current_user) - if params[:create_chat_team] && Gitlab.config.mattermost.enabled + if create_chat_team && Gitlab.config.mattermost.enabled Mattermost::CreateTeamWorker.perform_async(@group.id, current_user.id) end diff --git a/app/workers/mattermost/create_team_worker.rb b/app/workers/mattermost/create_team_worker.rb index 69486569cbf..168bdc7454d 100644 --- a/app/workers/mattermost/create_team_worker.rb +++ b/app/workers/mattermost/create_team_worker.rb @@ -3,26 +3,21 @@ module Mattermost include Sidekiq::Worker include DedicatedSidekiqQueue + sidekiq_options retry: 5 + + # Add 5 seconds so the first retry isn't 1 second later + sidekiq_retry_in do |count| + 5 + 5 ** n + end + def perform(group_id, current_user_id, options = {}) - @group = Group.find(group_id) + group = Group.find(group_id) current_user = User.find(current_user_id) - options = team_params.merge(options) - # The user that creates the team will be Team Admin - response = Mattermost::Team.new(current_user).create(options) - - ChatTeam.create!(namespace: @group, name: response['name'], team_id: response['id']) - end - - private + response = Mattermost::Team.new(current_user).create(group, options) - def team_params - { - name: @group.path[0..59], - display_name: @group.name[0..59], - type: @group.public? ? 'O' : 'I' # Open vs Invite-only - } + ChatTeam.create(namespace: group, name: response['name'], team_id: response['id']) end end end diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb index e55c0d6ac49..29b10e2afce 100644 --- a/lib/mattermost/client.rb +++ b/lib/mattermost/client.rb @@ -26,7 +26,7 @@ module Mattermost def session_get(path, options = {}) with_session do |session| - get(session, path, options) + get(session, path, options) end end diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb index 9e80f7f8571..d5648f7167f 100644 --- a/lib/mattermost/team.rb +++ b/lib/mattermost/team.rb @@ -5,8 +5,22 @@ module Mattermost session_get('/api/v3/teams/all') end - def create(params) - session_post('/api/v3/teams/create', body: params.to_json) + # Creates a team on the linked Mattermost instance, the team admin will be the + # `current_user` passed to the Mattermost::Client instance + def create(group, params) + session_post('/api/v3/teams/create', body: new_team_params(group, params).to_json) + end + + private + + MATTERMOST_TEAM_LENGTH_MAX = 59 + + def new_team_params(group, options) + { + name: group.path[0..MATTERMOST_TEAM_LENGTH_MAX], + display_name: group.name[0..MATTERMOST_TEAM_LENGTH_MAX], + type: group.public? ? 'O' : 'I' # Open vs Invite-only + }.merge(options) end end end diff --git a/spec/models/chat_team_spec.rb b/spec/models/chat_team_spec.rb index 37a22f5cd6d..1aab161ec13 100644 --- a/spec/models/chat_team_spec.rb +++ b/spec/models/chat_team_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe ChatTeam, type: :model do # Associations - it { is_expected.to belong_to(:group) } + it { is_expected.to belong_to(:namespace) } # Fields it { is_expected.to respond_to(:name) } diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index 14717a7455d..235fa5c5188 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -4,11 +4,11 @@ describe Groups::CreateService, '#execute', services: true do let!(:user) { create(:user) } let!(:group_params) { { path: "group_path", visibility_level: Gitlab::VisibilityLevel::PUBLIC } } + subject { service.execute } + describe 'visibility level restrictions' do let!(:service) { described_class.new(user, group_params) } - subject { service.execute } - context "create groups without restricted visibility level" do it { is_expected.to be_persisted } end @@ -24,8 +24,6 @@ describe Groups::CreateService, '#execute', services: true do let!(:group) { create(:group) } let!(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) } - subject { service.execute } - context 'as group owner' do before { group.add_owner(user) } @@ -40,4 +38,15 @@ describe Groups::CreateService, '#execute', services: true do end end end + + describe 'creating a mattermost team' do + let!(:params) { group_params.merge(create_chat_team: true) } + let!(:service) { described_class.new(user, params) } + + it 'queues a background job' do + expect(Mattermost::CreateTeamWorker).to receive(:perform_async) + + subject + end + end end diff --git a/spec/workers/mattermost/create_team_worker_spec.rb b/spec/workers/mattermost/create_team_worker_spec.rb index a6b87967ca2..d3230f535c2 100644 --- a/spec/workers/mattermost/create_team_worker_spec.rb +++ b/spec/workers/mattermost/create_team_worker_spec.rb @@ -7,15 +7,30 @@ describe Mattermost::CreateTeamWorker do describe '.perform' do subject { described_class.new.perform(group.id, admin.id) } - before do - allow_any_instance_of(Mattermost::Team). - to receive(:create). - with(name: "path", display_name: "name", type: "O"). - and_return('name' => 'my team', 'id' => 'sjfkdlwkdjfwlkfjwf') + context 'succesfull request to mattermost' do + before do + allow_any_instance_of(Mattermost::Team). + to receive(:create). + with(group, {}). + and_return('name' => 'my team', 'id' => 'sjfkdlwkdjfwlkfjwf') + end + + it 'creates a new chat team' do + expect { subject }.to change { ChatTeam.count }.from(0).to(1) + end end - it 'creates a new chat team' do - expect { subject }.to change { ChatTeam.count }.from(0).to(1) + context 'connection trouble' do + before do + allow_any_instance_of(Mattermost::Team). + to receive(:create). + with(group, {}). + and_raise(Mattermost::ClientError.new('Undefined error')) + end + + it 'does not rescue the error' do + expect { subject }.to raise_error(Mattermost::ClientError) + end end end end -- cgit v1.2.3 From 67620153ab64fccb6602446d3a0548b5a19b86b9 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 6 Feb 2017 16:10:32 +0000 Subject: Adds Mattermost label and icon to new group form --- app/assets/stylesheets/pages/groups.scss | 6 ++++++ app/views/groups/new.html.haml | 10 ++++++++-- app/views/shared/icons/_icon_mattermost.svg | 1 + 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 app/views/shared/icons/_icon_mattermost.svg diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index d377526e655..7db6af3531a 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -73,3 +73,9 @@ } } } + +.mattermost-icon svg{ + width: 16px; + height: 16px; + vertical-align: text-bottom; +} diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 8e1513b151b..29b5d2ab032 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -17,9 +17,15 @@ = render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group .form-group - = f.label :create_chat_team, "Create Mattermost Team", class: 'control-label' + = f.label :create_chat_team, class: 'control-label' do + %span.mattermost-icon + = custom_icon('icon_mattermost') + Mattermost .col-sm-10 - = f.check_box :chat_team + .checkbox + = f.label :chat_team do + = f.check_box :chat_team + Link the group to a new or existing Mattermost team .form-group .col-sm-offset-2.col-sm-10 diff --git a/app/views/shared/icons/_icon_mattermost.svg b/app/views/shared/icons/_icon_mattermost.svg new file mode 100644 index 00000000000..59f4b598712 --- /dev/null +++ b/app/views/shared/icons/_icon_mattermost.svg @@ -0,0 +1 @@ + \ No newline at end of file -- cgit v1.2.3 From 85b19d699bfa48792b313dfe12386943b18e9050 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 6 Feb 2017 17:23:06 +0000 Subject: Adds html and css to add mattermost team name to group in new and edit --- app/assets/stylesheets/pages/groups.scss | 12 +++++++++++- app/views/groups/edit.html.haml | 20 ++++++++++++++++++++ app/views/groups/new.html.haml | 6 ++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 7db6af3531a..84d21e48463 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -74,8 +74,18 @@ } } -.mattermost-icon svg{ +.mattermost-icon svg { width: 16px; height: 16px; vertical-align: text-bottom; } + +.mattermost-team-name { + color: $gl-text-color-secondary; +} + +.mattermost-info { + display: block; + color: $gl-text-color-secondary; + margin-top: 10px; +} diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 2706e8692d1..38d498f0343 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -22,6 +22,26 @@ = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group + .form-group + = f.label :name, class: 'control-label' do + %span.mattermost-icon + = custom_icon('icon_mattermost') + Mattermost + .col-sm-10 + .checkbox + = f.label :name do + = f.check_box :name, checked: true + Link the group to a new or existing Mattermost team + + + - enabled = Gitlab.config.mattermost.enabled + - if enabled + .form-group + .col-sm-offset-2.col-sm-10 + = f.text_field :name, placeholder: "FILL WITH TEAM NAME", class: 'form-control mattermost-team-name' + %span.mattermost-info + Team URL: INSERT TEAM URL + .form-group .col-sm-offset-2.col-sm-10 = render 'shared/allow_request_access', form: f diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 29b5d2ab032..3cc4f6d6e48 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -27,6 +27,12 @@ = f.check_box :chat_team Link the group to a new or existing Mattermost team +- enabled = Gitlab.config.mattermost.enabled +- if enabled + .form-group + .col-sm-offset-2.col-sm-10 + = f.text_field :name, placeholder: 'Mattermost team name', class: 'form-control mattermost-team-name' + .form-group .col-sm-offset-2.col-sm-10 = render 'shared/group_tips' -- cgit v1.2.3 From 571f34121141df5b934cdcc9b369db4c72dbad95 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 6 Feb 2017 17:30:05 +0000 Subject: Add newline at end of icon file --- app/views/groups/new.html.haml | 2 +- app/views/shared/icons/_icon_mattermost.svg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 3cc4f6d6e48..723071c7633 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -31,7 +31,7 @@ - if enabled .form-group .col-sm-offset-2.col-sm-10 - = f.text_field :name, placeholder: 'Mattermost team name', class: 'form-control mattermost-team-name' + = f.text_field :name, placeholder: 'Mattermost team name', class: 'form-control mattermost-team-name' .form-group .col-sm-offset-2.col-sm-10 diff --git a/app/views/shared/icons/_icon_mattermost.svg b/app/views/shared/icons/_icon_mattermost.svg index 59f4b598712..d1c541523ab 100644 --- a/app/views/shared/icons/_icon_mattermost.svg +++ b/app/views/shared/icons/_icon_mattermost.svg @@ -1 +1 @@ - \ No newline at end of file + -- cgit v1.2.3 From 34cc2a8eca59442f94a27c15459a8c230398b6cd Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Tue, 7 Feb 2017 08:24:57 +0100 Subject: Improve DRYness of views --- app/controllers/groups_controller.rb | 5 ++++- app/services/groups/create_service.rb | 4 +++- app/services/groups/update_service.rb | 5 +++++ app/views/groups/_create_chat_team.html.haml | 18 ++++++++++++++++++ app/views/groups/edit.html.haml | 20 +------------------- app/views/groups/new.html.haml | 17 +---------------- app/workers/mattermost/create_team_worker.rb | 9 +++++---- db/migrate/20170120131253_create_chat_teams.rb | 4 ++-- db/schema.rb | 12 ++++++++++++ 9 files changed, 51 insertions(+), 43 deletions(-) create mode 100644 app/views/groups/_create_chat_team.html.haml diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 0593dd01506..63efecd8f68 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -27,6 +27,7 @@ class GroupsController < Groups::ApplicationController end def create + byebug @group = Groups::CreateService.new(current_user, group_params).execute if @group.persisted? @@ -81,6 +82,7 @@ class GroupsController < Groups::ApplicationController end def update + byebug if Groups::UpdateService.new(@group, current_user, group_params).execute redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated." else @@ -139,7 +141,8 @@ class GroupsController < Groups::ApplicationController :request_access_enabled, :share_with_group_lock, :visibility_level, - :create_chat_team + :create_chat_team, + :chat_team_name ] end diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index aabd3c4bd05..13d1b545498 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -6,6 +6,7 @@ module Groups def execute create_chat_team = params.delete(:create_chat_team) + team_name = params.delete(:chat_team_name) @group = Group.new(params) @@ -26,7 +27,8 @@ module Groups @group.add_owner(current_user) if create_chat_team && Gitlab.config.mattermost.enabled - Mattermost::CreateTeamWorker.perform_async(@group.id, current_user.id) + options = team_name ? { name: team_name } : {} + Mattermost::CreateTeamWorker.perform_async(@group.id, current_user.id, options) end @group diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb index 4e878ec556a..aff42ad598c 100644 --- a/app/services/groups/update_service.rb +++ b/app/services/groups/update_service.rb @@ -1,6 +1,11 @@ module Groups class UpdateService < Groups::BaseService def execute + if params.delete(:create_chat_team) == '1' + chat_name = params[:chat_team_name] + options = chat_name ? { name: chat_name } : {} + end + # check that user is allowed to set specified visibility_level new_visibility = params[:visibility_level] if new_visibility && new_visibility.to_i != group.visibility_level diff --git a/app/views/groups/_create_chat_team.html.haml b/app/views/groups/_create_chat_team.html.haml new file mode 100644 index 00000000000..b2b5a2090f9 --- /dev/null +++ b/app/views/groups/_create_chat_team.html.haml @@ -0,0 +1,18 @@ +.form-group + = f.label :name, class: 'control-label' do + %span.mattermost-icon + = custom_icon('icon_mattermost') + Mattermost + .col-sm-10 + .checkbox + = f.label :name do + = f.check_box :create_chat_team, checked: true + Link the group to a new Mattermost team + +.form-group + = f.label :chat_team, class: 'control-label' do + Chat Team name + .col-sm-10 + = f.text_field :chat_team, placeholder: @group.name, class: 'form-control mattermost-team-name' + Leave blank to match your group name + diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 38d498f0343..c904b25fe94 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -22,25 +22,7 @@ = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group - .form-group - = f.label :name, class: 'control-label' do - %span.mattermost-icon - = custom_icon('icon_mattermost') - Mattermost - .col-sm-10 - .checkbox - = f.label :name do - = f.check_box :name, checked: true - Link the group to a new or existing Mattermost team - - - - enabled = Gitlab.config.mattermost.enabled - - if enabled - .form-group - .col-sm-offset-2.col-sm-10 - = f.text_field :name, placeholder: "FILL WITH TEAM NAME", class: 'form-control mattermost-team-name' - %span.mattermost-info - Team URL: INSERT TEAM URL + = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled .form-group .col-sm-offset-2.col-sm-10 diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 723071c7633..000c7af2326 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -16,22 +16,7 @@ = render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group - .form-group - = f.label :create_chat_team, class: 'control-label' do - %span.mattermost-icon - = custom_icon('icon_mattermost') - Mattermost - .col-sm-10 - .checkbox - = f.label :chat_team do - = f.check_box :chat_team - Link the group to a new or existing Mattermost team - -- enabled = Gitlab.config.mattermost.enabled -- if enabled - .form-group - .col-sm-offset-2.col-sm-10 - = f.text_field :name, placeholder: 'Mattermost team name', class: 'form-control mattermost-team-name' + = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled .form-group .col-sm-offset-2.col-sm-10 diff --git a/app/workers/mattermost/create_team_worker.rb b/app/workers/mattermost/create_team_worker.rb index 168bdc7454d..95b63dac8ab 100644 --- a/app/workers/mattermost/create_team_worker.rb +++ b/app/workers/mattermost/create_team_worker.rb @@ -7,17 +7,18 @@ module Mattermost # Add 5 seconds so the first retry isn't 1 second later sidekiq_retry_in do |count| - 5 + 5 ** n + 5 + 5**n end def perform(group_id, current_user_id, options = {}) - group = Group.find(group_id) - current_user = User.find(current_user_id) + group = Group.find_by(id: group_id) + current_user = User.find_by(id: current_user_id) + return unless group && current_user # The user that creates the team will be Team Admin response = Mattermost::Team.new(current_user).create(group, options) - ChatTeam.create(namespace: group, name: response['name'], team_id: response['id']) + group.create_chat_team(name: response['name'], team_id: response['id']) end end end diff --git a/db/migrate/20170120131253_create_chat_teams.rb b/db/migrate/20170120131253_create_chat_teams.rb index 6476c239152..8f76b43960b 100644 --- a/db/migrate/20170120131253_create_chat_teams.rb +++ b/db/migrate/20170120131253_create_chat_teams.rb @@ -5,13 +5,13 @@ class CreateChatTeams < ActiveRecord::Migration def change create_table :chat_teams do |t| - t.integer :namespace_id, index: true + t.integer :group_id, index: true t.string :team_id t.string :name t.timestamps null: false end - add_foreign_key :chat_teams, :namespaces, on_delete: :cascade + add_foreign_key :chat_teams, :groups, on_delete: :cascade end end diff --git a/db/schema.rb b/db/schema.rb index 5bca4171ebe..f30ab976062 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -109,6 +109,7 @@ ActiveRecord::Schema.define(version: 20170204181513) do t.boolean "html_emails_enabled", default: true t.string "plantuml_url" t.boolean "plantuml_enabled" + t.integer "max_pages_size", default: 100, null: false t.integer "terminal_max_session_time", default: 0, null: false end @@ -867,6 +868,17 @@ ActiveRecord::Schema.define(version: 20170204181513) do add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree + create_table "pages_domains", force: :cascade do |t| + t.integer "project_id" + t.text "certificate" + t.text "encrypted_key" + t.string "encrypted_key_iv" + t.string "encrypted_key_salt" + t.string "domain" + end + + add_index "pages_domains", ["domain"], name: "index_pages_domains_on_domain", unique: true, using: :btree + create_table "personal_access_tokens", force: :cascade do |t| t.integer "user_id", null: false t.string "token", null: false -- cgit v1.2.3 From 38f5bbf08834e297ffa101714a1e537e1889c37e Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 7 Feb 2017 16:13:52 +0000 Subject: Fix form fields to bing to correct model --- app/views/groups/_create_chat_team.html.haml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/views/groups/_create_chat_team.html.haml b/app/views/groups/_create_chat_team.html.haml index b2b5a2090f9..c06a7a23346 100644 --- a/app/views/groups/_create_chat_team.html.haml +++ b/app/views/groups/_create_chat_team.html.haml @@ -1,18 +1,18 @@ .form-group - = f.label :name, class: 'control-label' do + = f.label :create_chat_team, class: 'control-label' do %span.mattermost-icon = custom_icon('icon_mattermost') Mattermost .col-sm-10 .checkbox - = f.label :name do - = f.check_box :create_chat_team, checked: true + = f.label :create_chat_team do + = f.check_box :create_chat_team, checked: @group.chat_team Link the group to a new Mattermost team .form-group = f.label :chat_team, class: 'control-label' do Chat Team name .col-sm-10 - = f.text_field :chat_team, placeholder: @group.name, class: 'form-control mattermost-team-name' - Leave blank to match your group name - + = f.text_field :chat_team, placeholder: @group.chat_team, class: 'form-control mattermost-team-name' + %small + Leave blank to match your group name -- cgit v1.2.3 From 0d53ce9eb8b245b5382246df12e1b38a843e4f1d Mon Sep 17 00:00:00 2001 From: Reb Date: Wed, 8 Feb 2017 04:00:20 +0000 Subject: Fix typo in auth0.md doc Removed spurious character from Omnibus example --- doc/integration/auth0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/integration/auth0.md b/doc/integration/auth0.md index e5247082a89..b10f7054482 100644 --- a/doc/integration/auth0.md +++ b/doc/integration/auth0.md @@ -54,7 +54,7 @@ for initial settings. gitlab_rails['omniauth_providers'] = [ { "name" => "auth0", - "args" => { client_id: 'YOUR_AUTH0_CLIENT_ID'', + "args" => { client_id: 'YOUR_AUTH0_CLIENT_ID', client_secret: 'YOUR_AUTH0_CLIENT_SECRET', namespace: 'YOUR_AUTH0_DOMAIN' } -- cgit v1.2.3 From 2d2db6eec8f59316eff51240a74ebce017e06e92 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 8 Feb 2017 10:40:15 +0000 Subject: Sends checkbox parameters as boolean --- app/views/groups/_create_chat_team.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/groups/_create_chat_team.html.haml b/app/views/groups/_create_chat_team.html.haml index c06a7a23346..1e702c4f20f 100644 --- a/app/views/groups/_create_chat_team.html.haml +++ b/app/views/groups/_create_chat_team.html.haml @@ -6,7 +6,7 @@ .col-sm-10 .checkbox = f.label :create_chat_team do - = f.check_box :create_chat_team, checked: @group.chat_team + = f.check_box(:create_chat_team, { checked: @group.chat_team }, 'true', 'false') Link the group to a new Mattermost team .form-group -- cgit v1.2.3 From cd5bee0d4438e4bb5158b721580cec8687e95e93 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 3 Jan 2017 16:31:43 +0000 Subject: Removed jQuery UI highlight & autocomplete In an effort to tackle #18437 this removes 2 of the jQuery UI plugins. Highlight & autocomplete, both used once in our code. Highlight was just removed easily, autocomplete was replaced with GL dropdown --- app/assets/javascripts/application.js | 2 -- app/assets/javascripts/milestone.js | 1 - app/assets/javascripts/new_branch_form.js | 30 ++++++++++++++++---- .../protected_branch_edit.js.es6 | 9 ++++-- app/assets/stylesheets/application.scss | 1 - app/assets/stylesheets/framework/jquery.scss | 11 -------- .../personal_access_tokens/index.html.haml | 2 -- app/views/projects/branches/new.html.haml | 8 ++++-- app/views/projects/pipelines/new.html.haml | 6 +++- changelogs/unreleased/remove-jquery-ui-plugins.yml | 4 +++ features/project/commits/branches.feature | 8 ++---- features/steps/project/commits/branches.rb | 24 ++++++++-------- spec/features/projects/pipelines/pipelines_spec.rb | 33 +++++++++++----------- 13 files changed, 76 insertions(+), 63 deletions(-) create mode 100644 changelogs/unreleased/remove-jquery-ui-plugins.yml diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 4ecbf195b64..6f04e8fad4e 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -9,9 +9,7 @@ function requireAll(context) { return context.keys().map(context); } window.$ = window.jQuery = require('jquery'); -require('jquery-ui/ui/autocomplete'); require('jquery-ui/ui/draggable'); -require('jquery-ui/ui/effect-highlight'); require('jquery-ui/ui/sortable'); require('jquery-ujs'); require('vendor/jquery.endless-scroll'); diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index 051cb9fe5c5..35b03c91c98 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -78,7 +78,6 @@ } else { $(element).find('.assignee-icon').empty(); } - return $(element).effect('highlight'); }; function Milestone() { diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js index 7f763c13b50..7c508446859 100644 --- a/app/assets/javascripts/new_branch_form.js +++ b/app/assets/javascripts/new_branch_form.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len, object-shorthand */ (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }, indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; }; @@ -20,15 +20,35 @@ }; NewBranchForm.prototype.init = function() { - if (this.name.val().length > 0) { + if (this.name.length && this.name.val().length > 0) { return this.name.trigger('blur'); } }; NewBranchForm.prototype.setupAvailableRefs = function(availableRefs) { - return this.ref.autocomplete({ - source: availableRefs, - minLength: 1 + var $branchSelect = $('.js-branch-select'); + + $branchSelect.glDropdown({ + data: availableRefs, + filterable: true, + filterByText: true, + remote: false, + fieldName: $branchSelect.data('field-name'), + selectable: true, + isSelectable: function(branch, $el) { + return !$el.hasClass('is-active'); + }, + text: function(branch) { + return branch; + }, + id: function(branch) { + return branch; + }, + toggleLabel: function(branch) { + if (branch) { + return branch; + } + } }); }; 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 149e511451e..6ef59e94384 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 @@ -36,6 +36,9 @@ // Do not update if one dropdown has not selected any option if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return; + this.$allowedToMergeDropdown.disable(); + this.$allowedToPushDropdown.disable(); + $.ajax({ type: 'POST', url: this.$wrap.data('url'), @@ -53,13 +56,13 @@ }] } }, - success: () => { - this.$wrap.effect('highlight'); - }, error() { $.scrollTo(0); new Flash('Failed to update branch!'); } + }).always(() => { + this.$allowedToMergeDropdown.enable(); + this.$allowedToPushDropdown.enable(); }); } }; diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 1dcd1f8a6fc..83a8eeaafde 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -2,7 +2,6 @@ * This is a manifest file that'll automatically include all the stylesheets available in this directory * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at * the top of the compiled file, but it's generally better to create a new file per style scope. - *= require jquery-ui/autocomplete *= require jquery.atwho *= require select2 *= require_self diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss index d335fedefe2..300ba4f2de6 100644 --- a/app/assets/stylesheets/framework/jquery.scss +++ b/app/assets/stylesheets/framework/jquery.scss @@ -2,17 +2,6 @@ font-family: $regular_font; font-size: $font-size-base; - &.ui-autocomplete { - border-color: $jq-ui-border; - padding: 0; - margin-top: 2px; - z-index: 1001; - - .ui-menu-item a { - padding: 4px 10px; - } - } - .ui-state-default { border: 1px solid $white-light; background: $white-light; diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index 2c006e1712d..f76459761d6 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -101,5 +101,3 @@ $("#created-personal-access-token").click(function() { this.select(); }); - - $("#created-personal-access-token").effect('highlight', { color: '#ffff99' }, 2000); diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index e63bdb38bd8..d3c3e40d518 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -12,12 +12,16 @@ .form-group = label_tag :branch_name, nil, class: 'control-label' .col-sm-10 - = text_field_tag :branch_name, params[:branch_name], required: true, tabindex: 1, autofocus: true, class: 'form-control js-branch-name' + = text_field_tag :branch_name, params[:branch_name], required: true, autofocus: true, class: 'form-control js-branch-name' .help-block.text-danger.js-branch-name-error .form-group = label_tag :ref, 'Create from', class: 'control-label' .col-sm-10 - = text_field_tag :ref, params[:ref] || @project.default_branch, required: true, tabindex: 2, class: 'form-control' + = hidden_field_tag :ref, params[:ref] || @project.default_branch + = dropdown_tag(params[:ref] || @project.default_branch, + options: { toggle_class: 'js-branch-select wide', + filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search branches", + data: { selected: params[:ref] || @project.default_branch, field_name: 'ref' } }) .help-block Existing branch name, tag, or commit SHA .form-actions = button_tag 'Create branch', class: 'btn btn-create', tabindex: 3 diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml index 55202725b9e..14a270a3039 100644 --- a/app/views/projects/pipelines/new.html.haml +++ b/app/views/projects/pipelines/new.html.haml @@ -9,7 +9,11 @@ .form-group = f.label :ref, 'Create for', class: 'control-label' .col-sm-10 - = f.text_field :ref, required: true, tabindex: 2, class: 'form-control js-branch-name ui-autocomplete-input', autocomplete: :false, id: :ref + = hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch + = dropdown_tag(params[:ref] || @project.default_branch, + options: { toggle_class: 'js-branch-select wide', + filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search branches", + data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } }) .help-block Existing branch name, tag .form-actions = f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3 diff --git a/changelogs/unreleased/remove-jquery-ui-plugins.yml b/changelogs/unreleased/remove-jquery-ui-plugins.yml new file mode 100644 index 00000000000..c768f702ba2 --- /dev/null +++ b/changelogs/unreleased/remove-jquery-ui-plugins.yml @@ -0,0 +1,4 @@ +--- +title: Removed jQuery UI highlight & autocomplete +merge_request: +author: diff --git a/features/project/commits/branches.feature b/features/project/commits/branches.feature index 88fef674c0c..c57376aecff 100644 --- a/features/project/commits/branches.feature +++ b/features/project/commits/branches.feature @@ -13,6 +13,7 @@ Feature: Project Commits Branches Given I visit project protected branches page Then I should see "Shop" protected branches list + @javascript Scenario: I create a branch Given I visit project branches page And I click new branch link @@ -33,12 +34,7 @@ Feature: Project Commits Branches And I submit new branch form with invalid name Then I should see new an error that branch is invalid - Scenario: I create a branch with invalid reference - Given I visit project branches page - And I click new branch link - And I submit new branch form with invalid reference - Then I should see new an error that ref is invalid - + @javascript Scenario: I create a branch that already exists Given I visit project branches page And I click new branch link diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb index 5f9b9e0445e..ccaf3237815 100644 --- a/features/steps/project/commits/branches.rb +++ b/features/steps/project/commits/branches.rb @@ -34,25 +34,19 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps step 'I submit new branch form' do fill_in 'branch_name', with: 'deploy_keys' - fill_in 'ref', with: 'master' + select_branch('master') click_button 'Create branch' end step 'I submit new branch form with invalid name' do fill_in 'branch_name', with: '1.0 stable' - fill_in 'ref', with: 'master' - click_button 'Create branch' - end - - step 'I submit new branch form with invalid reference' do - fill_in 'branch_name', with: 'foo' - fill_in 'ref', with: 'foo' + select_branch('master') click_button 'Create branch' end step 'I submit new branch form with branch that already exists' do fill_in 'branch_name', with: 'master' - fill_in 'ref', with: 'master' + select_branch('master') click_button 'Create branch' end @@ -65,10 +59,6 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps expect(page).to have_content "can't contain spaces" end - step 'I should see new an error that ref is invalid' do - expect(page).to have_content 'Invalid reference name' - end - step 'I should see new an error that branch already exists' do expect(page).to have_content 'Branch already exists' end @@ -88,4 +78,12 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps step "I should not see branch 'improve/awesome'" do expect(page.all(visible: true)).not_to have_content 'improve/awesome' end + + def select_branch(branch_name) + click_button 'master' + + page.within '#new-branch-form .dropdown-menu' do + click_link branch_name + end + end end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 6555b2fc6c1..f0282d8db68 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -286,8 +286,14 @@ describe 'Pipelines', :feature, :js do visit new_namespace_project_pipeline_path(project.namespace, project) end - context 'for valid commit' do - before { fill_in('pipeline[ref]', with: 'master') } + context 'for valid commit', js: true do + before do + click_button project.default_branch + + page.within '.dropdown-menu' do + click_link 'master' + end + end context 'with gitlab-ci.yml' do before { stub_ci_pipeline_to_return_yaml_file } @@ -304,15 +310,6 @@ describe 'Pipelines', :feature, :js do 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' do @@ -324,18 +321,22 @@ describe 'Pipelines', :feature, :js do 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_selector('.js-branch-select') + expect(find('.js-branch-select')).to have_content project.default_branch 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) + click_button project.default_branch - within('.ui-autocomplete') do - expect(page).to have_selector('li', text: 'fix') + page.within '.dropdown-menu' do + find('.dropdown-input-field').native.send_keys('fix') + + page.within '.dropdown-content' do + expect(page).to have_content('fix') + end end end end -- cgit v1.2.3 From b161b845a64c0c65deeb8f3d90ba24e599abbad8 Mon Sep 17 00:00:00 2001 From: Pedro Moreira da Silva Date: Fri, 10 Feb 2017 15:26:28 +0000 Subject: Add clearer placeholders and channel definition. --- app/models/project_services/mattermost_service.rb | 14 +++++++------- app/models/project_services/slack_service.rb | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb index 4ebc5318da1..c13538e9fea 100644 --- a/app/models/project_services/mattermost_service.rb +++ b/app/models/project_services/mattermost_service.rb @@ -15,10 +15,10 @@ class MattermostService < ChatNotificationService 'This service sends notifications about projects events to Mattermost channels.
To set up this service:
    -
  1. Enable incoming webhooks in your Mattermost installation.
  2. -
  3. Add an incoming webhook in your Mattermost team. The default channel can be overridden for each event.
  4. -
  5. Paste the webhook URL into the field bellow.
  6. -
  7. Select events below to enable notifications. The channel and username are optional.
  8. +
  9. Enable incoming webhooks in your Mattermost installation.
  10. +
  11. Add an incoming webhook in your Mattermost team. The default channel can be overridden for each event.
  12. +
  13. Paste the webhook URL into the field below.
  14. +
  15. Select events below to enable notifications. The Channel handle and Username fields are optional.
' end @@ -28,14 +28,14 @@ class MattermostService < ChatNotificationService def default_fields [ - { type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' }, - { type: 'text', name: 'username', placeholder: 'username' }, + { type: 'text', name: 'webhook', placeholder: 'e.g. http://mattermost_host/hooks/…' }, + { type: 'text', name: 'username', placeholder: 'e.g. GitLab' }, { type: 'checkbox', name: 'notify_only_broken_builds' }, { type: 'checkbox', name: 'notify_only_broken_pipelines' }, ] end def default_channel_placeholder - "town-square" + "Channel handle (e.g. town-square)" end end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index f77d2d7c60b..da7496573ef 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -13,11 +13,11 @@ class SlackService < ChatNotificationService def help 'This service sends notifications about projects events to Slack channels.
- To setup this service: + To set up this service:
    -
  1. Add an incoming webhook in your Slack team. The default channel can be overridden for each event.
  2. -
  3. Paste the Webhook URL into the field below.
  4. -
  5. Select events below to enable notifications. The channel and username are optional.
  6. +
  7. Add an incoming webhook in your Slack team. The default channel can be overridden for each event.
  8. +
  9. Paste the Webhook URL into the field below.
  10. +
  11. Select events below to enable notifications. The Channel name and Username fields are optional.
' end @@ -27,14 +27,14 @@ class SlackService < ChatNotificationService def default_fields [ - { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' }, - { type: 'text', name: 'username', placeholder: 'username' }, + { type: 'text', name: 'webhook', placeholder: 'e.g. https://hooks.slack.com/services/…' }, + { type: 'text', name: 'username', placeholder: 'e.g. GitLab' }, { type: 'checkbox', name: 'notify_only_broken_builds' }, { type: 'checkbox', name: 'notify_only_broken_pipelines' }, ] end def default_channel_placeholder - "#general" + "Channel name (e.g. general)" end end -- cgit v1.2.3 From e7e2562e4f82b10301e899ebb0e355df6beac0e6 Mon Sep 17 00:00:00 2001 From: Jared Deckard Date: Mon, 6 Feb 2017 16:25:30 -0600 Subject: Position task list checkbox to match the list indent --- app/assets/stylesheets/framework/lists.scss | 10 --------- app/assets/stylesheets/framework/mixins.scss | 7 ++++++ app/assets/stylesheets/framework/typography.scss | 25 +++++++++++++++++++++- .../unreleased/22466-task-list-alignment.yml | 4 ++++ 4 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 changelogs/unreleased/22466-task-list-alignment.yml diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 2bfdb9f9601..55ed4b7b06c 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -96,16 +96,6 @@ ul.unstyled-list > li { border-bottom: none; } -ul.task-list { - li.task-list-item { - list-style-type: none; - } - - ul:not(.task-list) { - padding-left: 1.3em; - } -} - // Generic content list ul.content-list { @include basic-list; diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 1acd06122a3..df78bbdea51 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -76,6 +76,13 @@ #{$property}: $value; } +/* http://phrappe.com/css/conditional-css-for-webkit-based-browsers/ */ +@mixin on-webkit-only { + @media screen and (-webkit-min-device-pixel-ratio:0) { + @content; + } +} + @mixin keyframes($animation-name) { @-webkit-keyframes #{$animation-name} { @content; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 54958973f15..db5e2c51fe7 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -134,7 +134,7 @@ ul, ol { padding: 0; - margin: 3px 0 3px 28px !important; + margin: 3px 0 !important; } ul:dir(rtl), @@ -144,6 +144,29 @@ li { line-height: 1.6em; + margin-left: 25px; + padding-left: 3px; + + /* Normalize the bullet position on webkit. */ + @include on-webkit-only { + margin-left: 28px; + padding-left: 0; + } + } + + ul.task-list { + li.task-list-item { + list-style-type: none; + position: relative; + padding-left: 28px; + margin-left: 0 !important; + + input.task-list-item-checkbox { + position: absolute; + left: 8px; + top: 5px; + } + } } a[href*="/uploads/"], diff --git a/changelogs/unreleased/22466-task-list-alignment.yml b/changelogs/unreleased/22466-task-list-alignment.yml new file mode 100644 index 00000000000..6e6ccb873ec --- /dev/null +++ b/changelogs/unreleased/22466-task-list-alignment.yml @@ -0,0 +1,4 @@ +--- +title: Align task list checkboxes +merge_request: 6487 +author: Jared Deckard -- cgit v1.2.3 From 68bad19c74a0b07280bdcfdb5491eba0983bcf60 Mon Sep 17 00:00:00 2001 From: Pedro Moreira da Silva Date: Fri, 10 Feb 2017 15:49:52 +0000 Subject: Update docs on Mattermost and Slack notifications channels --- .../integrations/img/mattermost_configuration.png | Bin 73502 -> 249592 bytes .../integrations/img/slack_configuration.png | Bin 29825 -> 229050 bytes doc/user/project/integrations/mattermost.md | 11 ++++++----- doc/user/project/integrations/slack.md | 10 ++++++---- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/doc/user/project/integrations/img/mattermost_configuration.png b/doc/user/project/integrations/img/mattermost_configuration.png index 3c5ff5ee317..f52acf4ef3b 100644 Binary files a/doc/user/project/integrations/img/mattermost_configuration.png and b/doc/user/project/integrations/img/mattermost_configuration.png differ diff --git a/doc/user/project/integrations/img/slack_configuration.png b/doc/user/project/integrations/img/slack_configuration.png index fc8e58e686b..527824fc3eb 100644 Binary files a/doc/user/project/integrations/img/slack_configuration.png and b/doc/user/project/integrations/img/slack_configuration.png differ diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md index 09ba9994d3a..cfb0931273d 100644 --- a/doc/user/project/integrations/mattermost.md +++ b/doc/user/project/integrations/mattermost.md @@ -24,23 +24,24 @@ There, you will see a checkbox with the following events that can be triggered: - Push - Issue +- Confidential issue - Merge request - Note - Tag push - Build +- Pipeline - 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. +Below each of these event checkboxes, you have an input field to enter +which Mattermost channel you want to send that event message. Enter your preferred channel handle (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... | +| **Webhook** | The incoming webhook URL 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. | - +| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. | ![Mattermost configuration](img/mattermost_configuration.png) diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md index 57a9492044b..f27f9a726fc 100644 --- a/doc/user/project/integrations/slack.md +++ b/doc/user/project/integrations/slack.md @@ -21,23 +21,25 @@ There, you will see a checkbox with the following events that can be triggered: - Push - Issue +- Confidential issue - Merge request - Note - Tag push - Build +- Pipeline - Wiki page -Bellow each of these event checkboxes, you will have an input field to insert -which Slack channel you want to send that event message, with `#general` -being the default. Enter your preferred channel **without** the hash sign (`#`). +Below each of these event checkboxes, you have an input field to enter +which Slack channel you want to send that event message. Enter your preferred channel name **without** the hash sign (`#`). At the end, fill in your Slack details: | Field | Description | | ----- | ----------- | | **Webhook** | The [incoming webhook URL][slackhook] which you have to setup on Slack. | -| **Username** | Optional username which can be on messages sent to slack. Fill this in if you want to change the username of the bot. | +| **Username** | Optional username which can be on messages sent to Slack. 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. | +| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. | After you are all done, click **Save changes** for the changes to take effect. -- cgit v1.2.3 From 8aa757aa532c14445aca8731b7a553e657a58593 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 10 Feb 2017 17:12:18 +0000 Subject: Formats timeago dates to be more friendly Formats the timeago timestamps to be a short date. This will only be visible on slower connections whilst the JS is loading, after the JS has loaded it will be turned into a timeago string Closes #27537 --- app/helpers/application_helper.rb | 2 +- changelogs/unreleased/format-timeago-date.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/format-timeago-date.yml diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index bee323993a0..724641fd9d8 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -166,7 +166,7 @@ module ApplicationHelper css_classes = short_format ? 'js-short-timeago' : 'js-timeago' css_classes << " #{html_class}" unless html_class.blank? - element = content_tag :time, time.to_s, + element = content_tag :time, time.strftime("%b %d, %Y"), class: css_classes, title: time.to_time.in_time_zone.to_s(:medium), datetime: time.to_time.getutc.iso8601, diff --git a/changelogs/unreleased/format-timeago-date.yml b/changelogs/unreleased/format-timeago-date.yml new file mode 100644 index 00000000000..f331c34abbc --- /dev/null +++ b/changelogs/unreleased/format-timeago-date.yml @@ -0,0 +1,4 @@ +--- +title: Format timeago date to short format +merge_request: +author: -- cgit v1.2.3 From fca0c8d7f2e56a75d16a0b0d8b9155afa2d4cc02 Mon Sep 17 00:00:00 2001 From: Robert Marcano Date: Thu, 9 Feb 2017 14:38:33 -0400 Subject: Fix 'New Tag' layout on Tags page --- app/views/projects/tags/index.html.haml | 2 +- changelogs/unreleased/issue-tags-layout.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/issue-tags-layout.yml diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index e2f132f7742..7f9a44e565f 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -3,7 +3,7 @@ = render "projects/commits/head" .flex-list{ class: container_class } - .top-area.flex-row + .top-area.adjust .nav-text.row-main-content Tags give the ability to mark specific points in history as being important diff --git a/changelogs/unreleased/issue-tags-layout.yml b/changelogs/unreleased/issue-tags-layout.yml new file mode 100644 index 00000000000..abf4a609932 --- /dev/null +++ b/changelogs/unreleased/issue-tags-layout.yml @@ -0,0 +1,4 @@ +--- +title: Fix 'New Tag' layout on Tags page +merge_request: +author: Robert Marcano -- cgit v1.2.3 From a9c80dceb688623e8f06a925caf629fddfa180ec Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 13 Feb 2017 09:01:09 +0000 Subject: Fixed timeago specs --- spec/features/commits_spec.rb | 2 +- spec/helpers/application_helper_spec.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 8f561c8f90b..dede562c2b0 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -192,7 +192,7 @@ describe 'Commits' do commits = project.repository.commits(branch_name) commits.each do |commit| - expect(page).to have_content("committed #{commit.committed_date}") + expect(page).to have_content("committed #{commit.committed_date.strftime("%b %d, %Y")}") end end end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 8b201f348f1..9749936a2a0 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -193,8 +193,8 @@ describe ApplicationHelper do describe 'time_ago_with_tooltip' do def element(*arguments) Time.zone = 'UTC' - time = Time.zone.parse('2015-07-02 08:23') - element = helper.time_ago_with_tooltip(time, *arguments) + @time = Time.zone.parse('2015-07-02 08:23') + element = helper.time_ago_with_tooltip(@time, *arguments) Nokogiri::HTML::DocumentFragment.parse(element).first_element_child end @@ -204,7 +204,7 @@ describe ApplicationHelper do end it 'includes the date string' do - expect(element.text).to eq '2015-07-02 08:23:00 UTC' + expect(element.text).to eq @time.strftime("%b %d, %Y") end it 'has a datetime attribute' do -- cgit v1.2.3 From d2d30cff552e08c4b7ec2b5e23acced8eb29cbcb Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 14 Feb 2017 18:00:37 +0800 Subject: Initial implementation for default artifacts expiration TODO: Add tests and screenshots --- .../admin/application_settings_controller.rb | 1 + app/models/application_setting.rb | 19 ++++++++++++++-- app/models/ci/build.rb | 11 +++++++++ .../admin/application_settings/_form.html.haml | 9 +++++++- ...artifacts_expiration_to_application_settings.rb | 11 +++++++++ .../admin_area/settings/continuous_integration.md | 26 ++++++++++++++++++---- lib/api/settings.rb | 7 ++++-- lib/ci/api/builds.rb | 2 +- spec/models/ci/build_spec.rb | 2 +- 9 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index b0f5d4a9933..0559b5fa1bd 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -83,6 +83,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :akismet_api_key, :akismet_enabled, :container_registry_token_expire_delay, + :default_artifacts_expiration, :default_branch_protection, :default_group_visibility, :default_project_visibility, diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 74b358d8c40..a871dc9e5b9 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -76,6 +76,14 @@ class ApplicationSetting < ActiveRecord::Base presence: true, numericality: { only_integer: true, greater_than: 0 } + validates :max_artifacts_size, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + + validates :default_artifacts_expiration, + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :container_registry_token_expire_delay, presence: true, numericality: { only_integer: true, greater_than: 0 } @@ -168,6 +176,7 @@ class ApplicationSetting < ActiveRecord::Base after_sign_up_text: nil, akismet_enabled: false, container_registry_token_expire_delay: 5, + default_artifacts_expiration: 30, default_branch_protection: Settings.gitlab['default_branch_protection'], default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_projects_limit: Settings.gitlab['default_projects_limit'], @@ -201,9 +210,9 @@ class ApplicationSetting < ActiveRecord::Base sign_in_text: nil, signin_enabled: Settings.gitlab['signin_enabled'], signup_enabled: Settings.gitlab['signup_enabled'], + terminal_max_session_time: 0, two_factor_grace_period: 48, - user_default_external: false, - terminal_max_session_time: 0 + user_default_external: false } end @@ -282,6 +291,12 @@ class ApplicationSetting < ActiveRecord::Base sidekiq_throttling_enabled end + def default_artifacts_expire_in + if default_artifacts_expiration.nonzero? + "#{default_artifacts_expiration} days" + end + end + private def check_repository_storages diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 8c1b076c2d7..77f027d17b6 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -513,6 +513,17 @@ module Ci end end + def set_artifacts_expire_in(expire_in) + value = + if expire_in + expire_in + else + ApplicationSetting.current.default_artifacts_expire_in + end + + self.artifacts_expire_in = value + end + def has_expiring_artifacts? artifacts_expire_at.present? end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 816035ec442..86ce534ea18 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -212,8 +212,15 @@ .col-sm-10 = f.number_field :max_artifacts_size, class: 'form-control' .help-block - Set the maximum file size each jobs's artifacts can have + Set the maximum file size for each job's artifacts = link_to "(?)", help_page_path("user/admin_area/settings/continuous_integration", anchor: "maximum-artifacts-size") + .form-group + = f.label :default_artifacts_expiration, 'Default artifacts expiration (days)', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :default_artifacts_expiration, class: 'form-control' + .help-block + Set the default expiration time for each job's artifacts (0 as never expired) + = link_to "(?)", help_page_path("user/admin_area/settings/continuous_integration", anchor: "default-artifacts-expiration") - if Gitlab.config.registry.enabled %fieldset diff --git a/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb b/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb new file mode 100644 index 00000000000..728d581251c --- /dev/null +++ b/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb @@ -0,0 +1,11 @@ +class AddDefaultArtifactsExpirationToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :application_settings, + :default_artifacts_expiration, + :integer, default: 0, null: false + end +end diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md index 34e2e557f89..0455881ebf2 100644 --- a/doc/user/admin_area/settings/continuous_integration.md +++ b/doc/user/admin_area/settings/continuous_integration.md @@ -2,19 +2,37 @@ ## Maximum artifacts size -The maximum size of the [build artifacts][art-yml] can be set in the Admin area -of your GitLab instance. The value is in MB and the default is 100MB. Note that -this setting is set for each build. +The maximum size of the [build artifacts][art-yml] can be set in the Admin +area of your GitLab instance. The value is in *MB* and the default is 100MB. +Note that this setting is set for each job. 1. Go to **Admin area > Settings** (`/admin/application_settings`). ![Admin area settings button](img/admin_area_settings_button.png) -1. Change the value of the maximum artifacts size (in MB): +1. Change the value of maximum artifacts size (in MB): ![Admin area maximum artifacts size](img/admin_area_maximum_artifacts_size.png) 1. Hit **Save** for the changes to take effect. +[art-yml]: ../../../administration/build_artifacts.md + +## Default artifacts expiration time + +The default expiration time of the [build artifacts][art-yml] can be set in +the Admin area of your GitLab instance. The value is in *days* and the +default is 30 days. Note that this setting is set for each job. Set it to +0 as never expired by default. + +1. Go to **Admin area > Settings** (`/admin/application_settings`). + + ![Admin area settings button](img/admin_area_settings_button.png) + +1. Change the value of default expiration time (in days): + + ![Admin area default artifacts expiration](img/admin_area_default_artifacts_expiration.png) + +1. Hit **Save** for the changes to take effect. [art-yml]: ../../../administration/build_artifacts.md diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 747ceb4e3e0..dabe6281755 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -56,7 +56,8 @@ module API given shared_runners_enabled: ->(val) { val } do requires :shared_runners_text, type: String, desc: 'Shared runners text ' end - optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size each build's artifacts can have" + optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size for each job's artifacts" + optional :default_artifacts_expiration, type: Integer, desc: "Set the default expiration time for each job's artifacts" optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB' optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)' optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics' @@ -117,7 +118,9 @@ module API :send_user_confirmation_email, :domain_whitelist, :domain_blacklist_enabled, :after_sign_up_text, :signin_enabled, :require_two_factor_authentication, :home_page_url, :after_sign_out_path, :sign_in_text, :help_page_text, - :shared_runners_enabled, :max_artifacts_size, :max_pages_size, :container_registry_token_expire_delay, + :shared_runners_enabled, :max_artifacts_size, + :default_artifacts_expiration, :max_pages_size, + :container_registry_token_expire_delay, :metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled, :akismet_enabled, :admin_notification_email, :sentry_enabled, :repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled, diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb index 8b939663ffd..7aad6c50b7b 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -167,7 +167,7 @@ module Ci build.artifacts_file = artifacts build.artifacts_metadata = metadata - build.artifacts_expire_in = params['expire_in'] + build.set_artifacts_expire_in(params['expire_in']) if build.save present(build, with: Entities::BuildDetails) diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 4080092405d..1e88dc9d468 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -161,7 +161,7 @@ describe Ci::Build, :models do is_expected.to be_nil end - it 'when resseting value' do + it 'when resetting value' do build.artifacts_expire_in = nil is_expected.to be_nil -- cgit v1.2.3 From 696a5da7fd78093567ae2bc7a795a38462dd7b3d Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 14 Feb 2017 19:17:36 +0800 Subject: ApplicationSetting.current doesn't work well in tests Therefore we prefer `Gitlab::CurrentSettings.current_application_settings` and fix the tests by setting default_artifacts_expire_in to 0 to restore the original behaviour. --- app/models/ci/build.rb | 3 ++- spec/requests/ci/api/builds_spec.rb | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 77f027d17b6..d14f025aeaa 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -518,7 +518,8 @@ module Ci if expire_in expire_in else - ApplicationSetting.current.default_artifacts_expire_in + Gitlab::CurrentSettings.current_application_settings + .default_artifacts_expire_in end self.artifacts_expire_in = value diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index d85afdeab42..44f69bff30d 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -638,6 +638,8 @@ describe Ci::API::Builds do end before do + stub_application_setting(default_artifacts_expiration: 0) + post(post_url, post_data, headers_with_token) end -- cgit v1.2.3 From 3e158beca4f9e7e7887d7301e1ea97bef7eade84 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 14 Feb 2017 19:24:57 +0800 Subject: Add changelog entry --- changelogs/unreleased/27762-add-default-artifacts-expiration.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/27762-add-default-artifacts-expiration.yml diff --git a/changelogs/unreleased/27762-add-default-artifacts-expiration.yml b/changelogs/unreleased/27762-add-default-artifacts-expiration.yml new file mode 100644 index 00000000000..27fa77ed04d --- /dev/null +++ b/changelogs/unreleased/27762-add-default-artifacts-expiration.yml @@ -0,0 +1,4 @@ +--- +title: Add admin setting for default artifacts expiration +merge_request: 9219 +author: -- cgit v1.2.3 From ac872078486145f43e8a42dbd60ed47be70a301f Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 14 Feb 2017 19:34:07 +0800 Subject: Test build API if expire_in not set, set to app default --- spec/requests/ci/api/builds_spec.rb | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 44f69bff30d..0f2c6f2dc69 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -630,6 +630,7 @@ describe Ci::API::Builds do context 'with an expire date' do let!(:artifacts) { file_upload } + let(:default_artifacts_expiration) { 0 } let(:post_data) do { 'file.path' => artifacts.path, @@ -638,7 +639,8 @@ describe Ci::API::Builds do end before do - stub_application_setting(default_artifacts_expiration: 0) + stub_application_setting( + default_artifacts_expiration: default_artifacts_expiration) post(post_url, post_data, headers_with_token) end @@ -650,7 +652,8 @@ describe Ci::API::Builds do build.reload expect(response).to have_http_status(201) expect(json_response['artifacts_expire_at']).not_to be_empty - expect(build.artifacts_expire_at).to be_within(5.minutes).of(Time.now + 7.days) + expect(build.artifacts_expire_at). + to be_within(5.minutes).of(7.days.from_now) end end @@ -663,6 +666,18 @@ describe Ci::API::Builds do expect(json_response['artifacts_expire_at']).to be_nil expect(build.artifacts_expire_at).to be_nil end + + context 'with application default' do + let(:default_artifacts_expiration) { 5 } + + it 'sets to application default' do + build.reload + expect(response).to have_http_status(201) + expect(json_response['artifacts_expire_at']).not_to be_empty + expect(build.artifacts_expire_at). + to be_within(5.minutes).of(5.days.from_now) + end + end end end -- cgit v1.2.3 From 53c94f9ea25c9d6a25b58a01db5f855f0719dbf4 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 14 Feb 2017 22:54:46 +0800 Subject: Use the same syntax for default expiration Feedback: * https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9219#note_23343951 * https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9219#note_23344036 * https://gitlab.com/gitlab-org/gitlab-ce/issues/27762#note_23344797 --- .../admin/application_settings_controller.rb | 2 +- app/models/application_setting.rb | 20 ++++++++++---------- app/models/ci/build.rb | 12 ------------ app/views/admin/application_settings/_form.html.haml | 10 +++++----- ...t_artifacts_expiration_to_application_settings.rb | 4 ++-- lib/api/settings.rb | 4 ++-- lib/ci/api/builds.rb | 5 ++++- spec/requests/ci/api/builds_spec.rb | 6 +++--- 8 files changed, 27 insertions(+), 36 deletions(-) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 0559b5fa1bd..d807e6263ee 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -83,7 +83,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :akismet_api_key, :akismet_enabled, :container_registry_token_expire_delay, - :default_artifacts_expiration, + :default_artifacts_expire_in, :default_branch_protection, :default_group_visibility, :default_project_visibility, diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index a871dc9e5b9..2b97027c018 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -80,9 +80,7 @@ class ApplicationSetting < ActiveRecord::Base presence: true, numericality: { only_integer: true, greater_than: 0 } - validates :default_artifacts_expiration, - presence: true, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validate :check_default_artifacts_expire_in validates :container_registry_token_expire_delay, presence: true, @@ -176,7 +174,7 @@ class ApplicationSetting < ActiveRecord::Base after_sign_up_text: nil, akismet_enabled: false, container_registry_token_expire_delay: 5, - default_artifacts_expiration: 30, + default_artifacts_expire_in: '30 days', default_branch_protection: Settings.gitlab['default_branch_protection'], default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_projects_limit: Settings.gitlab['default_projects_limit'], @@ -291,12 +289,6 @@ class ApplicationSetting < ActiveRecord::Base sidekiq_throttling_enabled end - def default_artifacts_expire_in - if default_artifacts_expiration.nonzero? - "#{default_artifacts_expiration} days" - end - end - private def check_repository_storages @@ -304,4 +296,12 @@ class ApplicationSetting < ActiveRecord::Base errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless invalid.empty? end + + def check_default_artifacts_expire_in + ChronicDuration.parse(default_artifacts_expire_in) + true + rescue ChronicDuration::DurationParseError => e + errors.add(:default_artifacts_expire_in, ": #{e.message}") + false + end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index d14f025aeaa..8c1b076c2d7 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -513,18 +513,6 @@ module Ci end end - def set_artifacts_expire_in(expire_in) - value = - if expire_in - expire_in - else - Gitlab::CurrentSettings.current_application_settings - .default_artifacts_expire_in - end - - self.artifacts_expire_in = value - end - def has_expiring_artifacts? artifacts_expire_at.present? end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 86ce534ea18..f96ddc38af1 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -213,14 +213,14 @@ = f.number_field :max_artifacts_size, class: 'form-control' .help-block Set the maximum file size for each job's artifacts - = link_to "(?)", help_page_path("user/admin_area/settings/continuous_integration", anchor: "maximum-artifacts-size") + = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size') .form-group - = f.label :default_artifacts_expiration, 'Default artifacts expiration (days)', class: 'control-label col-sm-2' + = f.label :default_artifacts_expire_in, 'Default artifacts expiration', class: 'control-label col-sm-2' .col-sm-10 - = f.number_field :default_artifacts_expiration, class: 'form-control' + = f.text_field :default_artifacts_expire_in, class: 'form-control' .help-block - Set the default expiration time for each job's artifacts (0 as never expired) - = link_to "(?)", help_page_path("user/admin_area/settings/continuous_integration", anchor: "default-artifacts-expiration") + Set the default expiration time for each job's artifacts + = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration') - if Gitlab.config.registry.enabled %fieldset diff --git a/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb b/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb index 728d581251c..34905570739 100644 --- a/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb +++ b/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb @@ -5,7 +5,7 @@ class AddDefaultArtifactsExpirationToApplicationSettings < ActiveRecord::Migrati def change add_column :application_settings, - :default_artifacts_expiration, - :integer, default: 0, null: false + :default_artifacts_expire_in, + :string, null: true end end diff --git a/lib/api/settings.rb b/lib/api/settings.rb index dabe6281755..f46e7e0bcf1 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -57,7 +57,7 @@ module API requires :shared_runners_text, type: String, desc: 'Shared runners text ' end optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size for each job's artifacts" - optional :default_artifacts_expiration, type: Integer, desc: "Set the default expiration time for each job's artifacts" + optional :default_artifacts_expire_in, type: Integer, desc: "Set the default expiration time for each job's artifacts" optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB' optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)' optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics' @@ -119,7 +119,7 @@ module API :after_sign_up_text, :signin_enabled, :require_two_factor_authentication, :home_page_url, :after_sign_out_path, :sign_in_text, :help_page_text, :shared_runners_enabled, :max_artifacts_size, - :default_artifacts_expiration, :max_pages_size, + :default_artifacts_expire_in, :max_pages_size, :container_registry_token_expire_delay, :metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled, :akismet_enabled, :admin_notification_email, :sentry_enabled, diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb index 7aad6c50b7b..2018191c4bd 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -167,7 +167,10 @@ module Ci build.artifacts_file = artifacts build.artifacts_metadata = metadata - build.set_artifacts_expire_in(params['expire_in']) + build.artifacts_expire_in = + params['expire_in'] || + Gitlab::CurrentSettings.current_application_settings + .default_artifacts_expire_in if build.save present(build, with: Entities::BuildDetails) diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 0f2c6f2dc69..f2385da8c13 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -630,7 +630,7 @@ describe Ci::API::Builds do context 'with an expire date' do let!(:artifacts) { file_upload } - let(:default_artifacts_expiration) { 0 } + let(:default_artifacts_expire_in) {} let(:post_data) do { 'file.path' => artifacts.path, @@ -640,7 +640,7 @@ describe Ci::API::Builds do before do stub_application_setting( - default_artifacts_expiration: default_artifacts_expiration) + default_artifacts_expire_in: default_artifacts_expire_in) post(post_url, post_data, headers_with_token) end @@ -668,7 +668,7 @@ describe Ci::API::Builds do end context 'with application default' do - let(:default_artifacts_expiration) { 5 } + let(:default_artifacts_expire_in) { '5 days' } it 'sets to application default' do build.reload -- cgit v1.2.3 From a1b06a82eac888024dc0d4c77f7c93f6053a3815 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 14 Feb 2017 23:44:05 +0800 Subject: rubocop: Align the operands of an expression C: Style/MultilineOperationIndentation: Align the operands of an expression in an assignment spanning multiple lines. --- lib/ci/api/builds.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb index 2018191c4bd..0e17ac24d5a 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -169,8 +169,8 @@ module Ci build.artifacts_metadata = metadata build.artifacts_expire_in = params['expire_in'] || - Gitlab::CurrentSettings.current_application_settings - .default_artifacts_expire_in + Gitlab::CurrentSettings.current_application_settings + .default_artifacts_expire_in if build.save present(build, with: Entities::BuildDetails) -- cgit v1.2.3 From 4eff5eb89fc6eec5692f4119b2fc3c9622d1b3e3 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 14 Feb 2017 23:47:23 +0800 Subject: Check default_artifacts_expire_in only if existed --- app/models/application_setting.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 2b97027c018..818223dcc86 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -298,7 +298,8 @@ class ApplicationSetting < ActiveRecord::Base end def check_default_artifacts_expire_in - ChronicDuration.parse(default_artifacts_expire_in) + ChronicDuration.parse(default_artifacts_expire_in) if + default_artifacts_expire_in true rescue ChronicDuration::DurationParseError => e errors.add(:default_artifacts_expire_in, ": #{e.message}") -- cgit v1.2.3 From 602f3b84c08c06cd132a8c53c7bcbb3a139cebfd Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 15 Feb 2017 00:19:36 +0800 Subject: Add a few more tests and make sure empty value sets to nil --- app/models/application_setting.rb | 8 ++++++++ spec/models/application_setting_spec.rb | 22 ++++++++++++++++++++++ spec/requests/api/settings_spec.rb | 11 +++++++++-- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 818223dcc86..17193036fb6 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -269,6 +269,14 @@ class ApplicationSetting < ActiveRecord::Base self.repository_storages = [value] end + def default_artifacts_expire_in=(value) + if value.present? + super(value.strip) + else + super(nil) + end + end + # Choose one of the available repository storage options. Currently all have # equal weighting. def pick_repository_storage diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index b950fcdd81a..15632bac094 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -29,6 +29,28 @@ describe ApplicationSetting, models: true do it { is_expected.not_to allow_value(['test']).for(:disabled_oauth_sign_in_sources) } end + describe 'default_artifacts_expire_in' do + it 'sets an error if it is invalid' do + setting.update(default_artifacts_expire_in: 'a') + + expect(setting).to be_invalid + end + + it 'sets the value if it is valid' do + setting.update(default_artifacts_expire_in: '30 days') + + expect(setting).to be_valid + expect(setting.default_artifacts_expire_in).to eq('30 days') + end + + it 'does not set it if it is blank' do + setting.update(default_artifacts_expire_in: ' ') + + expect(setting).to be_valid + expect(setting.default_artifacts_expire_in).to be_nil + end + end + it { is_expected.to validate_presence_of(:max_attachment_size) } it do diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 91e3c333a02..411905edb49 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -30,8 +30,14 @@ describe API::Settings, 'Settings', api: true do it "updates application settings" do put api("/application/settings", admin), - default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com', - plantuml_enabled: true, plantuml_url: 'http://plantuml.example.com' + default_projects_limit: 3, + signin_enabled: false, + repository_storage: 'custom', + koding_enabled: true, + koding_url: 'http://koding.example.com', + plantuml_enabled: true, + plantuml_url: 'http://plantuml.example.com', + default_artifacts_expire_in: '2 days' expect(response).to have_http_status(200) expect(json_response['default_projects_limit']).to eq(3) expect(json_response['signin_enabled']).to be_falsey @@ -41,6 +47,7 @@ describe API::Settings, 'Settings', api: true do expect(json_response['koding_url']).to eq('http://koding.example.com') expect(json_response['plantuml_enabled']).to be_truthy expect(json_response['plantuml_url']).to eq('http://plantuml.example.com') + expect(json_response['default_artifacts_expire_in']).to eq('2 days') end end -- cgit v1.2.3 From cfd839d6f5092be2f5224eddc155f6cf05cd1be6 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 15 Feb 2017 15:31:25 +0800 Subject: Fix tests and disallow 0 to make it consistent with .gitlab-ci.yml --- app/models/application_setting.rb | 12 +++++++++--- lib/api/entities.rb | 1 + lib/api/settings.rb | 2 +- spec/models/application_setting_spec.rb | 6 ++++++ 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 17193036fb6..19af40eacb4 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -306,9 +306,15 @@ class ApplicationSetting < ActiveRecord::Base end def check_default_artifacts_expire_in - ChronicDuration.parse(default_artifacts_expire_in) if - default_artifacts_expire_in - true + return true unless default_artifacts_expire_in + + if ChronicDuration.parse(default_artifacts_expire_in).nil? + errors.add(:default_artifacts_expire_in, + "can't be 0. Leave it blank for unlimited") + false + else + true + end rescue ChronicDuration::DurationParseError => e errors.add(:default_artifacts_expire_in, ": #{e.message}") false diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 2a071e649fa..fb0584539db 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -559,6 +559,7 @@ module API expose :default_project_visibility expose :default_snippet_visibility expose :default_group_visibility + expose :default_artifacts_expire_in expose :domain_whitelist expose :domain_blacklist_enabled expose :domain_blacklist diff --git a/lib/api/settings.rb b/lib/api/settings.rb index f46e7e0bcf1..936c7e0930b 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -57,7 +57,7 @@ module API requires :shared_runners_text, type: String, desc: 'Shared runners text ' end optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size for each job's artifacts" - optional :default_artifacts_expire_in, type: Integer, desc: "Set the default expiration time for each job's artifacts" + optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts" optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB' optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)' optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics' diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 15632bac094..9629f858609 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -36,6 +36,12 @@ describe ApplicationSetting, models: true do expect(setting).to be_invalid end + it 'does not allow 0' do + setting.update(default_artifacts_expire_in: '0') + + expect(setting).to be_invalid + end + it 'sets the value if it is valid' do setting.update(default_artifacts_expire_in: '30 days') -- cgit v1.2.3 From b72f00c7cfdc854640cbb118c741a116ae4fd8ba Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 15 Feb 2017 16:03:58 +0800 Subject: Update docs to reflect current behaviour --- app/models/application_setting.rb | 2 +- app/views/admin/application_settings/_form.html.haml | 4 +++- doc/user/admin_area/settings/continuous_integration.md | 11 ++++++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 19af40eacb4..6d5f02a1011 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -310,7 +310,7 @@ class ApplicationSetting < ActiveRecord::Base if ChronicDuration.parse(default_artifacts_expire_in).nil? errors.add(:default_artifacts_expire_in, - "can't be 0. Leave it blank for unlimited") + "can't be 0. Leave it blank for no expiration") false else true diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index f96ddc38af1..3004f32f1f2 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -220,7 +220,9 @@ = f.text_field :default_artifacts_expire_in, class: 'form-control' .help-block Set the default expiration time for each job's artifacts - = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration') + = surround '(', ')' do + = link_to 'syntax', help_page_path('ci/yaml/README', anchor: 'artifactsexpire_in') + = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration-time') - if Gitlab.config.registry.enabled %fieldset diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md index 0455881ebf2..295e0adec45 100644 --- a/doc/user/admin_area/settings/continuous_integration.md +++ b/doc/user/admin_area/settings/continuous_integration.md @@ -16,14 +16,14 @@ Note that this setting is set for each job. 1. Hit **Save** for the changes to take effect. -[art-yml]: ../../../administration/build_artifacts.md +[art-yml]: ../../../administration/build_artifacts ## Default artifacts expiration time The default expiration time of the [build artifacts][art-yml] can be set in -the Admin area of your GitLab instance. The value is in *days* and the -default is 30 days. Note that this setting is set for each job. Set it to -0 as never expired by default. +the Admin area of your GitLab instance. The syntax of duration is described +in [artifacts:expire_in][duration-syntax]. The default is `30 days`. Note that +this setting is set for each job. Leave it blank for no default expiration. 1. Go to **Admin area > Settings** (`/admin/application_settings`). @@ -35,4 +35,5 @@ default is 30 days. Note that this setting is set for each job. Set it to 1. Hit **Save** for the changes to take effect. -[art-yml]: ../../../administration/build_artifacts.md +[art-yml]: ../../../administration/build_artifacts +[duration-syntax]: ../../../ci/yaml/README#artifactsexpire_in -- cgit v1.2.3 From 618ce941647177b560fb3f5b677325bb964edae3 Mon Sep 17 00:00:00 2001 From: Tomasz Maczukin Date: Tue, 14 Feb 2017 23:52:02 +0100 Subject: Add Runner registration/deletion API --- lib/api/api.rb | 1 + lib/api/ci.rb | 52 +++++++++++++++ lib/api/entities.rb | 4 ++ lib/api/helpers/ci.rb | 24 +++++++ spec/requests/api/ci_spec.rb | 148 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 229 insertions(+) create mode 100644 lib/api/ci.rb create mode 100644 lib/api/helpers/ci.rb create mode 100644 spec/requests/api/ci_spec.rb diff --git a/lib/api/api.rb b/lib/api/api.rb index 06346ae822a..6d7eb3eb84f 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -52,6 +52,7 @@ module API mount ::API::Branches mount ::API::BroadcastMessages mount ::API::Builds + mount ::API::Ci mount ::API::Commits mount ::API::CommitStatuses mount ::API::DeployKeys diff --git a/lib/api/ci.rb b/lib/api/ci.rb new file mode 100644 index 00000000000..635116cc88d --- /dev/null +++ b/lib/api/ci.rb @@ -0,0 +1,52 @@ +module API + class Ci < Grape::API + helpers ::API::Helpers::Ci + + resource :runners do + desc 'Registers a new Runner' do + success Entities::RunnerRegistrationDetails + http_codes [[201, 'Runner was created'], [403, 'Forbidden']] + end + params do + requires :token, type: String, desc: 'Registration token' + optional :description, type: String, desc: %q(Runner's description) + optional :info, type: Hash, desc: %q(Runner's metadata) + optional :locked, type: Boolean, desc: 'Should Runner be locked for current project' + optional :run_untagged, type: Boolean, desc: 'Should Runner handle untagged jobs' + optional :tag_list, type: Array[String], desc: %q(List of Runner's tags) + end + post '/' do + attributes = attributes_for_keys [:description, :locked, :run_untagged, :tag_list] + + runner = + if runner_registration_token_valid? + # Create shared runner. Requires admin access + ::Ci::Runner.create(attributes.merge(is_shared: true)) + elsif project = Project.find_by(runners_token: params[:token]) + # Create a specific runner for project. + project.runners.create(attributes) + end + + return forbidden! unless runner + + if runner.id + runner.update(get_runner_version_from_params) + present runner, with: Entities::RunnerRegistrationDetails + else + not_found! + end + end + + desc 'Deletes a registered Runner' do + http_codes [[200, 'Runner was deleted'], [403, 'Forbidden']] + end + params do + requires :token, type: String, desc: %q(Runner's authentication token) + end + delete '/' do + authenticate_runner! + ::Ci::Runner.find_by_token(params[:token]).destroy + end + end + end +end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 232f231ddd2..8229e67eeac 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -620,6 +620,10 @@ module API end end + class RunnerRegistrationDetails < Grape::Entity + expose :id, :token + end + class BuildArtifactFile < Grape::Entity expose :filename, :size end diff --git a/lib/api/helpers/ci.rb b/lib/api/helpers/ci.rb new file mode 100644 index 00000000000..24669eba4bb --- /dev/null +++ b/lib/api/helpers/ci.rb @@ -0,0 +1,24 @@ +module API + module Helpers + module Ci + def runner_registration_token_valid? + ActiveSupport::SecurityUtils.variable_size_secure_compare( + params[:token], + current_application_settings.runners_registration_token) + end + + def get_runner_version_from_params + return unless params['info'].present? + attributes_for_keys(%w(name version revision platform architecture), params['info']) + end + + def authenticate_runner! + forbidden! unless current_runner + end + + def current_runner + @runner ||= ::Ci::Runner.find_by_token(params[:token].to_s) + end + end + end +end \ No newline at end of file diff --git a/spec/requests/api/ci_spec.rb b/spec/requests/api/ci_spec.rb new file mode 100644 index 00000000000..c8d7abd3eb9 --- /dev/null +++ b/spec/requests/api/ci_spec.rb @@ -0,0 +1,148 @@ +require 'spec_helper' + +describe API::Ci do + include ApiHelpers + include StubGitlabCalls + + let(:registration_token) { 'abcdefg123456' } + + before do + stub_gitlab_calls + stub_application_setting(runners_registration_token: registration_token) + end + + describe '/api/v4/runners' do + describe 'POST /api/v4/runners' do + context 'when no token is provided' do + it 'returns 400 error' do + post api('/runners') + expect(response).to have_http_status 400 + end + end + + context 'when invalid token is provided' do + it 'returns 403 error' do + post api('/runners'), token: 'invalid' + expect(response).to have_http_status 403 + end + end + + context 'when valid token is provided' do + it 'creates runner with default values' do + post api('/runners'), token: registration_token + + runner = Ci::Runner.first + + expect(response).to have_http_status 201 + expect(json_response['id']).to eq(runner.id) + expect(json_response['token']).to eq(runner.token) + expect(runner.run_untagged).to be true + end + + context 'when project token is used' do + let(:project) { create(:empty_project) } + + it 'creates runner' do + post api('/runners'), token: project.runners_token + + expect(response).to have_http_status 201 + expect(project.runners.size).to eq(1) + end + end + end + + context 'when runner description is provided' do + it 'creates runner' do + post api('/runners'), token: registration_token, + description: 'server.hostname' + + expect(response).to have_http_status 201 + expect(Ci::Runner.first.description).to eq('server.hostname') + end + end + + context 'when runner tags are provided' do + it 'creates runner' do + post api('/runners'), token: registration_token, + tag_list: 'tag1, tag2' + + expect(response).to have_http_status 201 + expect(Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2)) + end + end + + context 'when option for running untagged jobs is provided' do + context 'when tags are provided' do + it 'creates runner' do + post api('/runners'), token: registration_token, + run_untagged: false, + tag_list: ['tag'] + + expect(response).to have_http_status 201 + expect(Ci::Runner.first.run_untagged).to be false + expect(Ci::Runner.first.tag_list.sort).to eq(['tag']) + end + end + + context 'when tags are not provided' do + it 'returns 404 error' do + post api('/runners'), token: registration_token, + run_untagged: false + + expect(response).to have_http_status 404 + end + end + end + + context 'when option for locking Runner is provided' do + it 'creates runner' do + post api('/runners'), token: registration_token, + locked: true + + expect(response).to have_http_status 201 + expect(Ci::Runner.first.locked).to be true + end + end + + %w(name version revision platform architecture).each do |param| + context "when info parameter '#{param}' info is present" do + let(:value) { "#{param}_value" } + + it %q(updates provided Runner's parameter) do + post api('/runners'), token: registration_token, + info: {param => value} + + expect(response).to have_http_status 201 + expect(Ci::Runner.first.read_attribute(param.to_sym)).to eq(value) + end + end + end + end + + describe 'DELETE /api/v4/runners' do + context 'when no token is provided' do + it 'returns 400 error' do + delete api('/runners') + expect(response).to have_http_status 400 + end + end + + context 'when invalid token is provided' do + it 'returns 403 error' do + delete api('/runners'), token: 'invalid' + expect(response).to have_http_status 403 + end + end + + context 'when valid token is provided' do + let(:runner) { create(:ci_runner) } + + it 'deletes Runner' do + delete api('/runners'), token: runner.token + expect(response).to have_http_status 200 + expect(Ci::Runner.count).to eq(0) + end + end + end + end +end \ No newline at end of file -- cgit v1.2.3 From a4b2e90e6418bedc20d2a648243f7ee140c6761d Mon Sep 17 00:00:00 2001 From: Tomasz Maczukin Date: Wed, 15 Feb 2017 00:04:55 +0100 Subject: Add changelog entry --- .../unreleased/feature-runners-registration-deletion-v4-api.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/feature-runners-registration-deletion-v4-api.yml diff --git a/changelogs/unreleased/feature-runners-registration-deletion-v4-api.yml b/changelogs/unreleased/feature-runners-registration-deletion-v4-api.yml new file mode 100644 index 00000000000..e646a6a17b7 --- /dev/null +++ b/changelogs/unreleased/feature-runners-registration-deletion-v4-api.yml @@ -0,0 +1,4 @@ +--- +title: Add Runner's registration/deletion v4 API +merge_request: 9246 +author: -- cgit v1.2.3 From 22c983d7a0219e48856800405aec7d9b930fc9a1 Mon Sep 17 00:00:00 2001 From: Tomasz Maczukin Date: Thu, 16 Feb 2017 01:20:17 +0100 Subject: Fix rubocop offenses --- lib/api/helpers/ci.rb | 7 +++---- spec/requests/api/ci_spec.rb | 16 ++++++++-------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/lib/api/helpers/ci.rb b/lib/api/helpers/ci.rb index 24669eba4bb..e928d7c874a 100644 --- a/lib/api/helpers/ci.rb +++ b/lib/api/helpers/ci.rb @@ -2,9 +2,8 @@ module API module Helpers module Ci def runner_registration_token_valid? - ActiveSupport::SecurityUtils.variable_size_secure_compare( - params[:token], - current_application_settings.runners_registration_token) + ActiveSupport::SecurityUtils.variable_size_secure_compare(params[:token], + current_application_settings.runners_registration_token) end def get_runner_version_from_params @@ -21,4 +20,4 @@ module API end end end -end \ No newline at end of file +end diff --git a/spec/requests/api/ci_spec.rb b/spec/requests/api/ci_spec.rb index c8d7abd3eb9..4eef78dd1c2 100644 --- a/spec/requests/api/ci_spec.rb +++ b/spec/requests/api/ci_spec.rb @@ -54,7 +54,7 @@ describe API::Ci do context 'when runner description is provided' do it 'creates runner' do post api('/runners'), token: registration_token, - description: 'server.hostname' + description: 'server.hostname' expect(response).to have_http_status 201 expect(Ci::Runner.first.description).to eq('server.hostname') @@ -64,7 +64,7 @@ describe API::Ci do context 'when runner tags are provided' do it 'creates runner' do post api('/runners'), token: registration_token, - tag_list: 'tag1, tag2' + tag_list: 'tag1, tag2' expect(response).to have_http_status 201 expect(Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2)) @@ -75,8 +75,8 @@ describe API::Ci do context 'when tags are provided' do it 'creates runner' do post api('/runners'), token: registration_token, - run_untagged: false, - tag_list: ['tag'] + run_untagged: false, + tag_list: ['tag'] expect(response).to have_http_status 201 expect(Ci::Runner.first.run_untagged).to be false @@ -87,7 +87,7 @@ describe API::Ci do context 'when tags are not provided' do it 'returns 404 error' do post api('/runners'), token: registration_token, - run_untagged: false + run_untagged: false expect(response).to have_http_status 404 end @@ -97,7 +97,7 @@ describe API::Ci do context 'when option for locking Runner is provided' do it 'creates runner' do post api('/runners'), token: registration_token, - locked: true + locked: true expect(response).to have_http_status 201 expect(Ci::Runner.first.locked).to be true @@ -110,7 +110,7 @@ describe API::Ci do it %q(updates provided Runner's parameter) do post api('/runners'), token: registration_token, - info: {param => value} + info: { param => value } expect(response).to have_http_status 201 expect(Ci::Runner.first.read_attribute(param.to_sym)).to eq(value) @@ -145,4 +145,4 @@ describe API::Ci do end end end -end \ No newline at end of file +end -- cgit v1.2.3 From e25f26e5503a5aa07f5cfc436e10b92254185f9a Mon Sep 17 00:00:00 2001 From: Tomasz Maczukin Date: Thu, 16 Feb 2017 01:30:46 +0100 Subject: Rename API::Ci to API::Runner --- lib/api/api.rb | 2 +- lib/api/ci.rb | 52 -------------- lib/api/helpers/ci.rb | 23 ------ lib/api/helpers/runner.rb | 23 ++++++ lib/api/runner.rb | 52 ++++++++++++++ spec/requests/api/ci_spec.rb | 148 --------------------------------------- spec/requests/api/runner_spec.rb | 148 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 224 insertions(+), 224 deletions(-) delete mode 100644 lib/api/ci.rb delete mode 100644 lib/api/helpers/ci.rb create mode 100644 lib/api/helpers/runner.rb create mode 100644 lib/api/runner.rb delete mode 100644 spec/requests/api/ci_spec.rb create mode 100644 spec/requests/api/runner_spec.rb diff --git a/lib/api/api.rb b/lib/api/api.rb index 6d7eb3eb84f..016b9572649 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -52,7 +52,6 @@ module API mount ::API::Branches mount ::API::BroadcastMessages mount ::API::Builds - mount ::API::Ci mount ::API::Commits mount ::API::CommitStatuses mount ::API::DeployKeys @@ -77,6 +76,7 @@ module API mount ::API::Projects mount ::API::ProjectSnippets mount ::API::Repositories + mount ::API::Runner mount ::API::Runners mount ::API::Services mount ::API::Session diff --git a/lib/api/ci.rb b/lib/api/ci.rb deleted file mode 100644 index 635116cc88d..00000000000 --- a/lib/api/ci.rb +++ /dev/null @@ -1,52 +0,0 @@ -module API - class Ci < Grape::API - helpers ::API::Helpers::Ci - - resource :runners do - desc 'Registers a new Runner' do - success Entities::RunnerRegistrationDetails - http_codes [[201, 'Runner was created'], [403, 'Forbidden']] - end - params do - requires :token, type: String, desc: 'Registration token' - optional :description, type: String, desc: %q(Runner's description) - optional :info, type: Hash, desc: %q(Runner's metadata) - optional :locked, type: Boolean, desc: 'Should Runner be locked for current project' - optional :run_untagged, type: Boolean, desc: 'Should Runner handle untagged jobs' - optional :tag_list, type: Array[String], desc: %q(List of Runner's tags) - end - post '/' do - attributes = attributes_for_keys [:description, :locked, :run_untagged, :tag_list] - - runner = - if runner_registration_token_valid? - # Create shared runner. Requires admin access - ::Ci::Runner.create(attributes.merge(is_shared: true)) - elsif project = Project.find_by(runners_token: params[:token]) - # Create a specific runner for project. - project.runners.create(attributes) - end - - return forbidden! unless runner - - if runner.id - runner.update(get_runner_version_from_params) - present runner, with: Entities::RunnerRegistrationDetails - else - not_found! - end - end - - desc 'Deletes a registered Runner' do - http_codes [[200, 'Runner was deleted'], [403, 'Forbidden']] - end - params do - requires :token, type: String, desc: %q(Runner's authentication token) - end - delete '/' do - authenticate_runner! - ::Ci::Runner.find_by_token(params[:token]).destroy - end - end - end -end diff --git a/lib/api/helpers/ci.rb b/lib/api/helpers/ci.rb deleted file mode 100644 index e928d7c874a..00000000000 --- a/lib/api/helpers/ci.rb +++ /dev/null @@ -1,23 +0,0 @@ -module API - module Helpers - module Ci - def runner_registration_token_valid? - ActiveSupport::SecurityUtils.variable_size_secure_compare(params[:token], - current_application_settings.runners_registration_token) - end - - def get_runner_version_from_params - return unless params['info'].present? - attributes_for_keys(%w(name version revision platform architecture), params['info']) - end - - def authenticate_runner! - forbidden! unless current_runner - end - - def current_runner - @runner ||= ::Ci::Runner.find_by_token(params[:token].to_s) - end - end - end -end diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb new file mode 100644 index 00000000000..119ca81b883 --- /dev/null +++ b/lib/api/helpers/runner.rb @@ -0,0 +1,23 @@ +module API + module Helpers + module Runner + def runner_registration_token_valid? + ActiveSupport::SecurityUtils.variable_size_secure_compare(params[:token], + current_application_settings.runners_registration_token) + end + + def get_runner_version_from_params + return unless params['info'].present? + attributes_for_keys(%w(name version revision platform architecture), params['info']) + end + + def authenticate_runner! + forbidden! unless current_runner + end + + def current_runner + @runner ||= ::Ci::Runner.find_by_token(params[:token].to_s) + end + end + end +end diff --git a/lib/api/runner.rb b/lib/api/runner.rb new file mode 100644 index 00000000000..804b27d40a7 --- /dev/null +++ b/lib/api/runner.rb @@ -0,0 +1,52 @@ +module API + class Runner < Grape::API + helpers ::API::Helpers::Runner + + resource :runners do + desc 'Registers a new Runner' do + success Entities::RunnerRegistrationDetails + http_codes [[201, 'Runner was created'], [403, 'Forbidden']] + end + params do + requires :token, type: String, desc: 'Registration token' + optional :description, type: String, desc: %q(Runner's description) + optional :info, type: Hash, desc: %q(Runner's metadata) + optional :locked, type: Boolean, desc: 'Should Runner be locked for current project' + optional :run_untagged, type: Boolean, desc: 'Should Runner handle untagged jobs' + optional :tag_list, type: Array[String], desc: %q(List of Runner's tags) + end + post '/' do + attributes = attributes_for_keys [:description, :locked, :run_untagged, :tag_list] + + runner = + if runner_registration_token_valid? + # Create shared runner. Requires admin access + Ci::Runner.create(attributes.merge(is_shared: true)) + elsif project = Project.find_by(runners_token: params[:token]) + # Create a specific runner for project. + project.runners.create(attributes) + end + + return forbidden! unless runner + + if runner.id + runner.update(get_runner_version_from_params) + present runner, with: Entities::RunnerRegistrationDetails + else + not_found! + end + end + + desc 'Deletes a registered Runner' do + http_codes [[200, 'Runner was deleted'], [403, 'Forbidden']] + end + params do + requires :token, type: String, desc: %q(Runner's authentication token) + end + delete '/' do + authenticate_runner! + Ci::Runner.find_by_token(params[:token]).destroy + end + end + end +end diff --git a/spec/requests/api/ci_spec.rb b/spec/requests/api/ci_spec.rb deleted file mode 100644 index 4eef78dd1c2..00000000000 --- a/spec/requests/api/ci_spec.rb +++ /dev/null @@ -1,148 +0,0 @@ -require 'spec_helper' - -describe API::Ci do - include ApiHelpers - include StubGitlabCalls - - let(:registration_token) { 'abcdefg123456' } - - before do - stub_gitlab_calls - stub_application_setting(runners_registration_token: registration_token) - end - - describe '/api/v4/runners' do - describe 'POST /api/v4/runners' do - context 'when no token is provided' do - it 'returns 400 error' do - post api('/runners') - expect(response).to have_http_status 400 - end - end - - context 'when invalid token is provided' do - it 'returns 403 error' do - post api('/runners'), token: 'invalid' - expect(response).to have_http_status 403 - end - end - - context 'when valid token is provided' do - it 'creates runner with default values' do - post api('/runners'), token: registration_token - - runner = Ci::Runner.first - - expect(response).to have_http_status 201 - expect(json_response['id']).to eq(runner.id) - expect(json_response['token']).to eq(runner.token) - expect(runner.run_untagged).to be true - end - - context 'when project token is used' do - let(:project) { create(:empty_project) } - - it 'creates runner' do - post api('/runners'), token: project.runners_token - - expect(response).to have_http_status 201 - expect(project.runners.size).to eq(1) - end - end - end - - context 'when runner description is provided' do - it 'creates runner' do - post api('/runners'), token: registration_token, - description: 'server.hostname' - - expect(response).to have_http_status 201 - expect(Ci::Runner.first.description).to eq('server.hostname') - end - end - - context 'when runner tags are provided' do - it 'creates runner' do - post api('/runners'), token: registration_token, - tag_list: 'tag1, tag2' - - expect(response).to have_http_status 201 - expect(Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2)) - end - end - - context 'when option for running untagged jobs is provided' do - context 'when tags are provided' do - it 'creates runner' do - post api('/runners'), token: registration_token, - run_untagged: false, - tag_list: ['tag'] - - expect(response).to have_http_status 201 - expect(Ci::Runner.first.run_untagged).to be false - expect(Ci::Runner.first.tag_list.sort).to eq(['tag']) - end - end - - context 'when tags are not provided' do - it 'returns 404 error' do - post api('/runners'), token: registration_token, - run_untagged: false - - expect(response).to have_http_status 404 - end - end - end - - context 'when option for locking Runner is provided' do - it 'creates runner' do - post api('/runners'), token: registration_token, - locked: true - - expect(response).to have_http_status 201 - expect(Ci::Runner.first.locked).to be true - end - end - - %w(name version revision platform architecture).each do |param| - context "when info parameter '#{param}' info is present" do - let(:value) { "#{param}_value" } - - it %q(updates provided Runner's parameter) do - post api('/runners'), token: registration_token, - info: { param => value } - - expect(response).to have_http_status 201 - expect(Ci::Runner.first.read_attribute(param.to_sym)).to eq(value) - end - end - end - end - - describe 'DELETE /api/v4/runners' do - context 'when no token is provided' do - it 'returns 400 error' do - delete api('/runners') - expect(response).to have_http_status 400 - end - end - - context 'when invalid token is provided' do - it 'returns 403 error' do - delete api('/runners'), token: 'invalid' - expect(response).to have_http_status 403 - end - end - - context 'when valid token is provided' do - let(:runner) { create(:ci_runner) } - - it 'deletes Runner' do - delete api('/runners'), token: runner.token - expect(response).to have_http_status 200 - expect(Ci::Runner.count).to eq(0) - end - end - end - end -end diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb new file mode 100644 index 00000000000..73e82647ca0 --- /dev/null +++ b/spec/requests/api/runner_spec.rb @@ -0,0 +1,148 @@ +require 'spec_helper' + +describe API::Runner do + include ApiHelpers + include StubGitlabCalls + + let(:registration_token) { 'abcdefg123456' } + + before do + stub_gitlab_calls + stub_application_setting(runners_registration_token: registration_token) + end + + describe '/api/v4/runners' do + describe 'POST /api/v4/runners' do + context 'when no token is provided' do + it 'returns 400 error' do + post api('/runners') + expect(response).to have_http_status 400 + end + end + + context 'when invalid token is provided' do + it 'returns 403 error' do + post api('/runners'), token: 'invalid' + expect(response).to have_http_status 403 + end + end + + context 'when valid token is provided' do + it 'creates runner with default values' do + post api('/runners'), token: registration_token + + runner = Ci::Runner.first + + expect(response).to have_http_status 201 + expect(json_response['id']).to eq(runner.id) + expect(json_response['token']).to eq(runner.token) + expect(runner.run_untagged).to be true + end + + context 'when project token is used' do + let(:project) { create(:empty_project) } + + it 'creates runner' do + post api('/runners'), token: project.runners_token + + expect(response).to have_http_status 201 + expect(project.runners.size).to eq(1) + end + end + end + + context 'when runner description is provided' do + it 'creates runner' do + post api('/runners'), token: registration_token, + description: 'server.hostname' + + expect(response).to have_http_status 201 + expect(Ci::Runner.first.description).to eq('server.hostname') + end + end + + context 'when runner tags are provided' do + it 'creates runner' do + post api('/runners'), token: registration_token, + tag_list: 'tag1, tag2' + + expect(response).to have_http_status 201 + expect(Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2)) + end + end + + context 'when option for running untagged jobs is provided' do + context 'when tags are provided' do + it 'creates runner' do + post api('/runners'), token: registration_token, + run_untagged: false, + tag_list: ['tag'] + + expect(response).to have_http_status 201 + expect(Ci::Runner.first.run_untagged).to be false + expect(Ci::Runner.first.tag_list.sort).to eq(['tag']) + end + end + + context 'when tags are not provided' do + it 'returns 404 error' do + post api('/runners'), token: registration_token, + run_untagged: false + + expect(response).to have_http_status 404 + end + end + end + + context 'when option for locking Runner is provided' do + it 'creates runner' do + post api('/runners'), token: registration_token, + locked: true + + expect(response).to have_http_status 201 + expect(Ci::Runner.first.locked).to be true + end + end + + %w(name version revision platform architecture).each do |param| + context "when info parameter '#{param}' info is present" do + let(:value) { "#{param}_value" } + + it %q(updates provided Runner's parameter) do + post api('/runners'), token: registration_token, + info: { param => value } + + expect(response).to have_http_status 201 + expect(Ci::Runner.first.read_attribute(param.to_sym)).to eq(value) + end + end + end + end + + describe 'DELETE /api/v4/runners' do + context 'when no token is provided' do + it 'returns 400 error' do + delete api('/runners') + expect(response).to have_http_status 400 + end + end + + context 'when invalid token is provided' do + it 'returns 403 error' do + delete api('/runners'), token: 'invalid' + expect(response).to have_http_status 403 + end + end + + context 'when valid token is provided' do + let(:runner) { create(:ci_runner) } + + it 'deletes Runner' do + delete api('/runners'), token: runner.token + expect(response).to have_http_status 200 + expect(Ci::Runner.count).to eq(0) + end + end + end + end +end -- cgit v1.2.3 From 297dc70158f905fef4557d1ee6510bcf459a08a9 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Thu, 2 Feb 2017 15:04:02 +0100 Subject: Create MM team for GitLab group --- app/models/group.rb | 1 + app/services/groups/create_service.rb | 5 ++++ app/views/groups/new.html.haml | 5 ++++ app/workers/mattermost/create_team_worker.rb | 28 ++++++++++++++++++++++ db/migrate/20170120131253_create_chat_teams.rb | 17 +++++++++++++ db/schema.rb | 11 +++++++++ lib/mattermost/session.rb | 2 +- lib/mattermost/team.rb | 5 ++++ spec/models/chat_team_spec.rb | 10 ++++++++ spec/models/group_spec.rb | 1 + spec/workers/mattermost/create_team_worker_spec.rb | 21 ++++++++++++++++ 11 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 app/workers/mattermost/create_team_worker.rb create mode 100644 db/migrate/20170120131253_create_chat_teams.rb create mode 100644 spec/models/chat_team_spec.rb create mode 100644 spec/workers/mattermost/create_team_worker_spec.rb diff --git a/app/models/group.rb b/app/models/group.rb index 240a17f1dc1..27dc4e8f319 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -21,6 +21,7 @@ class Group < Namespace has_many :shared_projects, through: :project_group_links, source: :project has_many :notification_settings, dependent: :destroy, as: :source has_many :labels, class_name: 'GroupLabel' + has_one :chat_team, dependent: :destroy validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :visibility_level_allowed_by_projects diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index febeb661fb5..209fd15f481 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -22,6 +22,11 @@ module Groups @group.name ||= @group.path.dup @group.save @group.add_owner(current_user) + + if params[:create_chat_team] && Gitlab.config.mattermost.enabled + Mattermost::CreateTeamWorker.perform_async(@group.id, current_user.id) + end + @group end end diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 38d63fd9acc..8e1513b151b 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -16,6 +16,11 @@ = render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group + .form-group + = f.label :create_chat_team, "Create Mattermost Team", class: 'control-label' + .col-sm-10 + = f.check_box :chat_team + .form-group .col-sm-offset-2.col-sm-10 = render 'shared/group_tips' diff --git a/app/workers/mattermost/create_team_worker.rb b/app/workers/mattermost/create_team_worker.rb new file mode 100644 index 00000000000..69486569cbf --- /dev/null +++ b/app/workers/mattermost/create_team_worker.rb @@ -0,0 +1,28 @@ +module Mattermost + class CreateTeamWorker + include Sidekiq::Worker + include DedicatedSidekiqQueue + + def perform(group_id, current_user_id, options = {}) + @group = Group.find(group_id) + current_user = User.find(current_user_id) + + options = team_params.merge(options) + + # The user that creates the team will be Team Admin + response = Mattermost::Team.new(current_user).create(options) + + ChatTeam.create!(namespace: @group, name: response['name'], team_id: response['id']) + end + + private + + def team_params + { + name: @group.path[0..59], + display_name: @group.name[0..59], + type: @group.public? ? 'O' : 'I' # Open vs Invite-only + } + end + end +end diff --git a/db/migrate/20170120131253_create_chat_teams.rb b/db/migrate/20170120131253_create_chat_teams.rb new file mode 100644 index 00000000000..6476c239152 --- /dev/null +++ b/db/migrate/20170120131253_create_chat_teams.rb @@ -0,0 +1,17 @@ +class CreateChatTeams < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + create_table :chat_teams do |t| + t.integer :namespace_id, index: true + t.string :team_id + t.string :name + + t.timestamps null: false + end + + add_foreign_key :chat_teams, :namespaces, on_delete: :cascade + end +end diff --git a/db/schema.rb b/db/schema.rb index 52672406ec6..0ae208fb5e4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -171,6 +171,16 @@ ActiveRecord::Schema.define(version: 20170214111112) do add_index "chat_names", ["service_id", "team_id", "chat_id"], name: "index_chat_names_on_service_id_and_team_id_and_chat_id", unique: true, using: :btree add_index "chat_names", ["user_id", "service_id"], name: "index_chat_names_on_user_id_and_service_id", unique: true, using: :btree + create_table "chat_teams", force: :cascade do |t| + t.integer "namespace_id" + t.string "team_id" + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "chat_teams", ["namespace_id"], name: "index_chat_teams_on_namespace_id", using: :btree + create_table "ci_application_settings", force: :cascade do |t| t.boolean "all_broken_builds" t.boolean "add_pusher" @@ -1330,6 +1340,7 @@ ActiveRecord::Schema.define(version: 20170214111112) do add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree add_foreign_key "boards", "projects" + add_foreign_key "chat_teams", "namespaces", on_delete: :cascade add_foreign_key "issue_metrics", "issues", on_delete: :cascade add_foreign_key "label_priorities", "labels", on_delete: :cascade add_foreign_key "label_priorities", "projects", on_delete: :cascade diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index 377cb7b1021..d4b4ba97f8c 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -153,7 +153,7 @@ module Mattermost yield rescue HTTParty::Error => e raise Mattermost::ConnectionError.new(e.message) - rescue Errno::ECONNREFUSED + rescue Errno::ECONNREFUSED => e raise Mattermost::ConnectionError.new(e.message) end end diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb index 09dfd082b3a..9e80f7f8571 100644 --- a/lib/mattermost/team.rb +++ b/lib/mattermost/team.rb @@ -1,7 +1,12 @@ module Mattermost class Team < Client + # Returns **all** teams for an admin def all session_get('/api/v3/teams/all') end + + def create(params) + session_post('/api/v3/teams/create', body: params.to_json) + end end end diff --git a/spec/models/chat_team_spec.rb b/spec/models/chat_team_spec.rb new file mode 100644 index 00000000000..37a22f5cd6d --- /dev/null +++ b/spec/models/chat_team_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper' + +describe ChatTeam, type: :model do + # Associations + it { is_expected.to belong_to(:group) } + + # Fields + it { is_expected.to respond_to(:name) } + it { is_expected.to respond_to(:team_id) } +end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index a4e6eb4e3a6..8cfc2085458 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -13,6 +13,7 @@ describe Group, models: true do it { is_expected.to have_many(:shared_projects).through(:project_group_links) } it { is_expected.to have_many(:notification_settings).dependent(:destroy) } it { is_expected.to have_many(:labels).class_name('GroupLabel') } + it { is_expected.to have_one(:chat_team) } describe '#members & #requesters' do let(:requester) { create(:user) } diff --git a/spec/workers/mattermost/create_team_worker_spec.rb b/spec/workers/mattermost/create_team_worker_spec.rb new file mode 100644 index 00000000000..a6b87967ca2 --- /dev/null +++ b/spec/workers/mattermost/create_team_worker_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Mattermost::CreateTeamWorker do + let(:group) { create(:group, path: 'path', name: 'name') } + let(:admin) { create(:admin) } + + describe '.perform' do + subject { described_class.new.perform(group.id, admin.id) } + + before do + allow_any_instance_of(Mattermost::Team). + to receive(:create). + with(name: "path", display_name: "name", type: "O"). + and_return('name' => 'my team', 'id' => 'sjfkdlwkdjfwlkfjwf') + end + + it 'creates a new chat team' do + expect { subject }.to change { ChatTeam.count }.from(0).to(1) + end + end +end -- cgit v1.2.3 From 1a0e54b32d43e2a3de88c20037bd167b1f8563d6 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Fri, 3 Feb 2017 11:29:56 +0100 Subject: Add tests for Mattermost team creation --- app/controllers/groups_controller.rb | 1 + app/models/chat_team.rb | 5 ++++ app/services/groups/create_service.rb | 4 ++- app/workers/mattermost/create_team_worker.rb | 25 ++++++++----------- lib/mattermost/client.rb | 2 +- lib/mattermost/team.rb | 18 ++++++++++++-- spec/models/chat_team_spec.rb | 2 +- spec/services/groups/create_service_spec.rb | 17 ++++++++++--- spec/workers/mattermost/create_team_worker_spec.rb | 29 ++++++++++++++++------ 9 files changed, 72 insertions(+), 31 deletions(-) create mode 100644 app/models/chat_team.rb diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 7ed54479599..63b37fb5eb3 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -143,6 +143,7 @@ class GroupsController < Groups::ApplicationController :share_with_group_lock, :visibility_level, :parent_id + :create_chat_team ] end diff --git a/app/models/chat_team.rb b/app/models/chat_team.rb new file mode 100644 index 00000000000..7952141a0d6 --- /dev/null +++ b/app/models/chat_team.rb @@ -0,0 +1,5 @@ +class ChatTeam < ActiveRecord::Base + validates :team_id, presence: true + + belongs_to :namespace +end diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index 209fd15f481..aabd3c4bd05 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -5,6 +5,8 @@ module Groups end def execute + create_chat_team = params.delete(:create_chat_team) + @group = Group.new(params) unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level]) @@ -23,7 +25,7 @@ module Groups @group.save @group.add_owner(current_user) - if params[:create_chat_team] && Gitlab.config.mattermost.enabled + if create_chat_team && Gitlab.config.mattermost.enabled Mattermost::CreateTeamWorker.perform_async(@group.id, current_user.id) end diff --git a/app/workers/mattermost/create_team_worker.rb b/app/workers/mattermost/create_team_worker.rb index 69486569cbf..168bdc7454d 100644 --- a/app/workers/mattermost/create_team_worker.rb +++ b/app/workers/mattermost/create_team_worker.rb @@ -3,26 +3,21 @@ module Mattermost include Sidekiq::Worker include DedicatedSidekiqQueue + sidekiq_options retry: 5 + + # Add 5 seconds so the first retry isn't 1 second later + sidekiq_retry_in do |count| + 5 + 5 ** n + end + def perform(group_id, current_user_id, options = {}) - @group = Group.find(group_id) + group = Group.find(group_id) current_user = User.find(current_user_id) - options = team_params.merge(options) - # The user that creates the team will be Team Admin - response = Mattermost::Team.new(current_user).create(options) - - ChatTeam.create!(namespace: @group, name: response['name'], team_id: response['id']) - end - - private + response = Mattermost::Team.new(current_user).create(group, options) - def team_params - { - name: @group.path[0..59], - display_name: @group.name[0..59], - type: @group.public? ? 'O' : 'I' # Open vs Invite-only - } + ChatTeam.create(namespace: group, name: response['name'], team_id: response['id']) end end end diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb index e55c0d6ac49..29b10e2afce 100644 --- a/lib/mattermost/client.rb +++ b/lib/mattermost/client.rb @@ -26,7 +26,7 @@ module Mattermost def session_get(path, options = {}) with_session do |session| - get(session, path, options) + get(session, path, options) end end diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb index 9e80f7f8571..d5648f7167f 100644 --- a/lib/mattermost/team.rb +++ b/lib/mattermost/team.rb @@ -5,8 +5,22 @@ module Mattermost session_get('/api/v3/teams/all') end - def create(params) - session_post('/api/v3/teams/create', body: params.to_json) + # Creates a team on the linked Mattermost instance, the team admin will be the + # `current_user` passed to the Mattermost::Client instance + def create(group, params) + session_post('/api/v3/teams/create', body: new_team_params(group, params).to_json) + end + + private + + MATTERMOST_TEAM_LENGTH_MAX = 59 + + def new_team_params(group, options) + { + name: group.path[0..MATTERMOST_TEAM_LENGTH_MAX], + display_name: group.name[0..MATTERMOST_TEAM_LENGTH_MAX], + type: group.public? ? 'O' : 'I' # Open vs Invite-only + }.merge(options) end end end diff --git a/spec/models/chat_team_spec.rb b/spec/models/chat_team_spec.rb index 37a22f5cd6d..1aab161ec13 100644 --- a/spec/models/chat_team_spec.rb +++ b/spec/models/chat_team_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe ChatTeam, type: :model do # Associations - it { is_expected.to belong_to(:group) } + it { is_expected.to belong_to(:namespace) } # Fields it { is_expected.to respond_to(:name) } diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index 14717a7455d..235fa5c5188 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -4,11 +4,11 @@ describe Groups::CreateService, '#execute', services: true do let!(:user) { create(:user) } let!(:group_params) { { path: "group_path", visibility_level: Gitlab::VisibilityLevel::PUBLIC } } + subject { service.execute } + describe 'visibility level restrictions' do let!(:service) { described_class.new(user, group_params) } - subject { service.execute } - context "create groups without restricted visibility level" do it { is_expected.to be_persisted } end @@ -24,8 +24,6 @@ describe Groups::CreateService, '#execute', services: true do let!(:group) { create(:group) } let!(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) } - subject { service.execute } - context 'as group owner' do before { group.add_owner(user) } @@ -40,4 +38,15 @@ describe Groups::CreateService, '#execute', services: true do end end end + + describe 'creating a mattermost team' do + let!(:params) { group_params.merge(create_chat_team: true) } + let!(:service) { described_class.new(user, params) } + + it 'queues a background job' do + expect(Mattermost::CreateTeamWorker).to receive(:perform_async) + + subject + end + end end diff --git a/spec/workers/mattermost/create_team_worker_spec.rb b/spec/workers/mattermost/create_team_worker_spec.rb index a6b87967ca2..d3230f535c2 100644 --- a/spec/workers/mattermost/create_team_worker_spec.rb +++ b/spec/workers/mattermost/create_team_worker_spec.rb @@ -7,15 +7,30 @@ describe Mattermost::CreateTeamWorker do describe '.perform' do subject { described_class.new.perform(group.id, admin.id) } - before do - allow_any_instance_of(Mattermost::Team). - to receive(:create). - with(name: "path", display_name: "name", type: "O"). - and_return('name' => 'my team', 'id' => 'sjfkdlwkdjfwlkfjwf') + context 'succesfull request to mattermost' do + before do + allow_any_instance_of(Mattermost::Team). + to receive(:create). + with(group, {}). + and_return('name' => 'my team', 'id' => 'sjfkdlwkdjfwlkfjwf') + end + + it 'creates a new chat team' do + expect { subject }.to change { ChatTeam.count }.from(0).to(1) + end end - it 'creates a new chat team' do - expect { subject }.to change { ChatTeam.count }.from(0).to(1) + context 'connection trouble' do + before do + allow_any_instance_of(Mattermost::Team). + to receive(:create). + with(group, {}). + and_raise(Mattermost::ClientError.new('Undefined error')) + end + + it 'does not rescue the error' do + expect { subject }.to raise_error(Mattermost::ClientError) + end end end end -- cgit v1.2.3 From 3a3d956a8b6d031cc71963b14f5c70f78bef24a4 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 6 Feb 2017 16:10:32 +0000 Subject: Adds Mattermost label and icon to new group form --- app/assets/stylesheets/pages/groups.scss | 6 ++++++ app/views/groups/new.html.haml | 10 ++++++++-- app/views/shared/icons/_icon_mattermost.svg | 1 + 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 app/views/shared/icons/_icon_mattermost.svg diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index d377526e655..7db6af3531a 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -73,3 +73,9 @@ } } } + +.mattermost-icon svg{ + width: 16px; + height: 16px; + vertical-align: text-bottom; +} diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 8e1513b151b..29b5d2ab032 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -17,9 +17,15 @@ = render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group .form-group - = f.label :create_chat_team, "Create Mattermost Team", class: 'control-label' + = f.label :create_chat_team, class: 'control-label' do + %span.mattermost-icon + = custom_icon('icon_mattermost') + Mattermost .col-sm-10 - = f.check_box :chat_team + .checkbox + = f.label :chat_team do + = f.check_box :chat_team + Link the group to a new or existing Mattermost team .form-group .col-sm-offset-2.col-sm-10 diff --git a/app/views/shared/icons/_icon_mattermost.svg b/app/views/shared/icons/_icon_mattermost.svg new file mode 100644 index 00000000000..59f4b598712 --- /dev/null +++ b/app/views/shared/icons/_icon_mattermost.svg @@ -0,0 +1 @@ + \ No newline at end of file -- cgit v1.2.3 From 59136d4ab6d13db74018ff301df31e70e45a3c27 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 6 Feb 2017 17:23:06 +0000 Subject: Adds html and css to add mattermost team name to group in new and edit --- app/assets/stylesheets/pages/groups.scss | 12 +++++++++++- app/views/groups/edit.html.haml | 20 ++++++++++++++++++++ app/views/groups/new.html.haml | 6 ++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 7db6af3531a..84d21e48463 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -74,8 +74,18 @@ } } -.mattermost-icon svg{ +.mattermost-icon svg { width: 16px; height: 16px; vertical-align: text-bottom; } + +.mattermost-team-name { + color: $gl-text-color-secondary; +} + +.mattermost-info { + display: block; + color: $gl-text-color-secondary; + margin-top: 10px; +} diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 2706e8692d1..38d498f0343 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -22,6 +22,26 @@ = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group + .form-group + = f.label :name, class: 'control-label' do + %span.mattermost-icon + = custom_icon('icon_mattermost') + Mattermost + .col-sm-10 + .checkbox + = f.label :name do + = f.check_box :name, checked: true + Link the group to a new or existing Mattermost team + + + - enabled = Gitlab.config.mattermost.enabled + - if enabled + .form-group + .col-sm-offset-2.col-sm-10 + = f.text_field :name, placeholder: "FILL WITH TEAM NAME", class: 'form-control mattermost-team-name' + %span.mattermost-info + Team URL: INSERT TEAM URL + .form-group .col-sm-offset-2.col-sm-10 = render 'shared/allow_request_access', form: f diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 29b5d2ab032..3cc4f6d6e48 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -27,6 +27,12 @@ = f.check_box :chat_team Link the group to a new or existing Mattermost team +- enabled = Gitlab.config.mattermost.enabled +- if enabled + .form-group + .col-sm-offset-2.col-sm-10 + = f.text_field :name, placeholder: 'Mattermost team name', class: 'form-control mattermost-team-name' + .form-group .col-sm-offset-2.col-sm-10 = render 'shared/group_tips' -- cgit v1.2.3 From b4244efaf1ab955e5900e87c3ec4e9465ba38bff Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 6 Feb 2017 17:30:05 +0000 Subject: Add newline at end of icon file --- app/views/groups/new.html.haml | 2 +- app/views/shared/icons/_icon_mattermost.svg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 3cc4f6d6e48..723071c7633 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -31,7 +31,7 @@ - if enabled .form-group .col-sm-offset-2.col-sm-10 - = f.text_field :name, placeholder: 'Mattermost team name', class: 'form-control mattermost-team-name' + = f.text_field :name, placeholder: 'Mattermost team name', class: 'form-control mattermost-team-name' .form-group .col-sm-offset-2.col-sm-10 diff --git a/app/views/shared/icons/_icon_mattermost.svg b/app/views/shared/icons/_icon_mattermost.svg index 59f4b598712..d1c541523ab 100644 --- a/app/views/shared/icons/_icon_mattermost.svg +++ b/app/views/shared/icons/_icon_mattermost.svg @@ -1 +1 @@ - \ No newline at end of file + -- cgit v1.2.3 From 8ddbc43576c1cebd652d6f3541574f0176794510 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Tue, 7 Feb 2017 08:24:57 +0100 Subject: Improve DRYness of views --- app/controllers/groups_controller.rb | 5 ++++- app/services/groups/create_service.rb | 4 +++- app/services/groups/update_service.rb | 5 +++++ app/views/groups/_create_chat_team.html.haml | 18 ++++++++++++++++++ app/views/groups/edit.html.haml | 20 +------------------- app/views/groups/new.html.haml | 17 +---------------- app/workers/mattermost/create_team_worker.rb | 9 +++++---- db/migrate/20170120131253_create_chat_teams.rb | 4 ++-- 8 files changed, 39 insertions(+), 43 deletions(-) create mode 100644 app/views/groups/_create_chat_team.html.haml diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 63b37fb5eb3..b2a18f0e65d 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -29,6 +29,7 @@ class GroupsController < Groups::ApplicationController end def create + byebug @group = Groups::CreateService.new(current_user, group_params).execute if @group.persisted? @@ -81,6 +82,7 @@ class GroupsController < Groups::ApplicationController end def update + byebug if Groups::UpdateService.new(@group, current_user, group_params).execute redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated." else @@ -143,7 +145,8 @@ class GroupsController < Groups::ApplicationController :share_with_group_lock, :visibility_level, :parent_id - :create_chat_team + :create_chat_team, + :chat_team_name ] end diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index aabd3c4bd05..13d1b545498 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -6,6 +6,7 @@ module Groups def execute create_chat_team = params.delete(:create_chat_team) + team_name = params.delete(:chat_team_name) @group = Group.new(params) @@ -26,7 +27,8 @@ module Groups @group.add_owner(current_user) if create_chat_team && Gitlab.config.mattermost.enabled - Mattermost::CreateTeamWorker.perform_async(@group.id, current_user.id) + options = team_name ? { name: team_name } : {} + Mattermost::CreateTeamWorker.perform_async(@group.id, current_user.id, options) end @group diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb index 4e878ec556a..aff42ad598c 100644 --- a/app/services/groups/update_service.rb +++ b/app/services/groups/update_service.rb @@ -1,6 +1,11 @@ module Groups class UpdateService < Groups::BaseService def execute + if params.delete(:create_chat_team) == '1' + chat_name = params[:chat_team_name] + options = chat_name ? { name: chat_name } : {} + end + # check that user is allowed to set specified visibility_level new_visibility = params[:visibility_level] if new_visibility && new_visibility.to_i != group.visibility_level diff --git a/app/views/groups/_create_chat_team.html.haml b/app/views/groups/_create_chat_team.html.haml new file mode 100644 index 00000000000..b2b5a2090f9 --- /dev/null +++ b/app/views/groups/_create_chat_team.html.haml @@ -0,0 +1,18 @@ +.form-group + = f.label :name, class: 'control-label' do + %span.mattermost-icon + = custom_icon('icon_mattermost') + Mattermost + .col-sm-10 + .checkbox + = f.label :name do + = f.check_box :create_chat_team, checked: true + Link the group to a new Mattermost team + +.form-group + = f.label :chat_team, class: 'control-label' do + Chat Team name + .col-sm-10 + = f.text_field :chat_team, placeholder: @group.name, class: 'form-control mattermost-team-name' + Leave blank to match your group name + diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 38d498f0343..c904b25fe94 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -22,25 +22,7 @@ = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group - .form-group - = f.label :name, class: 'control-label' do - %span.mattermost-icon - = custom_icon('icon_mattermost') - Mattermost - .col-sm-10 - .checkbox - = f.label :name do - = f.check_box :name, checked: true - Link the group to a new or existing Mattermost team - - - - enabled = Gitlab.config.mattermost.enabled - - if enabled - .form-group - .col-sm-offset-2.col-sm-10 - = f.text_field :name, placeholder: "FILL WITH TEAM NAME", class: 'form-control mattermost-team-name' - %span.mattermost-info - Team URL: INSERT TEAM URL + = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled .form-group .col-sm-offset-2.col-sm-10 diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 723071c7633..000c7af2326 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -16,22 +16,7 @@ = render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group - .form-group - = f.label :create_chat_team, class: 'control-label' do - %span.mattermost-icon - = custom_icon('icon_mattermost') - Mattermost - .col-sm-10 - .checkbox - = f.label :chat_team do - = f.check_box :chat_team - Link the group to a new or existing Mattermost team - -- enabled = Gitlab.config.mattermost.enabled -- if enabled - .form-group - .col-sm-offset-2.col-sm-10 - = f.text_field :name, placeholder: 'Mattermost team name', class: 'form-control mattermost-team-name' + = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled .form-group .col-sm-offset-2.col-sm-10 diff --git a/app/workers/mattermost/create_team_worker.rb b/app/workers/mattermost/create_team_worker.rb index 168bdc7454d..95b63dac8ab 100644 --- a/app/workers/mattermost/create_team_worker.rb +++ b/app/workers/mattermost/create_team_worker.rb @@ -7,17 +7,18 @@ module Mattermost # Add 5 seconds so the first retry isn't 1 second later sidekiq_retry_in do |count| - 5 + 5 ** n + 5 + 5**n end def perform(group_id, current_user_id, options = {}) - group = Group.find(group_id) - current_user = User.find(current_user_id) + group = Group.find_by(id: group_id) + current_user = User.find_by(id: current_user_id) + return unless group && current_user # The user that creates the team will be Team Admin response = Mattermost::Team.new(current_user).create(group, options) - ChatTeam.create(namespace: group, name: response['name'], team_id: response['id']) + group.create_chat_team(name: response['name'], team_id: response['id']) end end end diff --git a/db/migrate/20170120131253_create_chat_teams.rb b/db/migrate/20170120131253_create_chat_teams.rb index 6476c239152..8f76b43960b 100644 --- a/db/migrate/20170120131253_create_chat_teams.rb +++ b/db/migrate/20170120131253_create_chat_teams.rb @@ -5,13 +5,13 @@ class CreateChatTeams < ActiveRecord::Migration def change create_table :chat_teams do |t| - t.integer :namespace_id, index: true + t.integer :group_id, index: true t.string :team_id t.string :name t.timestamps null: false end - add_foreign_key :chat_teams, :namespaces, on_delete: :cascade + add_foreign_key :chat_teams, :groups, on_delete: :cascade end end -- cgit v1.2.3 From c3a1c47a460342ff670bbf4a83e27174dd45f0d2 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 7 Feb 2017 16:13:52 +0000 Subject: Fix form fields to bing to correct model --- app/views/groups/_create_chat_team.html.haml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/views/groups/_create_chat_team.html.haml b/app/views/groups/_create_chat_team.html.haml index b2b5a2090f9..c06a7a23346 100644 --- a/app/views/groups/_create_chat_team.html.haml +++ b/app/views/groups/_create_chat_team.html.haml @@ -1,18 +1,18 @@ .form-group - = f.label :name, class: 'control-label' do + = f.label :create_chat_team, class: 'control-label' do %span.mattermost-icon = custom_icon('icon_mattermost') Mattermost .col-sm-10 .checkbox - = f.label :name do - = f.check_box :create_chat_team, checked: true + = f.label :create_chat_team do + = f.check_box :create_chat_team, checked: @group.chat_team Link the group to a new Mattermost team .form-group = f.label :chat_team, class: 'control-label' do Chat Team name .col-sm-10 - = f.text_field :chat_team, placeholder: @group.name, class: 'form-control mattermost-team-name' - Leave blank to match your group name - + = f.text_field :chat_team, placeholder: @group.chat_team, class: 'form-control mattermost-team-name' + %small + Leave blank to match your group name -- cgit v1.2.3 From 1f66d255ee0aee23a829640a5638eeac485f4883 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 8 Feb 2017 10:40:15 +0000 Subject: Sends checkbox parameters as boolean --- app/views/groups/_create_chat_team.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/groups/_create_chat_team.html.haml b/app/views/groups/_create_chat_team.html.haml index c06a7a23346..1e702c4f20f 100644 --- a/app/views/groups/_create_chat_team.html.haml +++ b/app/views/groups/_create_chat_team.html.haml @@ -6,7 +6,7 @@ .col-sm-10 .checkbox = f.label :create_chat_team do - = f.check_box :create_chat_team, checked: @group.chat_team + = f.check_box(:create_chat_team, { checked: @group.chat_team }, 'true', 'false') Link the group to a new Mattermost team .form-group -- cgit v1.2.3 From 3acf4fbe3b6e96ac4bc438e24030fde076832b51 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 16 Feb 2017 11:00:35 +0000 Subject: Update schema --- db/schema.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/db/schema.rb b/db/schema.rb index 52672406ec6..b11792d14f5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -111,6 +111,7 @@ ActiveRecord::Schema.define(version: 20170214111112) do t.boolean "plantuml_enabled" t.integer "max_pages_size", default: 100, null: false t.integer "terminal_max_session_time", default: 0, null: false + t.string "default_artifacts_expire_in", limit: 255 end create_table "audit_events", force: :cascade do |t| -- cgit v1.2.3 From 46082f4b652cac097c72354b394881a128fd3a5f Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 16 Feb 2017 19:27:37 +0800 Subject: Use static error message and don't give booleans in validation. Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9219#note_23437431 https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9219#note_23437868 --- app/models/application_setting.rb | 13 ++++--------- spec/models/application_setting_spec.rb | 4 ++++ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 6d5f02a1011..e865da0ce7a 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -306,17 +306,12 @@ class ApplicationSetting < ActiveRecord::Base end def check_default_artifacts_expire_in - return true unless default_artifacts_expire_in - - if ChronicDuration.parse(default_artifacts_expire_in).nil? - errors.add(:default_artifacts_expire_in, + if default_artifacts_expire_in && + ChronicDuration.parse(default_artifacts_expire_in).nil? + errors.add(:default_artifacts_expiration, "can't be 0. Leave it blank for no expiration") - false - else - true end rescue ChronicDuration::DurationParseError => e - errors.add(:default_artifacts_expire_in, ": #{e.message}") - false + errors.add(:default_artifacts_expiration, "is invalid") end end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 9629f858609..719eda9b5b3 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -34,12 +34,16 @@ describe ApplicationSetting, models: true do setting.update(default_artifacts_expire_in: 'a') expect(setting).to be_invalid + expect(setting.errors.messages) + .to have_key(:default_artifacts_expiration) end it 'does not allow 0' do setting.update(default_artifacts_expire_in: '0') expect(setting).to be_invalid + expect(setting.errors.messages) + .to have_key(:default_artifacts_expiration) end it 'sets the value if it is valid' do -- cgit v1.2.3 From 36afa43e4da5c7e5804880401f2ef52c1119ba7c Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 16 Feb 2017 19:33:30 +0800 Subject: Use squish to also compact the string Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9219#note_23436980 --- app/models/application_setting.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index e865da0ce7a..3a2f69fde97 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -271,7 +271,7 @@ class ApplicationSetting < ActiveRecord::Base def default_artifacts_expire_in=(value) if value.present? - super(value.strip) + super(value.squish) else super(nil) end -- cgit v1.2.3 From 9096647ff66029e188562ff36331342220ab1c1b Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 16 Feb 2017 19:34:50 +0800 Subject: Wordings change, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9219#note_23437712 --- doc/user/admin_area/settings/continuous_integration.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md index 81247786173..d7b598b7506 100644 --- a/doc/user/admin_area/settings/continuous_integration.md +++ b/doc/user/admin_area/settings/continuous_integration.md @@ -23,7 +23,8 @@ that this setting is set for each job. The default expiration time of the [build artifacts][art-yml] can be set in the Admin area of your GitLab instance. The syntax of duration is described in [artifacts:expire_in][duration-syntax]. The default is `30 days`. Note that -this setting is set for each job. Leave it blank for no default expiration. +this setting is set for each job. Leave it blank if you don't want to set +default expiration time. 1. Go to **Admin area > Settings** (`/admin/application_settings`). -- cgit v1.2.3 From 37cc3aaefb65e775bd3baa93dd7dec218a76a23c Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 16 Feb 2017 22:29:29 +0800 Subject: The exception was no longer used --- app/models/application_setting.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 3a2f69fde97..d64a847d487 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -311,7 +311,7 @@ class ApplicationSetting < ActiveRecord::Base errors.add(:default_artifacts_expiration, "can't be 0. Leave it blank for no expiration") end - rescue ChronicDuration::DurationParseError => e + rescue ChronicDuration::DurationParseError errors.add(:default_artifacts_expiration, "is invalid") end end -- cgit v1.2.3 From eede4ab1a2509ef4aa14d21527386224c4116adc Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 16 Feb 2017 23:40:13 +0800 Subject: 0 for unlimited, disallow blank, feedback: https://gitlab.com/gitlab-org/gitlab-ce/issues/27762#note_23520780 --- app/models/application_setting.rb | 16 ++++--------- app/models/ci/build.rb | 2 +- .../admin/application_settings/_form.html.haml | 5 +++-- ...artifacts_expiration_to_application_settings.rb | 4 ++-- db/schema.rb | 4 ++-- .../admin_area/settings/continuous_integration.md | 8 +++---- spec/models/application_setting_spec.rb | 26 ++++++++++++---------- spec/models/ci/build_spec.rb | 6 +++++ 8 files changed, 36 insertions(+), 35 deletions(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index d64a847d487..36832185b6f 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -269,14 +269,6 @@ class ApplicationSetting < ActiveRecord::Base self.repository_storages = [value] end - def default_artifacts_expire_in=(value) - if value.present? - super(value.squish) - else - super(nil) - end - end - # Choose one of the available repository storage options. Currently all have # equal weighting. def pick_repository_storage @@ -306,10 +298,10 @@ class ApplicationSetting < ActiveRecord::Base end def check_default_artifacts_expire_in - if default_artifacts_expire_in && - ChronicDuration.parse(default_artifacts_expire_in).nil? - errors.add(:default_artifacts_expiration, - "can't be 0. Leave it blank for no expiration") + if default_artifacts_expire_in.blank? + errors.add(:default_artifacts_expiration, "is not presented") + else + ChronicDuration.parse(default_artifacts_expire_in) end rescue ChronicDuration::DurationParseError errors.add(:default_artifacts_expiration, "is invalid") diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 8c1b076c2d7..60655bf2ea7 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -509,7 +509,7 @@ module Ci def artifacts_expire_in=(value) self.artifacts_expire_at = if value - Time.now + ChronicDuration.parse(value) + ChronicDuration.parse(value)&.seconds&.from_now end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 3004f32f1f2..12b12fd248b 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -219,10 +219,11 @@ .col-sm-10 = f.text_field :default_artifacts_expire_in, class: 'form-control' .help-block - Set the default expiration time for each job's artifacts + Set the default expiration time for each job's artifacts. + 0 for unlimited. = surround '(', ')' do = link_to 'syntax', help_page_path('ci/yaml/README', anchor: 'artifactsexpire_in') - = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration-time') + = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration') - if Gitlab.config.registry.enabled %fieldset diff --git a/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb b/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb index 34905570739..e0e3ff8957a 100644 --- a/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb +++ b/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb @@ -5,7 +5,7 @@ class AddDefaultArtifactsExpirationToApplicationSettings < ActiveRecord::Migrati def change add_column :application_settings, - :default_artifacts_expire_in, - :string, null: true + :default_artifacts_expire_in, :string, + null: false, default: '0' end end diff --git a/db/schema.rb b/db/schema.rb index b11792d14f5..29ba7167bfa 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -111,7 +111,7 @@ ActiveRecord::Schema.define(version: 20170214111112) do t.boolean "plantuml_enabled" t.integer "max_pages_size", default: 100, null: false t.integer "terminal_max_session_time", default: 0, null: false - t.string "default_artifacts_expire_in", limit: 255 + t.string "default_artifacts_expire_in", default: '0', null: false end create_table "audit_events", force: :cascade do |t| @@ -1352,4 +1352,4 @@ ActiveRecord::Schema.define(version: 20170214111112) do add_foreign_key "timelogs", "merge_requests", name: "fk_timelogs_merge_requests_merge_request_id", 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 diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md index d7b598b7506..0fd165fc095 100644 --- a/doc/user/admin_area/settings/continuous_integration.md +++ b/doc/user/admin_area/settings/continuous_integration.md @@ -18,13 +18,13 @@ that this setting is set for each job. [art-yml]: ../../../administration/build_artifacts -## Default artifacts expiration time +## Default artifacts expiration -The default expiration time of the [build artifacts][art-yml] can be set in +The default expiration time of the [job artifacts][art-yml] can be set in the Admin area of your GitLab instance. The syntax of duration is described in [artifacts:expire_in][duration-syntax]. The default is `30 days`. Note that -this setting is set for each job. Leave it blank if you don't want to set -default expiration time. +this setting is set for each job. Set it to 0 if you don't want default +expiration. 1. Go to **Admin area > Settings** (`/admin/application_settings`). diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 719eda9b5b3..f8c38ed8569 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -30,20 +30,16 @@ describe ApplicationSetting, models: true do end describe 'default_artifacts_expire_in' do - it 'sets an error if it is invalid' do + it 'sets an error if it cannot parse' do setting.update(default_artifacts_expire_in: 'a') - expect(setting).to be_invalid - expect(setting.errors.messages) - .to have_key(:default_artifacts_expiration) + expect_invalid end - it 'does not allow 0' do - setting.update(default_artifacts_expire_in: '0') + it 'sets an error if it is blank' do + setting.update(default_artifacts_expire_in: ' ') - expect(setting).to be_invalid - expect(setting.errors.messages) - .to have_key(:default_artifacts_expiration) + expect_invalid end it 'sets the value if it is valid' do @@ -53,11 +49,17 @@ describe ApplicationSetting, models: true do expect(setting.default_artifacts_expire_in).to eq('30 days') end - it 'does not set it if it is blank' do - setting.update(default_artifacts_expire_in: ' ') + it 'sets the value if it is 0' do + setting.update(default_artifacts_expire_in: '0') expect(setting).to be_valid - expect(setting.default_artifacts_expire_in).to be_nil + expect(setting.default_artifacts_expire_in).to eq('0') + end + + def expect_invalid + expect(setting).to be_invalid + expect(setting.errors.messages) + .to have_key(:default_artifacts_expiration) end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 1e88dc9d468..a4edabfbc5b 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -166,6 +166,12 @@ describe Ci::Build, :models do is_expected.to be_nil end + + it 'when setting to 0' do + build.artifacts_expire_in = '0' + + is_expected.to be_nil + end end describe '#commit' do -- cgit v1.2.3 From 905fdfba927dcfa32b99d649521a52445bc6838f Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Fri, 17 Feb 2017 07:03:42 +1100 Subject: Add merge request count to each issue on issues list --- app/assets/images/icon-merge-request-unmerged.svg | 1 + app/assets/stylesheets/pages/issues.scss | 5 +++++ app/controllers/concerns/issuable_collections.rb | 20 ++++++++++++++------ app/controllers/projects/issues_controller.rb | 2 +- .../projects/merge_requests_controller.rb | 2 +- app/models/concerns/issuable.rb | 4 ++-- app/models/merge_requests_closing_issues.rb | 8 ++++++++ app/views/shared/_issuable_meta_data.html.haml | 6 ++++++ changelogs/unreleased/add_mr_info_to_issues_list.yml | 4 ++++ 9 files changed, 42 insertions(+), 10 deletions(-) create mode 100644 app/assets/images/icon-merge-request-unmerged.svg create mode 100644 changelogs/unreleased/add_mr_info_to_issues_list.yml diff --git a/app/assets/images/icon-merge-request-unmerged.svg b/app/assets/images/icon-merge-request-unmerged.svg new file mode 100644 index 00000000000..c4d8e65122d --- /dev/null +++ b/app/assets/images/icon-merge-request-unmerged.svg @@ -0,0 +1 @@ + diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 80b0c9493d8..b595480561b 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -10,6 +10,11 @@ .issue-labels { display: inline-block; } + + .icon-merge-request-unmerged { + height: 13px; + margin-bottom: 3px; + } } } diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index a6e158ebae6..d7d781cbe72 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -9,24 +9,32 @@ module IssuableCollections private - def issuable_meta_data(issuable_collection) + def issuable_meta_data(issuable_collection, collection_type) # map has to be used here since using pluck or select will # throw an error when ordering issuables by priority which inserts # a new order into the collection. # We cannot use reorder to not mess up the paginated collection. - issuable_ids = issuable_collection.map(&:id) - issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type) + issuable_ids = issuable_collection.map(&:id) + issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type) issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type) + issuable_merge_requests_count = + if collection_type == 'Issue' + MergeRequestsClosingIssues.count_for_collection(issuable_ids) + else + [] + end issuable_ids.each_with_object({}) do |id, issuable_meta| downvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.downvote? } - upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? } - notes = issuable_note_count.find { |notes| notes.noteable_id == id } + upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? } + notes = issuable_note_count.find { |notes| notes.noteable_id == id } + merge_requests = issuable_merge_requests_count.find { |mr| mr.issue_id == id } issuable_meta[id] = Issuable::IssuableMeta.new( upvotes.try(:count).to_i, downvotes.try(:count).to_i, - notes.try(:count).to_i + notes.try(:count).to_i, + merge_requests.try(:count).to_i ) end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 744a4af1c51..05056ad046a 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -26,7 +26,7 @@ class Projects::IssuesController < Projects::ApplicationController @collection_type = "Issue" @issues = issues_collection @issues = @issues.page(params[:page]) - @issuable_meta_data = issuable_meta_data(@issues) + @issuable_meta_data = issuable_meta_data(@issues, @collection_type) if @issues.out_of_range? && @issues.total_pages != 0 return redirect_to url_for(params.merge(page: @issues.total_pages)) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index c3e1760f168..8a7aeeaa96f 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -39,7 +39,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @collection_type = "MergeRequest" @merge_requests = merge_requests_collection @merge_requests = @merge_requests.page(params[:page]) - @issuable_meta_data = issuable_meta_data(@merge_requests) + @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type) if @merge_requests.out_of_range? && @merge_requests.total_pages != 0 return redirect_to url_for(params.merge(page: @merge_requests.total_pages)) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 5f53c48fc88..c9c6bd24d75 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -16,9 +16,9 @@ module Issuable include TimeTrackable # This object is used to gather issuable meta data for displaying - # upvotes, downvotes and notes count for issues and merge requests + # upvotes, downvotes, notes and closing merge requests count for issues and merge requests # lists avoiding n+1 queries and improving performance. - IssuableMeta = Struct.new(:upvotes, :downvotes, :notes_count) + IssuableMeta = Struct.new(:upvotes, :downvotes, :notes_count, :merge_requests_count) included do cache_markdown_field :title, pipeline: :single_line diff --git a/app/models/merge_requests_closing_issues.rb b/app/models/merge_requests_closing_issues.rb index ab597c37947..1ecdfd1dfdb 100644 --- a/app/models/merge_requests_closing_issues.rb +++ b/app/models/merge_requests_closing_issues.rb @@ -4,4 +4,12 @@ class MergeRequestsClosingIssues < ActiveRecord::Base validates :merge_request_id, uniqueness: { scope: :issue_id }, presence: true validates :issue_id, presence: true + + class << self + def count_for_collection(ids) + select('issue_id', 'COUNT(*) as count'). + group(:issue_id). + where(issue_id: ids) + end + end end diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml index 1264e524d86..66310da5cd6 100644 --- a/app/views/shared/_issuable_meta_data.html.haml +++ b/app/views/shared/_issuable_meta_data.html.haml @@ -2,6 +2,12 @@ - issue_votes = @issuable_meta_data[issuable.id] - upvotes, downvotes = issue_votes.upvotes, issue_votes.downvotes - issuable_url = @collection_type == "Issue" ? issue_path(issuable, anchor: 'notes') : merge_request_path(issuable, anchor: 'notes') +- issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count + +- if issuable_mr > 0 + %li + = image_tag('icon-merge-request-unmerged', class: 'icon-merge-request-unmerged') + = issuable_mr - if upvotes > 0 %li diff --git a/changelogs/unreleased/add_mr_info_to_issues_list.yml b/changelogs/unreleased/add_mr_info_to_issues_list.yml new file mode 100644 index 00000000000..8087aa6296c --- /dev/null +++ b/changelogs/unreleased/add_mr_info_to_issues_list.yml @@ -0,0 +1,4 @@ +--- +title: Add merge request count to each issue on issues list +merge_request: 9252 +author: blackst0ne -- cgit v1.2.3 From 204a0865e0e69d740abcaa85a689218140678a49 Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Fri, 17 Feb 2017 07:09:24 +1100 Subject: Add merge request count to each issue on issues list --- app/assets/images/icon-merge-request-unmerged.svg | 1 + app/assets/stylesheets/pages/issues.scss | 5 +++++ app/controllers/concerns/issuable_collections.rb | 20 ++++++++++++++------ app/controllers/projects/issues_controller.rb | 2 +- .../projects/merge_requests_controller.rb | 2 +- app/models/concerns/issuable.rb | 4 ++-- app/models/merge_requests_closing_issues.rb | 8 ++++++++ app/views/shared/_issuable_meta_data.html.haml | 6 ++++++ changelogs/unreleased/add_mr_info_to_issues_list.yml | 4 ++++ 9 files changed, 42 insertions(+), 10 deletions(-) create mode 100644 app/assets/images/icon-merge-request-unmerged.svg create mode 100644 changelogs/unreleased/add_mr_info_to_issues_list.yml diff --git a/app/assets/images/icon-merge-request-unmerged.svg b/app/assets/images/icon-merge-request-unmerged.svg new file mode 100644 index 00000000000..c4d8e65122d --- /dev/null +++ b/app/assets/images/icon-merge-request-unmerged.svg @@ -0,0 +1 @@ + diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 80b0c9493d8..b595480561b 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -10,6 +10,11 @@ .issue-labels { display: inline-block; } + + .icon-merge-request-unmerged { + height: 13px; + margin-bottom: 3px; + } } } diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index a6e158ebae6..d7d781cbe72 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -9,24 +9,32 @@ module IssuableCollections private - def issuable_meta_data(issuable_collection) + def issuable_meta_data(issuable_collection, collection_type) # map has to be used here since using pluck or select will # throw an error when ordering issuables by priority which inserts # a new order into the collection. # We cannot use reorder to not mess up the paginated collection. - issuable_ids = issuable_collection.map(&:id) - issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type) + issuable_ids = issuable_collection.map(&:id) + issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type) issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type) + issuable_merge_requests_count = + if collection_type == 'Issue' + MergeRequestsClosingIssues.count_for_collection(issuable_ids) + else + [] + end issuable_ids.each_with_object({}) do |id, issuable_meta| downvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.downvote? } - upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? } - notes = issuable_note_count.find { |notes| notes.noteable_id == id } + upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? } + notes = issuable_note_count.find { |notes| notes.noteable_id == id } + merge_requests = issuable_merge_requests_count.find { |mr| mr.issue_id == id } issuable_meta[id] = Issuable::IssuableMeta.new( upvotes.try(:count).to_i, downvotes.try(:count).to_i, - notes.try(:count).to_i + notes.try(:count).to_i, + merge_requests.try(:count).to_i ) end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 744a4af1c51..05056ad046a 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -26,7 +26,7 @@ class Projects::IssuesController < Projects::ApplicationController @collection_type = "Issue" @issues = issues_collection @issues = @issues.page(params[:page]) - @issuable_meta_data = issuable_meta_data(@issues) + @issuable_meta_data = issuable_meta_data(@issues, @collection_type) if @issues.out_of_range? && @issues.total_pages != 0 return redirect_to url_for(params.merge(page: @issues.total_pages)) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 63b5bcbb586..1d286ca62e5 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -39,7 +39,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @collection_type = "MergeRequest" @merge_requests = merge_requests_collection @merge_requests = @merge_requests.page(params[:page]) - @issuable_meta_data = issuable_meta_data(@merge_requests) + @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type) if @merge_requests.out_of_range? && @merge_requests.total_pages != 0 return redirect_to url_for(params.merge(page: @merge_requests.total_pages)) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 5f53c48fc88..c9c6bd24d75 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -16,9 +16,9 @@ module Issuable include TimeTrackable # This object is used to gather issuable meta data for displaying - # upvotes, downvotes and notes count for issues and merge requests + # upvotes, downvotes, notes and closing merge requests count for issues and merge requests # lists avoiding n+1 queries and improving performance. - IssuableMeta = Struct.new(:upvotes, :downvotes, :notes_count) + IssuableMeta = Struct.new(:upvotes, :downvotes, :notes_count, :merge_requests_count) included do cache_markdown_field :title, pipeline: :single_line diff --git a/app/models/merge_requests_closing_issues.rb b/app/models/merge_requests_closing_issues.rb index ab597c37947..1ecdfd1dfdb 100644 --- a/app/models/merge_requests_closing_issues.rb +++ b/app/models/merge_requests_closing_issues.rb @@ -4,4 +4,12 @@ class MergeRequestsClosingIssues < ActiveRecord::Base validates :merge_request_id, uniqueness: { scope: :issue_id }, presence: true validates :issue_id, presence: true + + class << self + def count_for_collection(ids) + select('issue_id', 'COUNT(*) as count'). + group(:issue_id). + where(issue_id: ids) + end + end end diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml index 1264e524d86..66310da5cd6 100644 --- a/app/views/shared/_issuable_meta_data.html.haml +++ b/app/views/shared/_issuable_meta_data.html.haml @@ -2,6 +2,12 @@ - issue_votes = @issuable_meta_data[issuable.id] - upvotes, downvotes = issue_votes.upvotes, issue_votes.downvotes - issuable_url = @collection_type == "Issue" ? issue_path(issuable, anchor: 'notes') : merge_request_path(issuable, anchor: 'notes') +- issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count + +- if issuable_mr > 0 + %li + = image_tag('icon-merge-request-unmerged', class: 'icon-merge-request-unmerged') + = issuable_mr - if upvotes > 0 %li diff --git a/changelogs/unreleased/add_mr_info_to_issues_list.yml b/changelogs/unreleased/add_mr_info_to_issues_list.yml new file mode 100644 index 00000000000..8087aa6296c --- /dev/null +++ b/changelogs/unreleased/add_mr_info_to_issues_list.yml @@ -0,0 +1,4 @@ +--- +title: Add merge request count to each issue on issues list +merge_request: 9252 +author: blackst0ne -- cgit v1.2.3 From ffd3583486f4772c8aec58b2750619d889d18d1b Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Fri, 17 Feb 2017 08:30:00 +1100 Subject: Added second parameter to @issuable_meta_data variables --- app/controllers/concerns/issues_action.rb | 2 +- app/controllers/concerns/merge_requests_action.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb index fb5edb34370..b17c138d5c7 100644 --- a/app/controllers/concerns/issues_action.rb +++ b/app/controllers/concerns/issues_action.rb @@ -10,7 +10,7 @@ module IssuesAction .page(params[:page]) @collection_type = "Issue" - @issuable_meta_data = issuable_meta_data(@issues) + @issuable_meta_data = issuable_meta_data(@issues, @collection_type) respond_to do |format| format.html diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb index 6229759dcf1..d3c8e4888bc 100644 --- a/app/controllers/concerns/merge_requests_action.rb +++ b/app/controllers/concerns/merge_requests_action.rb @@ -9,7 +9,7 @@ module MergeRequestsAction .page(params[:page]) @collection_type = "MergeRequest" - @issuable_meta_data = issuable_meta_data(@merge_requests) + @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type) end private -- cgit v1.2.3 From 27ffc2817554902c380a1101db7a0feb5816955f Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Fri, 17 Feb 2017 08:33:09 +1100 Subject: Add merge request count to each issue on issues list --- app/assets/images/icon-merge-request-unmerged.svg | 1 + app/assets/stylesheets/pages/issues.scss | 5 +++++ app/controllers/concerns/issuable_collections.rb | 20 ++++++++++++++------ app/controllers/concerns/issues_action.rb | 2 +- app/controllers/concerns/merge_requests_action.rb | 2 +- app/controllers/projects/issues_controller.rb | 2 +- .../projects/merge_requests_controller.rb | 2 +- app/models/concerns/issuable.rb | 4 ++-- app/models/merge_requests_closing_issues.rb | 8 ++++++++ app/views/shared/_issuable_meta_data.html.haml | 6 ++++++ changelogs/unreleased/add_mr_info_to_issues_list.yml | 4 ++++ 11 files changed, 44 insertions(+), 12 deletions(-) create mode 100644 app/assets/images/icon-merge-request-unmerged.svg create mode 100644 changelogs/unreleased/add_mr_info_to_issues_list.yml diff --git a/app/assets/images/icon-merge-request-unmerged.svg b/app/assets/images/icon-merge-request-unmerged.svg new file mode 100644 index 00000000000..c4d8e65122d --- /dev/null +++ b/app/assets/images/icon-merge-request-unmerged.svg @@ -0,0 +1 @@ + diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 80b0c9493d8..b595480561b 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -10,6 +10,11 @@ .issue-labels { display: inline-block; } + + .icon-merge-request-unmerged { + height: 13px; + margin-bottom: 3px; + } } } diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index a6e158ebae6..d7d781cbe72 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -9,24 +9,32 @@ module IssuableCollections private - def issuable_meta_data(issuable_collection) + def issuable_meta_data(issuable_collection, collection_type) # map has to be used here since using pluck or select will # throw an error when ordering issuables by priority which inserts # a new order into the collection. # We cannot use reorder to not mess up the paginated collection. - issuable_ids = issuable_collection.map(&:id) - issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type) + issuable_ids = issuable_collection.map(&:id) + issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type) issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type) + issuable_merge_requests_count = + if collection_type == 'Issue' + MergeRequestsClosingIssues.count_for_collection(issuable_ids) + else + [] + end issuable_ids.each_with_object({}) do |id, issuable_meta| downvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.downvote? } - upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? } - notes = issuable_note_count.find { |notes| notes.noteable_id == id } + upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? } + notes = issuable_note_count.find { |notes| notes.noteable_id == id } + merge_requests = issuable_merge_requests_count.find { |mr| mr.issue_id == id } issuable_meta[id] = Issuable::IssuableMeta.new( upvotes.try(:count).to_i, downvotes.try(:count).to_i, - notes.try(:count).to_i + notes.try(:count).to_i, + merge_requests.try(:count).to_i ) end end diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb index fb5edb34370..b17c138d5c7 100644 --- a/app/controllers/concerns/issues_action.rb +++ b/app/controllers/concerns/issues_action.rb @@ -10,7 +10,7 @@ module IssuesAction .page(params[:page]) @collection_type = "Issue" - @issuable_meta_data = issuable_meta_data(@issues) + @issuable_meta_data = issuable_meta_data(@issues, @collection_type) respond_to do |format| format.html diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb index 6229759dcf1..d3c8e4888bc 100644 --- a/app/controllers/concerns/merge_requests_action.rb +++ b/app/controllers/concerns/merge_requests_action.rb @@ -9,7 +9,7 @@ module MergeRequestsAction .page(params[:page]) @collection_type = "MergeRequest" - @issuable_meta_data = issuable_meta_data(@merge_requests) + @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type) end private diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 744a4af1c51..05056ad046a 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -26,7 +26,7 @@ class Projects::IssuesController < Projects::ApplicationController @collection_type = "Issue" @issues = issues_collection @issues = @issues.page(params[:page]) - @issuable_meta_data = issuable_meta_data(@issues) + @issuable_meta_data = issuable_meta_data(@issues, @collection_type) if @issues.out_of_range? && @issues.total_pages != 0 return redirect_to url_for(params.merge(page: @issues.total_pages)) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 63b5bcbb586..1d286ca62e5 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -39,7 +39,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @collection_type = "MergeRequest" @merge_requests = merge_requests_collection @merge_requests = @merge_requests.page(params[:page]) - @issuable_meta_data = issuable_meta_data(@merge_requests) + @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type) if @merge_requests.out_of_range? && @merge_requests.total_pages != 0 return redirect_to url_for(params.merge(page: @merge_requests.total_pages)) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 5f53c48fc88..c9c6bd24d75 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -16,9 +16,9 @@ module Issuable include TimeTrackable # This object is used to gather issuable meta data for displaying - # upvotes, downvotes and notes count for issues and merge requests + # upvotes, downvotes, notes and closing merge requests count for issues and merge requests # lists avoiding n+1 queries and improving performance. - IssuableMeta = Struct.new(:upvotes, :downvotes, :notes_count) + IssuableMeta = Struct.new(:upvotes, :downvotes, :notes_count, :merge_requests_count) included do cache_markdown_field :title, pipeline: :single_line diff --git a/app/models/merge_requests_closing_issues.rb b/app/models/merge_requests_closing_issues.rb index ab597c37947..1ecdfd1dfdb 100644 --- a/app/models/merge_requests_closing_issues.rb +++ b/app/models/merge_requests_closing_issues.rb @@ -4,4 +4,12 @@ class MergeRequestsClosingIssues < ActiveRecord::Base validates :merge_request_id, uniqueness: { scope: :issue_id }, presence: true validates :issue_id, presence: true + + class << self + def count_for_collection(ids) + select('issue_id', 'COUNT(*) as count'). + group(:issue_id). + where(issue_id: ids) + end + end end diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml index 1264e524d86..66310da5cd6 100644 --- a/app/views/shared/_issuable_meta_data.html.haml +++ b/app/views/shared/_issuable_meta_data.html.haml @@ -2,6 +2,12 @@ - issue_votes = @issuable_meta_data[issuable.id] - upvotes, downvotes = issue_votes.upvotes, issue_votes.downvotes - issuable_url = @collection_type == "Issue" ? issue_path(issuable, anchor: 'notes') : merge_request_path(issuable, anchor: 'notes') +- issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count + +- if issuable_mr > 0 + %li + = image_tag('icon-merge-request-unmerged', class: 'icon-merge-request-unmerged') + = issuable_mr - if upvotes > 0 %li diff --git a/changelogs/unreleased/add_mr_info_to_issues_list.yml b/changelogs/unreleased/add_mr_info_to_issues_list.yml new file mode 100644 index 00000000000..8087aa6296c --- /dev/null +++ b/changelogs/unreleased/add_mr_info_to_issues_list.yml @@ -0,0 +1,4 @@ +--- +title: Add merge request count to each issue on issues list +merge_request: 9252 +author: blackst0ne -- cgit v1.2.3 From 0f36cfd7f58977becea9d3ecf410d3669440fbe9 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 15 Feb 2017 14:06:00 +0000 Subject: Adds Pending and Finished tabs to pipelines page Fix broken test --- .../vue_pipelines_index/pipelines.js.es6 | 2 +- app/controllers/projects/pipelines_controller.rb | 12 ++++++-- app/finders/pipelines_finder.rb | 6 +++- app/views/projects/pipelines/index.html.haml | 22 ++++++++++---- changelogs/unreleased/26900-pipelines-tabs.yml | 4 +++ .../projects/pipelines_controller_spec.rb | 4 ++- spec/features/projects/pipelines/pipelines_spec.rb | 35 ++++++++++++++++++++++ spec/finders/pipelines_finder_spec.rb | 4 +-- 8 files changed, 77 insertions(+), 12 deletions(-) create mode 100644 changelogs/unreleased/26900-pipelines-tabs.yml diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 index dac364977d5..83e045c6d3d 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 @@ -23,7 +23,7 @@ const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_s apiScope: 'all', pageInfo: {}, pagenum: 1, - count: { all: 0, running_or_pending: 0 }, + count: {}, pageRequest: false, }; }, diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 84451257b98..8657bc4dfdc 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -13,9 +13,15 @@ class Projects::PipelinesController < Projects::ApplicationController .page(params[:page]) .per(30) - @running_or_pending_count = PipelinesFinder + @running_count = PipelinesFinder .new(project).execute(scope: 'running').count + @pending_count = PipelinesFinder + .new(project).execute(scope: 'pending').count + + @finished_count = PipelinesFinder + .new(project).execute(scope: 'finished').count + @pipelines_count = PipelinesFinder .new(project).execute.count @@ -29,7 +35,9 @@ class Projects::PipelinesController < Projects::ApplicationController .represent(@pipelines), count: { all: @pipelines_count, - running_or_pending: @running_or_pending_count + running: @running_count, + pending: @pending_count, + finished: @finished_count, } } end diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb index 32aea75486d..a9172f6767f 100644 --- a/app/finders/pipelines_finder.rb +++ b/app/finders/pipelines_finder.rb @@ -10,7 +10,11 @@ class PipelinesFinder scoped_pipelines = case scope when 'running' - pipelines.running_or_pending + pipelines.running + when 'pending' + pipelines.pending + when 'finished' + pipelines.finished when 'branches' from_ids(ids_for_ref(branches)) when 'tags' diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 6e0428e2a31..4147a617d95 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -5,23 +5,35 @@ %div{ class: container_class } .top-area %ul.nav-links - %li{ class: active_when(@scope.nil?) }> + %li.js-pipelines-tab-all{ class: active_when(@scope.nil?) }> = link_to project_pipelines_path(@project) do All %span.badge.js-totalbuilds-count = number_with_delimiter(@pipelines_count) - %li{ class: active_when(@scope == 'running') }> + %li.js-pipelines-tab-pending{ class: active_when(@scope == 'pending') }> + = link_to project_pipelines_path(@project, scope: :pending) do + Pending + %span.badge + = number_with_delimiter(@pending_count) + + %li.js-pipelines-tab-running{ class: active_when(@scope == 'running') }> = link_to project_pipelines_path(@project, scope: :running) do Running %span.badge.js-running-count - = number_with_delimiter(@running_or_pending_count) + = number_with_delimiter(@running_count) + + %li.js-pipelines-tab-finished{ class: active_when(@scope == 'finished') }> + = link_to project_pipelines_path(@project, scope: :finished) do + Finished + %span.badge + = number_with_delimiter(@finished_count) - %li{ class: active_when(@scope == 'branches') }> + %li.js-pipelines-tab-branches{ class: active_when(@scope == 'branches') }> = link_to project_pipelines_path(@project, scope: :branches) do Branches - %li{ class: active_when(@scope == 'tags') }> + %li.js-pipelines-tab-tags{ class: active_when(@scope == 'tags') }> = link_to project_pipelines_path(@project, scope: :tags) do Tags diff --git a/changelogs/unreleased/26900-pipelines-tabs.yml b/changelogs/unreleased/26900-pipelines-tabs.yml new file mode 100644 index 00000000000..f08514c621f --- /dev/null +++ b/changelogs/unreleased/26900-pipelines-tabs.yml @@ -0,0 +1,4 @@ +--- +title: Adds Pending and Finished tabs to pipelines page +merge_request: +author: diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index 1ed2ee3ab4a..242cf18c42b 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -25,7 +25,9 @@ describe Projects::PipelinesController do expect(json_response).to include('pipelines') expect(json_response['pipelines'].count).to eq 2 expect(json_response['count']['all']).to eq 2 - expect(json_response['count']['running_or_pending']).to eq 2 + expect(json_response['count']['running']).to eq 0 + expect(json_response['count']['pending']).to eq 2 + expect(json_response['count']['finished']).to eq 0 end end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index b56be499264..8f4317181df 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -42,6 +42,41 @@ describe 'Pipelines', :feature, :js do end end + context 'header tabs' do + before do + visit namespace_project_pipelines_path(project.namespace, project) + wait_for_vue_resource + end + + it 'shows a tab for All pipelines and count' do + expect(page.find('.js-pipelines-tab-all a').text).to include('All') + expect(page.find('.js-pipelines-tab-all .badge').text).to include('1') + end + + it 'shows a tab for Pending pipelines and count' do + expect(page.find('.js-pipelines-tab-pending a').text).to include('Pending') + expect(page.find('.js-pipelines-tab-pending .badge').text).to include('0') + end + + it 'shows a tab for Running pipelines and count' do + expect(page.find('.js-pipelines-tab-running a').text).to include('Running') + expect(page.find('.js-pipelines-tab-running .badge').text).to include('1') + end + + it 'shows a tab for Finished pipelines and count' do + expect(page.find('.js-pipelines-tab-finished a').text).to include('Finished') + expect(page.find('.js-pipelines-tab-finished .badge').text).to include('0') + end + + it 'shows a tab for Branches' do + expect(page.find('.js-pipelines-tab-branches a').text).to include('Branches') + end + + it 'shows a tab for Tags' do + expect(page.find('.js-pipelines-tab-tags a').text).to include('Tags') + end + end + context 'when pipeline is cancelable' do let!(:build) do create(:ci_build, pipeline: pipeline, diff --git a/spec/finders/pipelines_finder_spec.rb b/spec/finders/pipelines_finder_spec.rb index fdc8215aa47..6bada7b3eb9 100644 --- a/spec/finders/pipelines_finder_spec.rb +++ b/spec/finders/pipelines_finder_spec.rb @@ -39,8 +39,8 @@ describe PipelinesFinder do end end - # Scoping to running will speed up the test as it doesn't hit the FS - let(:params) { { scope: 'running' } } + # Scoping to pending will speed up the test as it doesn't hit the FS + let(:params) { { scope: 'pending' } } it 'orders in descending order on ID' do feature_pipeline = create(:ci_pipeline, project: project, ref: 'feature') -- cgit v1.2.3 From ac868482a7780a332045ef270a409ff70bcf8efd Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 1 Feb 2017 12:41:01 -0600 Subject: Allow issues in boards to be ordered --- .../boards/components/board_list.js.es6 | 5 +- app/assets/javascripts/boards/models/list.js.es6 | 25 +++- .../boards/services/board_service.js.es6 | 6 +- .../javascripts/boards/stores/boards_store.js.es6 | 3 + .../projects/boards/issues_controller.rb | 2 +- app/models/concerns/relative_positioning.rb | 132 +++++++++++++++++++++ app/models/issue.rb | 1 + app/services/boards/issues/list_service.rb | 7 +- app/services/boards/issues/move_service.rb | 29 +++-- app/services/issuable_base_service.rb | 2 +- app/services/issues/update_service.rb | 21 ++++ ...170131221752_add_relative_position_to_issues.rb | 31 +++++ db/schema.rb | 37 ++++-- 13 files changed, 270 insertions(+), 31 deletions(-) create mode 100644 app/models/concerns/relative_positioning.rb create mode 100644 db/migrate/20170131221752_add_relative_position_to_issues.rb diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index 60b0a30af3f..d9a42a23beb 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -86,7 +86,7 @@ require('./board_new_issue'); const options = gl.issueBoards.getBoardSortableDefaultOptions({ scroll: document.querySelectorAll('.boards-list')[0], group: 'issues', - sort: false, + sort: true, disabled: this.disabled, filter: '.board-list-count, .is-disabled', onStart: (e) => { @@ -105,6 +105,9 @@ require('./board_new_issue'); e.item.remove(); }); }, + onUpdate: (e) => { + gl.issueBoards.BoardsStore.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex); + }, }); this.sortable = Sortable.create(this.$refs.list, options); diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index 5152be56b66..4f7745e6e8d 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -111,8 +111,16 @@ class List { addIssue (issue, listFrom, newIndex) { if (!this.findIssue(issue.id)) { + let moveBeforeIid = null; + let moveAfterIid = null; + if (newIndex !== undefined) { this.issues.splice(newIndex, 0, issue); + + const issueBefore = this.issues[newIndex - 1]; + if (issueBefore) moveAfterIid = issueBefore.id; + const issueAfter = this.issues[newIndex + 1]; + if (issueAfter) moveBeforeIid = issueAfter.id; } else { this.issues.push(issue); } @@ -123,7 +131,7 @@ class List { if (listFrom) { this.issuesSize += 1; - gl.boardService.moveIssue(issue.id, listFrom.id, this.id) + gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveAfterIid, moveBeforeIid) .then(() => { listFrom.getIssues(false); }); @@ -131,6 +139,21 @@ class List { } } + moveIssue (issue, oldIndex, newIndex) { + let moveBeforeIid = null; + let moveAfterIid = null; + + this.issues.splice(oldIndex, 1); + this.issues.splice(newIndex, 0, issue); + + const issueBefore = this.issues[newIndex - 1]; + if (issueBefore) moveAfterIid = issueBefore.id; + const issueAfter = this.issues[newIndex + 1]; + if (issueAfter) moveBeforeIid = issueAfter.id; + + gl.boardService.moveIssue(issue.id, null, null, moveAfterIid, moveBeforeIid); + } + findIssue (id) { return this.issues.filter(issue => issue.id === id)[0]; } diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 index 065e90518df..1561f27de7f 100644 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ b/app/assets/javascripts/boards/services/board_service.js.es6 @@ -64,10 +64,12 @@ class BoardService { return this.issues.get(data); } - moveIssue (id, from_list_id, to_list_id) { + moveIssue (id, from_list_id = null, to_list_id = null, move_after_iid = null, move_before_iid = null) { return this.issue.update({ id }, { from_list_id, - to_list_id + to_list_id, + move_before_iid, + move_after_iid }); } diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index 50842ecbaaa..c8b76f839a5 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -106,6 +106,9 @@ listFrom.removeIssue(issue); } }, + moveIssueInList (list, issue, oldIndex, newIndex) { + list.moveIssue(issue, oldIndex, newIndex); + }, findList (key, val, type = 'label') { return this.state.lists.filter((list) => { const byType = type ? list['type'] === type : true; diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb index 61fef4dc133..25e50995764 100644 --- a/app/controllers/projects/boards/issues_controller.rb +++ b/app/controllers/projects/boards/issues_controller.rb @@ -63,7 +63,7 @@ module Projects end def move_params - params.permit(:board_id, :id, :from_list_id, :to_list_id) + params.permit(:board_id, :id, :from_list_id, :to_list_id, :move_before_iid, :move_after_iid) end def issue_params diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb new file mode 100644 index 00000000000..b9af6350c07 --- /dev/null +++ b/app/models/concerns/relative_positioning.rb @@ -0,0 +1,132 @@ +module RelativePositioning + extend ActiveSupport::Concern + + MIN_POSITION = Float::MIN + MAX_POSITION = Float::MAX + + included do + after_save :save_positionable_neighbours + end + + def min_relative_position + self.class.minimum(:relative_position) + end + + def max_relative_position + self.class.maximum(:relative_position) + end + + def prev_relative_position + prev_pos = nil + + if self.relative_position + prev_pos = self.class. + where('relative_position < ?', self.relative_position). + maximum(:relative_position) + end + + prev_pos || MIN_POSITION + end + + def next_relative_position + next_pos = nil + + if self.relative_position + next_pos = self.class. + where('relative_position > ?', self.relative_position). + minimum(:relative_position) + end + + next_pos || MAX_POSITION + end + + def move_between(before, after) + return move_nowhere unless before || after + return move_after(before) if before && !after + return move_before(after) if after && !before + + pos_before = before.relative_position + pos_after = after.relative_position + + if pos_before && pos_after + if pos_before == pos_after + pos = pos_before + + self.relative_position = pos + before.move_before(self) + after.move_after(self) + @positionable_neighbours = [before, after] + else + self.relative_position = position_between(pos_before, pos_after) + end + elsif pos_before + self.move_after(before) + after.move_after(self) + @positionable_neighbours = [after] + elsif pos_after + self.move_before(after) + before.move_before(self) + @positionable_neighbours = [before] + else + move_to_end + before.move_before(self) + after.move_after(self) + @positionable_neighbours = [before, after] + end + end + + def move_before(after) + pos_after = after.relative_position + if pos_after + self.relative_position = position_between(after.prev_relative_position, pos_after) + else + move_to_end + after.move_after(self) + @positionable_neighbours = [after] + end + end + + def move_after(before) + pos_before = before.relative_position + if pos_before + self.relative_position = position_between(pos_before, before.next_relative_position) + else + move_to_end + before.move_before(self) + @positionable_neighbours = [before] + end + end + + def move_nowhere + self.relative_position = nil + end + + def move_to_front + self.relative_position = position_between(MIN_POSITION, min_relative_position || MAX_POSITION) + end + + def move_to_end + self.relative_position = position_between(max_relative_position || MIN_POSITION, MAX_POSITION) + end + + def move_between!(*args) + move_between(*args) && save! + end + + private + + def position_between(pos_before, pos_after) + pos_before, pos_after = [pos_before, pos_after].sort + + pos_before + (pos_after - pos_before) / 2 + end + + def save_positionable_neighbours + return unless @positionable_neighbours + + @positionable_neighbours.each(&:save) + @positionable_neighbours = nil + + true + end +end diff --git a/app/models/issue.rb b/app/models/issue.rb index d8826b65fcc..f3c8f49cc66 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -7,6 +7,7 @@ class Issue < ActiveRecord::Base include Sortable include Spammable include FasterCacheKeys + include RelativePositioning DueDateStruct = Struct.new(:title, :name).freeze NoDueDate = DueDateStruct.new('No Due Date', '0').freeze diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index 8a94c54b6ab..185838764c1 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -5,7 +5,7 @@ module Boards issues = IssuesFinder.new(current_user, filter_params).execute issues = without_board_labels(issues) unless movable_list? issues = with_list_label(issues) if movable_list? - issues + issues.reorder(Gitlab::Database.nulls_last_order('relative_position', 'ASC')) end private @@ -26,7 +26,6 @@ module Boards def filter_params set_default_scope - set_default_sort set_project set_state @@ -37,10 +36,6 @@ module Boards params[:scope] = 'all' end - def set_default_sort - params[:sort] = 'priority' - end - def set_project params[:project_id] = project.id end diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb index 96554a92a02..7c0df55e9b6 100644 --- a/app/services/boards/issues/move_service.rb +++ b/app/services/boards/issues/move_service.rb @@ -3,7 +3,7 @@ module Boards class MoveService < BaseService def execute(issue) return false unless can?(current_user, :update_issue, issue) - return false unless valid_move? + return false if issue_params.empty? update_service.execute(issue) end @@ -14,7 +14,7 @@ module Boards @board ||= project.boards.find(params[:board_id]) end - def valid_move? + def move_between_lists? moving_from_list.present? && moving_to_list.present? && moving_from_list != moving_to_list end @@ -32,11 +32,19 @@ module Boards end def issue_params - { - add_label_ids: add_label_ids, - remove_label_ids: remove_label_ids, - state_event: issue_state - } + attrs = {} + + if move_between_lists? + attrs.merge!( + add_label_ids: add_label_ids, + remove_label_ids: remove_label_ids, + state_event: issue_state, + ) + end + + attrs[:move_between_iids] = move_between_iids if move_between_iids + + attrs end def issue_state @@ -58,6 +66,13 @@ module Boards Array(label_ids).compact end + + def move_between_iids + move_after_iid = params[:move_after_iid] + move_before_iid = params[:move_before_iid] + return unless move_after_iid || move_before_iid + [move_after_iid, move_before_iid] + end end end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 5f3ced49665..2256d7db008 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -212,7 +212,7 @@ class IssuableBaseService < BaseService 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) + if (issuable.changed? || params.present?) && update_issuable(issuable, params) # 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/issues/update_service.rb b/app/services/issues/update_service.rb index 78cbf94ec69..ba06f208dbc 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -1,6 +1,7 @@ module Issues class UpdateService < Issues::BaseService def execute(issue) + handle_move_between_iids(issue) update(issue) end @@ -47,6 +48,26 @@ module Issues Issues::CloseService end + def handle_move_between_iids(issue) + if move_between_iids = params.delete(:move_between_iids) + before_iid, after_iid = move_between_iids + + issue_before = nil + if before_iid + issue_before = issue.project.issues.find_by(iid: before_iid) + issue_before = nil unless can?(current_user, :update_issue, issue_before) + end + + issue_after = nil + if after_iid + issue_after = issue.project.issues.find_by(iid: after_iid) + issue_after = nil unless can?(current_user, :update_issue, issue_after) + end + + issue.move_between(issue_before, issue_after) + end + end + private def create_confidentiality_note(issue) diff --git a/db/migrate/20170131221752_add_relative_position_to_issues.rb b/db/migrate/20170131221752_add_relative_position_to_issues.rb new file mode 100644 index 00000000000..41e17cf13ba --- /dev/null +++ b/db/migrate/20170131221752_add_relative_position_to_issues.rb @@ -0,0 +1,31 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddRelativePositionToIssues < 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 :issues, :relative_position, :float + + add_index :issues, :relative_position + end +end diff --git a/db/schema.rb b/db/schema.rb index 52672406ec6..545dd4ad7c0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -87,9 +87,9 @@ ActiveRecord::Schema.define(version: 20170214111112) do t.boolean "send_user_confirmation_email", default: false t.integer "container_registry_token_expire_delay", default: 5 t.text "after_sign_up_text" - t.boolean "user_default_external", default: false, null: false t.string "repository_storages", default: "default" t.string "enabled_git_access_protocol" + t.boolean "user_default_external", default: false, null: false t.boolean "domain_blacklist_enabled", default: false t.text "domain_blacklist" t.boolean "koding_enabled" @@ -402,22 +402,22 @@ ActiveRecord::Schema.define(version: 20170214111112) do add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree create_table "deployments", force: :cascade do |t| - t.integer "iid", null: false - t.integer "project_id", null: false - t.integer "environment_id", null: false - t.string "ref", null: false - t.boolean "tag", null: false - t.string "sha", null: false + t.integer "iid" + t.integer "project_id" + t.integer "environment_id" + t.string "ref" + t.boolean "tag" + t.string "sha" t.integer "user_id" - t.integer "deployable_id" - t.string "deployable_type" + t.integer "deployable_id", null: false + t.string "deployable_type", null: false t.datetime "created_at" t.datetime "updated_at" t.string "on_stop" 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", "iid"], name: "index_deployments_on_project_id_and_iid", unique: true, using: :btree + add_index "deployments", ["project_id", "iid"], name: "index_deployments_on_project_id_and_iid", using: :btree create_table "emails", force: :cascade do |t| t.integer "user_id", null: false @@ -514,6 +514,7 @@ ActiveRecord::Schema.define(version: 20170214111112) do t.text "title_html" t.text "description_html" t.integer "time_estimate" + t.float "relative_position" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -525,6 +526,7 @@ ActiveRecord::Schema.define(version: 20170214111112) do add_index "issues", ["due_date"], name: "index_issues_on_due_date", using: :btree add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree + add_index "issues", ["relative_position"], name: "index_issues_on_relative_position", using: :btree add_index "issues", ["state"], name: "index_issues_on_state", using: :btree add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} @@ -692,8 +694,8 @@ ActiveRecord::Schema.define(version: 20170214111112) do t.integer "merge_user_id" t.string "merge_commit_sha" t.datetime "deleted_at" - t.string "in_progress_merge_commit_sha" t.integer "lock_version" + t.string "in_progress_merge_commit_sha" t.text "title_html" t.text "description_html" t.integer "time_estimate" @@ -770,6 +772,16 @@ ActiveRecord::Schema.define(version: 20170214111112) do 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 + create_table "note_templates", force: :cascade do |t| + t.integer "user_id" + t.string "title" + t.text "note" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "note_templates", ["user_id"], name: "index_note_templates_on_user_id", using: :btree + create_table "notes", force: :cascade do |t| t.text "note" t.string "noteable_type" @@ -785,6 +797,7 @@ ActiveRecord::Schema.define(version: 20170214111112) do t.text "st_diff" t.integer "updated_by_id" t.string "type" + t.string "system_type" t.text "position" t.text "original_position" t.datetime "resolved_at" @@ -978,7 +991,7 @@ ActiveRecord::Schema.define(version: 20170214111112) do t.boolean "has_external_wiki" t.boolean "lfs_enabled" t.text "description_html" - t.boolean "only_allow_merge_if_all_discussions_are_resolved" + t.boolean "only_allow_merge_if_all_discussions_are_resolved", default: false, null: false end add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree -- cgit v1.2.3 From be7d978a6a971764bec10539dbf3d4e3c63c2f2d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 1 Feb 2017 15:58:20 -0600 Subject: Pick a random float position between two positions instead of one exactly halfway --- app/models/concerns/relative_positioning.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb index b9af6350c07..1d4e141d905 100644 --- a/app/models/concerns/relative_positioning.rb +++ b/app/models/concerns/relative_positioning.rb @@ -118,7 +118,7 @@ module RelativePositioning def position_between(pos_before, pos_after) pos_before, pos_after = [pos_before, pos_after].sort - pos_before + (pos_after - pos_before) / 2 + rand(pos_before.next_float..pos_after.prev_float) end def save_positionable_neighbours -- cgit v1.2.3 From c032414b2d7b6ba75c3b202fe8791e32a0eb09e1 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 7 Feb 2017 08:56:33 +0000 Subject: Frontend updates to positioning the issue in lists --- .../boards/components/board_list.js.es6 | 9 ++------ app/assets/javascripts/boards/models/issue.js.es6 | 5 +--- app/assets/javascripts/boards/models/list.js.es6 | 27 ++++++---------------- .../boards/services/board_service.js.es6 | 2 +- .../javascripts/boards/stores/boards_store.js.es6 | 7 ++++-- .../projects/boards/issues_controller.rb | 2 +- app/models/concerns/relative_positioning.rb | 4 ++-- app/services/issues/update_service.rb | 2 +- .../boards/components/_board_list.html.haml | 2 +- 9 files changed, 21 insertions(+), 39 deletions(-) diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index d9a42a23beb..1af8850b6d5 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -56,11 +56,6 @@ require('./board_new_issue'); }); } }, - computed: { - orderedIssues () { - return _.sortBy(this.issues, 'priority'); - }, - }, methods: { listHeight () { return this.$refs.list.getBoundingClientRect().height; @@ -86,9 +81,9 @@ require('./board_new_issue'); const options = gl.issueBoards.getBoardSortableDefaultOptions({ scroll: document.querySelectorAll('.boards-list')[0], group: 'issues', - sort: true, disabled: this.disabled, filter: '.board-list-count, .is-disabled', + dataIdAttr: 'data-issue-id', onStart: (e) => { const card = this.$refs.issue[e.oldIndex]; @@ -106,7 +101,7 @@ require('./board_new_issue'); }); }, onUpdate: (e) => { - gl.issueBoards.BoardsStore.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex); + gl.issueBoards.BoardsStore.moveIssueInList(this.list, Store.moving.issue, this.sortable.toArray(), e.newIndex); }, }); diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6 index 2d0a295ae4d..ca5e6fa7e9d 100644 --- a/app/assets/javascripts/boards/models/issue.js.es6 +++ b/app/assets/javascripts/boards/models/issue.js.es6 @@ -15,6 +15,7 @@ class ListIssue { this.labels = []; this.selected = false; this.assignee = false; + this.position = obj.relative_position || Infinity; if (obj.assignee) { this.assignee = new ListUser(obj.assignee); @@ -27,10 +28,6 @@ class ListIssue { obj.labels.forEach((label) => { this.labels.push(new ListLabel(label)); }); - - this.priority = this.labels.reduce((max, label) => { - return (label.priority < max) ? label.priority : max; - }, Infinity); } addLabel (label) { diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index 4f7745e6e8d..a07fac380f7 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -110,17 +110,15 @@ class List { } addIssue (issue, listFrom, newIndex) { - if (!this.findIssue(issue.id)) { - let moveBeforeIid = null; - let moveAfterIid = null; + let moveBeforeIid; + let moveAfterIid; - if (newIndex !== undefined) { + if (!this.findIssue(issue.id)) { + if (newIndex) { this.issues.splice(newIndex, 0, issue); - const issueBefore = this.issues[newIndex - 1]; - if (issueBefore) moveAfterIid = issueBefore.id; - const issueAfter = this.issues[newIndex + 1]; - if (issueAfter) moveBeforeIid = issueAfter.id; + moveBeforeIid = this.issues[newIndex - 1].id || null; + moveAfterIid = this.issues[newIndex + 1].id || null; } else { this.issues.push(issue); } @@ -139,18 +137,7 @@ class List { } } - moveIssue (issue, oldIndex, newIndex) { - let moveBeforeIid = null; - let moveAfterIid = null; - - this.issues.splice(oldIndex, 1); - this.issues.splice(newIndex, 0, issue); - - const issueBefore = this.issues[newIndex - 1]; - if (issueBefore) moveAfterIid = issueBefore.id; - const issueAfter = this.issues[newIndex + 1]; - if (issueAfter) moveBeforeIid = issueAfter.id; - + moveIssue (issue, moveBeforeIid, moveAfterIid) { gl.boardService.moveIssue(issue.id, null, null, moveAfterIid, moveBeforeIid); } diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 index 1561f27de7f..a5999c511d4 100644 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ b/app/assets/javascripts/boards/services/board_service.js.es6 @@ -69,7 +69,7 @@ class BoardService { from_list_id, to_list_id, move_before_iid, - move_after_iid + move_after_iid, }); } diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index c8b76f839a5..841d27fe0ac 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -106,8 +106,11 @@ listFrom.removeIssue(issue); } }, - moveIssueInList (list, issue, oldIndex, newIndex) { - list.moveIssue(issue, oldIndex, newIndex); + moveIssueInList (list, issue, idArray, index) { + const beforeId = parseInt(idArray[index - 1], 10) || null; + const afterId = parseInt(idArray[index + 1], 10) || null; + + list.moveIssue(issue, beforeId, afterId); }, findList (key, val, type = 'label') { return this.state.lists.filter((list) => { diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb index 25e50995764..68437f5729f 100644 --- a/app/controllers/projects/boards/issues_controller.rb +++ b/app/controllers/projects/boards/issues_controller.rb @@ -73,7 +73,7 @@ module Projects def serialize_as_json(resource) resource.as_json( labels: true, - only: [:id, :iid, :title, :confidential, :due_date], + only: [:id, :iid, :title, :confidential, :due_date, :relative_position], include: { assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, milestone: { only: [:id, :title] } diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb index 1d4e141d905..3c1e726e6c8 100644 --- a/app/models/concerns/relative_positioning.rb +++ b/app/models/concerns/relative_positioning.rb @@ -78,7 +78,7 @@ module RelativePositioning def move_before(after) pos_after = after.relative_position if pos_after - self.relative_position = position_between(after.prev_relative_position, pos_after) + self.relative_position = position_between(MIN_POSITION, pos_after) else move_to_end after.move_after(self) @@ -89,7 +89,7 @@ module RelativePositioning def move_after(before) pos_before = before.relative_position if pos_before - self.relative_position = position_between(pos_before, before.next_relative_position) + self.relative_position = position_between(pos_before, MAX_POSITION) else move_to_end before.move_before(self) diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index ba06f208dbc..05f1da9165f 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -50,7 +50,7 @@ module Issues def handle_move_between_iids(issue) if move_between_iids = params.delete(:move_between_iids) - before_iid, after_iid = move_between_iids + after_iid, before_iid = move_between_iids issue_before = nil if before_iid diff --git a/app/views/projects/boards/components/_board_list.html.haml b/app/views/projects/boards/components/_board_list.html.haml index f413a5e94c1..56f566cbe24 100644 --- a/app/views/projects/boards/components/_board_list.html.haml +++ b/app/views/projects/boards/components/_board_list.html.haml @@ -28,7 +28,7 @@ "v-show" => "!loading", ":data-board" => "list.id", ":class" => '{ "is-smaller": showIssueForm }' } - %board-card{ "v-for" => "(issue, index) in orderedIssues", + %board-card{ "v-for" => "(issue, index) in issues", "ref" => "issue", ":index" => "index", ":list" => "list", -- cgit v1.2.3 From db0253cf443cb685859b4518040ee307ff8a446a Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 13 Feb 2017 09:42:09 +0000 Subject: Fixed bug when dragging issue to first index The was actually stopping the issue from being added at the correct index & would instead be added to the end of the array --- app/assets/javascripts/boards/models/list.js.es6 | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index a07fac380f7..dc815d2f815 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -110,15 +110,20 @@ class List { } addIssue (issue, listFrom, newIndex) { - let moveBeforeIid; - let moveAfterIid; + let moveBeforeIid = null; + let moveAfterIid = null; if (!this.findIssue(issue.id)) { - if (newIndex) { + if (newIndex !== undefined) { this.issues.splice(newIndex, 0, issue); - moveBeforeIid = this.issues[newIndex - 1].id || null; - moveAfterIid = this.issues[newIndex + 1].id || null; + if (this.issues[newIndex - 1]) { + moveBeforeIid = this.issues[newIndex - 1].id; + } + + if (this.issues[newIndex + 1]) { + moveAfterIid = this.issues[newIndex + 1].id + } } else { this.issues.push(issue); } -- cgit v1.2.3 From b31f9102f3a038798d9e2c65421891b68221110e Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 13 Feb 2017 12:15:20 +0000 Subject: Added ordering feature specs --- .../boards/components/board_list.js.es6 | 2 +- app/assets/javascripts/boards/models/list.js.es6 | 5 +- .../javascripts/boards/stores/boards_store.js.es6 | 8 +- app/assets/javascripts/test_utils/simulate_drag.js | 11 ++ spec/features/boards/issue_ordering_spec.rb | 141 +++++++++++++++++++++ 5 files changed, 161 insertions(+), 6 deletions(-) create mode 100644 spec/features/boards/issue_ordering_spec.rb diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index 1af8850b6d5..69e67a960bd 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -101,7 +101,7 @@ require('./board_new_issue'); }); }, onUpdate: (e) => { - gl.issueBoards.BoardsStore.moveIssueInList(this.list, Store.moving.issue, this.sortable.toArray(), e.newIndex); + gl.issueBoards.BoardsStore.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, this.sortable.toArray()); }, }); diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index dc815d2f815..f0943ec0784 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -142,7 +142,10 @@ class List { } } - moveIssue (issue, moveBeforeIid, moveAfterIid) { + moveIssue (issue, oldIndex, newIndex, moveBeforeIid, moveAfterIid) { + this.issues.splice(oldIndex, 1); + this.issues.splice(newIndex, 0, issue); + gl.boardService.moveIssue(issue.id, null, null, moveAfterIid, moveBeforeIid); } diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index 841d27fe0ac..46ed775138c 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -106,11 +106,11 @@ listFrom.removeIssue(issue); } }, - moveIssueInList (list, issue, idArray, index) { - const beforeId = parseInt(idArray[index - 1], 10) || null; - const afterId = parseInt(idArray[index + 1], 10) || null; + moveIssueInList (list, issue, oldIndex, newIndex, idArray) { + const beforeId = parseInt(idArray[newIndex - 1], 10) || null; + const afterId = parseInt(idArray[newIndex + 1], 10) || null; - list.moveIssue(issue, beforeId, afterId); + list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId); }, findList (key, val, type = 'label') { return this.state.lists.filter((list) => { diff --git a/app/assets/javascripts/test_utils/simulate_drag.js b/app/assets/javascripts/test_utils/simulate_drag.js index 7dba5840c8a..b49f8310c2a 100644 --- a/app/assets/javascripts/test_utils/simulate_drag.js +++ b/app/assets/javascripts/test_utils/simulate_drag.js @@ -55,6 +55,13 @@ ); } + function isLast(target) { + var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el; + var children = el.children; + + return children.length - 1 === target.index; + } + function getRect(el) { var rect = el.getBoundingClientRect(); var width = rect.right - rect.left; @@ -88,6 +95,10 @@ options.ontap && options.ontap(); window.SIMULATE_DRAG_ACTIVE = 1; + if (isLast(options.to)) { + toRect.cy += 100; + } + var dragInterval = setInterval(function loop() { var progress = (new Date().getTime() - startTime) / duration; var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft; diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb new file mode 100644 index 00000000000..5b11ac467b6 --- /dev/null +++ b/spec/features/boards/issue_ordering_spec.rb @@ -0,0 +1,141 @@ +require 'rails_helper' + +describe 'Issue Boards', :feature, :js do + include WaitForVueResource + include DragTo + + let(:project) { create(:empty_project, :public) } + let(:board) { create(:board, project: project) } + let(:user) { create(:user) } + let(:label) { create(:label, project: project) } + let!(:list1) { create(:list, board: board, label: label, position: 0) } + let!(:issue1) { create(:labeled_issue, project: project, labels: [label]) } + let!(:issue2) { create(:labeled_issue, project: project, labels: [label]) } + let!(:issue3) { create(:labeled_issue, project: project, labels: [label]) } + + before do + project.team << [user, :master] + + login_as(user) + end + + context 'ordering in list' do + before do + visit namespace_project_board_path(project.namespace, project, board) + wait_for_vue_resource + + expect(page).to have_selector('.board', count: 2) + end + + it 'moves from middle to top' do + drag(from_index: 1, to_index: 0) + + wait_for_vue_resource + + expect(first('.card')).to have_content(issue2.iid) + end + + it 'moves from middle to bottom' do + drag(from_index: 1, to_index: 2) + + wait_for_vue_resource + + expect(all('.card').last).to have_content(issue2.iid) + end + + it 'moves from top to bottom' do + drag(from_index: 0, to_index: 2) + + wait_for_vue_resource + + expect(all('.card').last).to have_content(issue3.iid) + end + + it 'moves from bottom to top' do + drag(from_index: 2, to_index: 0) + + wait_for_vue_resource + + expect(first('.card')).to have_content(issue1.iid) + end + + it 'moves from top to middle' do + drag(from_index: 0, to_index: 1) + + wait_for_vue_resource + + expect(first('.card')).to have_content(issue2.iid) + end + + it 'moves from bottom to middle' do + drag(from_index: 2, to_index: 1) + + wait_for_vue_resource + + expect(all('.card').last).to have_content(issue2.iid) + end + end + + context 'ordering when changing list' do + let(:label2) { create(:label, project: project) } + let!(:list2) { create(:list, board: board, label: label2, position: 1) } + let!(:issue4) { create(:labeled_issue, project: project, labels: [label2]) } + let!(:issue5) { create(:labeled_issue, project: project, labels: [label2]) } + let!(:issue6) { create(:labeled_issue, project: project, labels: [label2]) } + + before do + visit namespace_project_board_path(project.namespace, project, board) + wait_for_vue_resource + + expect(page).to have_selector('.board', count: 3) + end + + it 'moves to top of another list' do + drag(list_from_index: 0, list_to_index: 1) + + wait_for_vue_resource + + expect(first('.board')).to have_selector('.card', count: 2) + expect(all('.board')[1]).to have_selector('.card', count: 4) + + page.within(all('.board')[1]) do + expect(first('.card')).to have_content(issue3.iid) + end + end + + it 'moves to bottom of another list' do + drag(list_from_index: 0, list_to_index: 1, to_index: 2) + + wait_for_vue_resource + + expect(first('.board')).to have_selector('.card', count: 2) + expect(all('.board')[1]).to have_selector('.card', count: 4) + + page.within(all('.board')[1]) do + expect(all('.card').last).to have_content(issue3.iid) + end + end + + it 'moves to index of another list' do + drag(list_from_index: 0, list_to_index: 1, to_index: 1) + + wait_for_vue_resource + + expect(first('.board')).to have_selector('.card', count: 2) + expect(all('.board')[1]).to have_selector('.card', count: 4) + + page.within(all('.board')[1]) do + expect(all('.card')[1]).to have_content(issue3.iid) + end + end + end + + def drag(selector: '.board-list', list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0) + drag_to(selector: selector, + scrollable: '#board-app', + list_from_index: list_from_index, + from_index: from_index, + to_index: to_index, + list_to_index: list_to_index) + end +end -- cgit v1.2.3 From 723c8532244d1d08cd8c414270945ecc6e3f728b Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 13 Feb 2017 14:46:28 +0000 Subject: eslint fix --- app/assets/javascripts/boards/models/list.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index f0943ec0784..b4c08079125 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -122,7 +122,7 @@ class List { } if (this.issues[newIndex + 1]) { - moveAfterIid = this.issues[newIndex + 1].id + moveAfterIid = this.issues[newIndex + 1].id; } } else { this.issues.push(issue); -- cgit v1.2.3 From 3ab101ccb16eaa19b41cd8825299d811f142c044 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 14 Feb 2017 12:25:45 +0000 Subject: Added JS specs --- app/assets/javascripts/boards/models/list.js.es6 | 4 +- .../boards/services/board_service.js.es6 | 2 +- spec/javascripts/boards/boards_store_spec.js.es6 | 77 ++++++++++++++++++++++ spec/javascripts/boards/issue_spec.js.es6 | 16 +++++ 4 files changed, 96 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index b4c08079125..7914708a268 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -134,7 +134,7 @@ class List { if (listFrom) { this.issuesSize += 1; - gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveAfterIid, moveBeforeIid) + gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid) .then(() => { listFrom.getIssues(false); }); @@ -146,7 +146,7 @@ class List { this.issues.splice(oldIndex, 1); this.issues.splice(newIndex, 0, issue); - gl.boardService.moveIssue(issue.id, null, null, moveAfterIid, moveBeforeIid); + gl.boardService.moveIssue(issue.id, null, null, moveBeforeIid, moveAfterIid); } findIssue (id) { diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 index a5999c511d4..e54102814d6 100644 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ b/app/assets/javascripts/boards/services/board_service.js.es6 @@ -64,7 +64,7 @@ class BoardService { return this.issues.get(data); } - moveIssue (id, from_list_id = null, to_list_id = null, move_after_iid = null, move_before_iid = null) { + moveIssue (id, from_list_id = null, to_list_id = null, move_before_iid = null, move_after_iid = null) { return this.issue.update({ id }, { from_list_id, to_list_id, diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6 index 9dd741a680b..55bcd2572bf 100644 --- a/spec/javascripts/boards/boards_store_spec.js.es6 +++ b/spec/javascripts/boards/boards_store_spec.js.es6 @@ -1,3 +1,5 @@ +window.ES6Promise = require('vendor/es6-promise.auto'); +window.ES6Promise.polyfill(); /* eslint-disable comma-dangle, one-var, no-unused-vars */ /* global Vue */ /* global BoardService */ @@ -21,6 +23,12 @@ describe('Store', () => { gl.boardService = new BoardService('/test/issue-boards/board', '', '1'); gl.issueBoards.BoardsStore.create(); + spyOn(gl.boardService, 'moveIssue').and.callFake(() => { + return new Promise((resolve) => { + resolve(); + }); + }); + Cookies.set('issue_board_welcome_hidden', 'false', { expires: 365 * 10, path: '' @@ -154,5 +162,74 @@ describe('Store', () => { done(); }, 0); }); + + it('moves issue to top of another list', (done) => { + const listOne = gl.issueBoards.BoardsStore.addList(listObj); + const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate); + + expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2); + + setTimeout(() => { + listOne.issues[0].id = 2; + + expect(listOne.issues.length).toBe(1); + expect(listTwo.issues.length).toBe(1); + + gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(2), 0); + + expect(listOne.issues.length).toBe(0); + expect(listTwo.issues.length).toBe(2); + expect(listTwo.issues[0].id).toBe(2); + expect(gl.boardService.moveIssue).toHaveBeenCalledWith(2, listOne.id, listTwo.id, null, 1); + + done(); + }, 0); + }); + + it('moves issue to bottom of another list', (done) => { + const listOne = gl.issueBoards.BoardsStore.addList(listObj); + const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate); + + expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2); + + setTimeout(() => { + listOne.issues[0].id = 2; + + expect(listOne.issues.length).toBe(1); + expect(listTwo.issues.length).toBe(1); + + gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(2), 1); + + expect(listOne.issues.length).toBe(0); + expect(listTwo.issues.length).toBe(2); + expect(listTwo.issues[1].id).toBe(2); + expect(gl.boardService.moveIssue).toHaveBeenCalledWith(2, listOne.id, listTwo.id, 1, null); + + done(); + }, 0); + }); + + it('moves issue in list', (done) => { + const issue = new ListIssue({ + title: 'Testing', + iid: 2, + confidential: false, + labels: [] + }); + const list = gl.issueBoards.BoardsStore.addList(listObj); + + setTimeout(() => { + list.addIssue(issue); + + expect(list.issues.length).toBe(2); + + gl.issueBoards.BoardsStore.moveIssueInList(list, issue, 0, 1, [1, 2]); + + expect(list.issues[0].id).toBe(2); + expect(gl.boardService.moveIssue).toHaveBeenCalledWith(2, null, null, 1, null); + + done(); + }); + }); }); }); diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6 index aab4d9c501e..c96dfe94a4a 100644 --- a/spec/javascripts/boards/issue_spec.js.es6 +++ b/spec/javascripts/boards/issue_spec.js.es6 @@ -79,4 +79,20 @@ describe('Issue model', () => { issue.removeLabels([issue.labels[0], issue.labels[1]]); expect(issue.labels.length).toBe(0); }); + + it('sets position to infinity if no position is stored', () => { + expect(issue.position).toBe(Infinity); + }); + + it('sets position', () => { + const relativePositionIssue = new ListIssue({ + title: 'Testing', + iid: 1, + confidential: false, + relative_position: 1, + labels: [] + }); + + expect(relativePositionIssue.position).toBe(1); + }); }); -- cgit v1.2.3 From 9007a293a4af1872aa3ce2f0d613510f7839928f Mon Sep 17 00:00:00 2001 From: George Andrinopoulos Date: Fri, 17 Feb 2017 15:43:37 +0200 Subject: Fix todos API endpoint application error --- lib/api/entities.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 232f231ddd2..4484ea92d48 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -397,7 +397,11 @@ module API expose :target_type expose :target do |todo, options| - Entities.const_get(todo.target_type).represent(todo.target, options) + if todo.target_type == 'Commit' + Entities.const_get('RepoCommit').represent(todo.target, options) + else + Entities.const_get(todo.target_type).represent(todo.target, options) + end end expose :target_url do |todo, options| -- cgit v1.2.3 From 034397d31f0ff64771520e8ee0cc745d793f31e4 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 17 Feb 2017 14:44:39 +0000 Subject: This should make the ordering feature specs more reliable --- app/assets/javascripts/test_utils/simulate_drag.js | 36 ++++++++++++++-------- spec/features/boards/issue_ordering_spec.rb | 30 +++++++++--------- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/app/assets/javascripts/test_utils/simulate_drag.js b/app/assets/javascripts/test_utils/simulate_drag.js index b49f8310c2a..d48f2404fa5 100644 --- a/app/assets/javascripts/test_utils/simulate_drag.js +++ b/app/assets/javascripts/test_utils/simulate_drag.js @@ -43,7 +43,14 @@ return event; } - function getTraget(target) { + function isLast(target) { + var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el; + var children = el.children; + + return children.length - 1 === target.index; + } + + function getTarget(target) { var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el; var children = el.children; @@ -55,13 +62,6 @@ ); } - function isLast(target) { - var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el; - var children = el.children; - - return children.length - 1 === target.index; - } - function getRect(el) { var rect = el.getBoundingClientRect(); var width = rect.right - rect.left; @@ -82,12 +82,22 @@ function simulateDrag(options, callback) { options.to.el = options.to.el || options.from.el; - var fromEl = getTraget(options.from); - var toEl = getTraget(options.to); + var fromEl = getTarget(options.from); + var toEl = getTarget(options.to); + var firstEl = getTarget({ + el: options.to.el, + index: 'first' + }); + var lastEl = getTarget({ + el: options.to.el, + index: 'last' + }); var scrollable = options.scrollable; var fromRect = getRect(fromEl); var toRect = getRect(toEl); + var firstRect = getRect(firstEl); + var lastRect = getRect(lastEl); var startTime = new Date().getTime(); var duration = options.duration || 1000; @@ -95,8 +105,10 @@ options.ontap && options.ontap(); window.SIMULATE_DRAG_ACTIVE = 1; - if (isLast(options.to)) { - toRect.cy += 100; + if (options.to.index === 0) { + toRect.cy = firstRect.y; + } else if (isLast(options.to)) { + toRect.cy = lastRect.y + lastRect.h + 50; } var dragInterval = setInterval(function loop() { diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb index 5b11ac467b6..047de40ef9e 100644 --- a/spec/features/boards/issue_ordering_spec.rb +++ b/spec/features/boards/issue_ordering_spec.rb @@ -9,9 +9,9 @@ describe 'Issue Boards', :feature, :js do let(:user) { create(:user) } let(:label) { create(:label, project: project) } let!(:list1) { create(:list, board: board, label: label, position: 0) } - let!(:issue1) { create(:labeled_issue, project: project, labels: [label]) } - let!(:issue2) { create(:labeled_issue, project: project, labels: [label]) } - let!(:issue3) { create(:labeled_issue, project: project, labels: [label]) } + let!(:issue1) { create(:labeled_issue, project: project, title: 'testing 1', labels: [label], relative_position: 3.0) } + let!(:issue2) { create(:labeled_issue, project: project, title: 'testing 2', labels: [label], relative_position: 2.0) } + let!(:issue3) { create(:labeled_issue, project: project, title: 'testing 3', labels: [label], relative_position: 1.0) } before do project.team << [user, :master] @@ -32,7 +32,7 @@ describe 'Issue Boards', :feature, :js do wait_for_vue_resource - expect(first('.card')).to have_content(issue2.iid) + expect(first('.card')).to have_content(issue2.title) end it 'moves from middle to bottom' do @@ -40,7 +40,7 @@ describe 'Issue Boards', :feature, :js do wait_for_vue_resource - expect(all('.card').last).to have_content(issue2.iid) + expect(all('.card').last).to have_content(issue2.title) end it 'moves from top to bottom' do @@ -48,7 +48,7 @@ describe 'Issue Boards', :feature, :js do wait_for_vue_resource - expect(all('.card').last).to have_content(issue3.iid) + expect(all('.card').last).to have_content(issue3.title) end it 'moves from bottom to top' do @@ -56,7 +56,7 @@ describe 'Issue Boards', :feature, :js do wait_for_vue_resource - expect(first('.card')).to have_content(issue1.iid) + expect(first('.card')).to have_content(issue1.title) end it 'moves from top to middle' do @@ -64,7 +64,7 @@ describe 'Issue Boards', :feature, :js do wait_for_vue_resource - expect(first('.card')).to have_content(issue2.iid) + expect(first('.card')).to have_content(issue2.title) end it 'moves from bottom to middle' do @@ -72,16 +72,16 @@ describe 'Issue Boards', :feature, :js do wait_for_vue_resource - expect(all('.card').last).to have_content(issue2.iid) + expect(all('.card').last).to have_content(issue2.title) end end context 'ordering when changing list' do let(:label2) { create(:label, project: project) } let!(:list2) { create(:list, board: board, label: label2, position: 1) } - let!(:issue4) { create(:labeled_issue, project: project, labels: [label2]) } - let!(:issue5) { create(:labeled_issue, project: project, labels: [label2]) } - let!(:issue6) { create(:labeled_issue, project: project, labels: [label2]) } + let!(:issue4) { create(:labeled_issue, project: project, title: 'testing 1', labels: [label2], relative_position: 3.0) } + let!(:issue5) { create(:labeled_issue, project: project, title: 'testing 2', labels: [label2], relative_position: 2.0) } + let!(:issue6) { create(:labeled_issue, project: project, title: 'testing 3', labels: [label2], relative_position: 1.0) } before do visit namespace_project_board_path(project.namespace, project, board) @@ -99,7 +99,7 @@ describe 'Issue Boards', :feature, :js do expect(all('.board')[1]).to have_selector('.card', count: 4) page.within(all('.board')[1]) do - expect(first('.card')).to have_content(issue3.iid) + expect(first('.card')).to have_content(issue3.title) end end @@ -112,7 +112,7 @@ describe 'Issue Boards', :feature, :js do expect(all('.board')[1]).to have_selector('.card', count: 4) page.within(all('.board')[1]) do - expect(all('.card').last).to have_content(issue3.iid) + expect(all('.card').last).to have_content(issue3.title) end end @@ -125,7 +125,7 @@ describe 'Issue Boards', :feature, :js do expect(all('.board')[1]).to have_selector('.card', count: 4) page.within(all('.board')[1]) do - expect(all('.card')[1]).to have_content(issue3.iid) + expect(all('.card')[1]).to have_content(issue3.title) end end end -- cgit v1.2.3 From 12c262c4edc604db687a30ead2aebebd13ab86b7 Mon Sep 17 00:00:00 2001 From: Regis Date: Fri, 17 Feb 2017 12:36:42 -0700 Subject: shape header - bold correct text - border radius - white background for the rest of widget --- app/assets/stylesheets/pages/merge_requests.scss | 10 +++++++++- app/views/projects/merge_requests/_show.html.haml | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 0b0c4bc130d..7df6a03ccd7 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -3,7 +3,6 @@ * */ .mr-state-widget { - background: $gray-light; color: $gl-text-color; border: 1px solid $border-color; border-radius: 2px; @@ -335,7 +334,16 @@ } .mr-source-target { + background-color: #FAFAFA; line-height: 31px; + border-style: solid; + border-width: 1px; + border-color: $border-color; + border-top-right-radius: 10px; + border-top-left-radius: 10px; + border--radius: 0px; + padding: 10px; + margin-bottom: -1px; } .panel-new-merge-request { diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 521b0694ca9..2d017ac3fa2 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -28,9 +28,9 @@ %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) .normal - %span Request to merge + %span Request to merge %span.label-branch= source_branch_with_namespace(@merge_request) - %span into + %span into %span.label-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? -- cgit v1.2.3 From 32b59a1fa7f439cccbf0de29094c3fab3ec518a8 Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Sat, 18 Feb 2017 11:47:56 +1100 Subject: Added specs --- spec/features/issuables/issuable_list_spec.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb index e31bc40adc3..13ea9ce853c 100644 --- a/spec/features/issuables/issuable_list_spec.rb +++ b/spec/features/issuables/issuable_list_spec.rb @@ -30,6 +30,12 @@ describe 'issuable list', feature: true do end end + it "counts merge requests closing issues icons for each issue" do + visit_issuable_list(:issue) + + expect(first('.icon-merge-request-unmerged').find(:xpath, '..')).to have_content(1) + end + def visit_issuable_list(issuable_type) if issuable_type == :issue visit namespace_project_issues_path(project.namespace, project) @@ -42,6 +48,13 @@ describe 'issuable list', feature: true do 3.times do if issuable_type == :issue issuable = create(:issue, project: project, author: user) + merge_request = create(:merge_request, + title: FFaker::Lorem.sentence, + description: "Closes #{issuable.to_reference}", + source_project: project, + source_branch: FFaker::Name.name) + + MergeRequestsClosingIssues.create!(issue: issuable, merge_request: merge_request) else issuable = create(:merge_request, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name) end -- cgit v1.2.3 From c21df05066a3a9351db649ebc3f00f4bee63cce8 Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Sat, 18 Feb 2017 11:49:13 +1100 Subject: Remove empty line in note --- app/views/projects/notes/_note.html.haml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index a68a09baf4f..1b08165c14c 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -63,7 +63,6 @@ = icon('pencil', class: 'link-highlight') = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do = icon('trash-o', class: 'danger-highlight') - .note-body{ class: note_editable ? 'js-task-list-container' : '' } .note-text.md = preserve do -- cgit v1.2.3 From 24ba7585e6da5ee8881ff8b4db53558940cb0c23 Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Sat, 18 Feb 2017 12:58:24 +1100 Subject: Fixed rubocop offenses --- spec/features/issuables/issuable_list_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb index 13ea9ce853c..ce4dca1175f 100644 --- a/spec/features/issuables/issuable_list_spec.rb +++ b/spec/features/issuables/issuable_list_spec.rb @@ -30,11 +30,11 @@ describe 'issuable list', feature: true do end end - it "counts merge requests closing issues icons for each issue" do - visit_issuable_list(:issue) + it "counts merge requests closing issues icons for each issue" do + visit_issuable_list(:issue) - expect(first('.icon-merge-request-unmerged').find(:xpath, '..')).to have_content(1) - end + expect(first('.icon-merge-request-unmerged').find(:xpath, '..')).to have_content(1) + end def visit_issuable_list(issuable_type) if issuable_type == :issue -- cgit v1.2.3 From 0b49a2c7e0e20caed2a5985bf572a85b75dfeb5c Mon Sep 17 00:00:00 2001 From: sytses Date: Sun, 19 Feb 2017 16:21:35 -0800 Subject: But notice first. --- CONTRIBUTING.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index de32a953f63..eaedb147892 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,7 +26,16 @@ -# Contribute to GitLab +## Contributor license agreement + +By submitting code as an individual you agree to the +[individual contributor license agreement](doc/legal/individual_contributor_license_agreement.md). +By submitting code as an entity you agree to the +[corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md). + +This notice should stay a the first item in the CONTRIBUTING.MD file. + +## Contribute to GitLab Thank you for your interest in contributing to GitLab. This guide details how to contribute to GitLab in a way that is efficient for everyone. @@ -41,13 +50,6 @@ operates please see [the GitLab contributing process](PROCESS.md). - [GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/) -## Contributor license agreement - -By submitting code as an individual you agree to the -[individual contributor license agreement](doc/legal/individual_contributor_license_agreement.md). -By submitting code as an entity you agree to the -[corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md). - ## Security vulnerability disclosure Please report suspected security vulnerabilities in private to -- cgit v1.2.3 From d38fb942e188021d7def5eb577a73c82b8e5e66d Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 16 Feb 2017 21:08:47 +0800 Subject: Optimize Ci::Pipeline.latest query Since we already know which ref we want, we could filter it out first. Feedback: https://gitlab.com/gitlab-org/gitlab-ce/issues/26570#note_23423376 Closes #26570 --- app/models/ci/pipeline.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index dc4590a9923..2a987bfa87b 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -91,6 +91,7 @@ module Ci scope :latest, ->(ref = nil) do max_id = unscope(:select) .select("max(#{quoted_table_name}.id)") + .where(ref: ref) .group(:ref, :sha) relation = ref ? where(ref: ref) : self -- cgit v1.2.3 From 3750b06b8cdc02bc87eba27dd8e4ab5f1f24802f Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 16 Feb 2017 22:17:42 +0800 Subject: Consider the case where we don't specify ref for pipeline --- app/models/ci/pipeline.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 2a987bfa87b..6e89b18aee5 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -91,11 +91,13 @@ module Ci scope :latest, ->(ref = nil) do max_id = unscope(:select) .select("max(#{quoted_table_name}.id)") - .where(ref: ref) .group(:ref, :sha) - relation = ref ? where(ref: ref) : self - relation.where(id: max_id) + if ref + where(ref: ref, id: max_id.where(ref: ref)) + else + where(id: max_id) + end end def self.latest_status(ref = nil) -- cgit v1.2.3 From 22d6f96cf982765fa9b957905248a573c6e3c498 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 16 Feb 2017 22:19:24 +0800 Subject: Add index for ci_commits for gl_project_id, ref, status and remove the old one which we don't really need. --- .../20170216135621_add_index_for_latest_successful_pipeline.rb | 10 ++++++++++ .../20170216141440_drop_index_for_builds_project_status.rb | 8 ++++++++ db/schema.rb | 4 ++-- 3 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb create mode 100644 db/migrate/20170216141440_drop_index_for_builds_project_status.rb diff --git a/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb b/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb new file mode 100644 index 00000000000..7b1e687977b --- /dev/null +++ b/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb @@ -0,0 +1,10 @@ +class AddIndexForLatestSuccessfulPipeline < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + DOWNTIME = false + + disable_ddl_transaction! + + def change + add_concurrent_index(:ci_commits, [:gl_project_id, :ref, :status]) + end +end diff --git a/db/migrate/20170216141440_drop_index_for_builds_project_status.rb b/db/migrate/20170216141440_drop_index_for_builds_project_status.rb new file mode 100644 index 00000000000..906711b9f3f --- /dev/null +++ b/db/migrate/20170216141440_drop_index_for_builds_project_status.rb @@ -0,0 +1,8 @@ +class DropIndexForBuildsProjectStatus < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + DOWNTIME = false + + def change + remove_index(:ci_commits, [:gl_project_id, :status]) + end +end diff --git a/db/schema.rb b/db/schema.rb index e0208dab3d3..6be017e66b3 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: 20170215200045) do +ActiveRecord::Schema.define(version: 20170216141440) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -251,8 +251,8 @@ ActiveRecord::Schema.define(version: 20170215200045) do t.integer "lock_version" end + add_index "ci_commits", ["gl_project_id", "ref", "status"], name: "index_ci_commits_on_gl_project_id_and_ref_and_status", using: :btree add_index "ci_commits", ["gl_project_id", "sha"], name: "index_ci_commits_on_gl_project_id_and_sha", using: :btree - add_index "ci_commits", ["gl_project_id", "status"], name: "index_ci_commits_on_gl_project_id_and_status", using: :btree add_index "ci_commits", ["gl_project_id"], name: "index_ci_commits_on_gl_project_id", using: :btree add_index "ci_commits", ["status"], name: "index_ci_commits_on_status", using: :btree add_index "ci_commits", ["user_id"], name: "index_ci_commits_on_user_id", using: :btree -- cgit v1.2.3 From c1714355276476776ec3f16686722f49cac3895d Mon Sep 17 00:00:00 2001 From: Nur Rony Date: Mon, 20 Feb 2017 13:48:16 +0600 Subject: fixes line number copy issue for unfolded parallel view diff --- app/views/projects/blob/diff.html.haml | 4 ++-- .../unreleased/28367-fix-unfold-diff-line-number-copy-paste.yml | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/28367-fix-unfold-diff-line-number-copy-paste.yml diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index d1f7f65bf53..c48d9dd982c 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -19,10 +19,10 @@ = line_content - when :parallel %td.old_line.diff-line-num{ data: { linenumber: line_old } } - = link_to raw(line_old), "##{line_old}" + %a{ href: "##{line_old}", data: { linenumber: line_old } } = line_content %td.new_line.diff-line-num{ data: { linenumber: line_new } } - = link_to raw(line_new), "##{line_new}" + %a{ href: "##{line_new}", data: { linenumber: line_new } } = line_content - if @form.unfold? && @form.bottom? && @form.to < @blob.lines.size diff --git a/changelogs/unreleased/28367-fix-unfold-diff-line-number-copy-paste.yml b/changelogs/unreleased/28367-fix-unfold-diff-line-number-copy-paste.yml new file mode 100644 index 00000000000..6fc89fd91dd --- /dev/null +++ b/changelogs/unreleased/28367-fix-unfold-diff-line-number-copy-paste.yml @@ -0,0 +1,4 @@ +--- +title: Fixes includes line number during unfold copy n paste in parallel diff view +merge_request: 9365 +author: -- cgit v1.2.3 From b2f1c2b8be610695cf0523ae93524a1f53831c44 Mon Sep 17 00:00:00 2001 From: Rudi Kramer Date: Mon, 20 Feb 2017 11:14:40 +0000 Subject: Update two_factor_authentication.md by correcting FreeOTP link. --- doc/user/profile/account/two_factor_authentication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md index a23ad79ae1d..eaa39a0c4ea 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -213,5 +213,5 @@ your GitLab server's time is synchronized via a service like NTP. Otherwise, you may have cases where authorization always fails because of time differences. [Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en -[FreeOTP]: https://fedorahosted.org/freeotp/ +[FreeOTP]: https://freeotp.github.io/ [YubiKey]: https://www.yubico.com/products/yubikey-hardware/ -- cgit v1.2.3 From 444d71e043eb19979ec1b08504b2760910cb2a47 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Mon, 20 Feb 2017 13:41:50 +0100 Subject: Transactional mattermost team creation Before this commit, but still on this feature branch, the creation of mattermost teams where a background job. However, it was decided it was better that these happened as transaction so feedback could be displayed to the user. --- app/controllers/groups_controller.rb | 1 - app/models/group.rb | 1 - app/models/namespace.rb | 1 + app/services/groups/create_service.rb | 21 ++++++++----- app/services/mattermost/create_team_service.rb | 15 +++++++++ app/workers/mattermost/create_team_worker.rb | 24 --------------- lib/mattermost/team.rb | 8 ++--- spec/services/groups/create_service_spec.rb | 11 +++++-- spec/workers/mattermost/create_team_worker_spec.rb | 36 ---------------------- 9 files changed, 42 insertions(+), 76 deletions(-) create mode 100644 app/services/mattermost/create_team_service.rb delete mode 100644 app/workers/mattermost/create_team_worker.rb delete mode 100644 spec/workers/mattermost/create_team_worker_spec.rb diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index b2a18f0e65d..0e421b915cf 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -29,7 +29,6 @@ class GroupsController < Groups::ApplicationController end def create - byebug @group = Groups::CreateService.new(current_user, group_params).execute if @group.persisted? diff --git a/app/models/group.rb b/app/models/group.rb index 27dc4e8f319..240a17f1dc1 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -21,7 +21,6 @@ class Group < Namespace has_many :shared_projects, through: :project_group_links, source: :project has_many :notification_settings, dependent: :destroy, as: :source has_many :labels, class_name: 'GroupLabel' - has_one :chat_team, dependent: :destroy validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :visibility_level_allowed_by_projects diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 6de4d08fc28..461a38d3e8e 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -20,6 +20,7 @@ class Namespace < ActiveRecord::Base belongs_to :parent, class_name: "Namespace" has_many :children, class_name: "Namespace", foreign_key: :parent_id + has_one :chat_team, dependent: :destroy validates :owner, presence: true, unless: ->(n) { n.type == "Group" } validates :name, diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index 13d1b545498..3028025fc6e 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -2,12 +2,10 @@ module Groups class CreateService < Groups::BaseService def initialize(user, params = {}) @current_user, @params = user, params.dup + @chat_team = @params.delete(:create_chat_team) end def execute - create_chat_team = params.delete(:create_chat_team) - team_name = params.delete(:chat_team_name) - @group = Group.new(params) unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level]) @@ -23,15 +21,22 @@ module Groups end @group.name ||= @group.path.dup - @group.save - @group.add_owner(current_user) - if create_chat_team && Gitlab.config.mattermost.enabled - options = team_name ? { name: team_name } : {} - Mattermost::CreateTeamWorker.perform_async(@group.id, current_user.id, options) + if create_chat_team? + Mattermost::CreateTeamService.new(@group, current_user).execute + + return @group if @group.errors.any? end + @group.save + @group.add_owner(current_user) @group end + + private + + def create_chat_team? + @chat_team && Gitlab.config.mattermost.enabled + end end end diff --git a/app/services/mattermost/create_team_service.rb b/app/services/mattermost/create_team_service.rb new file mode 100644 index 00000000000..199d15aee92 --- /dev/null +++ b/app/services/mattermost/create_team_service.rb @@ -0,0 +1,15 @@ +module Mattermost + class CreateTeamService < ::BaseService + def initialize(group, current_user) + @group, @current_user = group, current_user + end + + def execute + # The user that creates the team will be Team Admin + response = Mattermost::Team.new(current_user).create(@group) + @group.build_chat_team(name: response['name'], team_id: response['id']) + rescue Mattermost::ClientError => e + @group.errors.add(:chat_team, e.message) + end + end +end diff --git a/app/workers/mattermost/create_team_worker.rb b/app/workers/mattermost/create_team_worker.rb deleted file mode 100644 index 95b63dac8ab..00000000000 --- a/app/workers/mattermost/create_team_worker.rb +++ /dev/null @@ -1,24 +0,0 @@ -module Mattermost - class CreateTeamWorker - include Sidekiq::Worker - include DedicatedSidekiqQueue - - sidekiq_options retry: 5 - - # Add 5 seconds so the first retry isn't 1 second later - sidekiq_retry_in do |count| - 5 + 5**n - end - - def perform(group_id, current_user_id, options = {}) - group = Group.find_by(id: group_id) - current_user = User.find_by(id: current_user_id) - return unless group && current_user - - # The user that creates the team will be Team Admin - response = Mattermost::Team.new(current_user).create(group, options) - - group.create_chat_team(name: response['name'], team_id: response['id']) - end - end -end diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb index d5648f7167f..52486fd1a54 100644 --- a/lib/mattermost/team.rb +++ b/lib/mattermost/team.rb @@ -7,20 +7,20 @@ module Mattermost # Creates a team on the linked Mattermost instance, the team admin will be the # `current_user` passed to the Mattermost::Client instance - def create(group, params) - session_post('/api/v3/teams/create', body: new_team_params(group, params).to_json) + def create(group) + session_post('/api/v3/teams/create', body: new_team_params(group).to_json) end private MATTERMOST_TEAM_LENGTH_MAX = 59 - def new_team_params(group, options) + def new_team_params(group) { name: group.path[0..MATTERMOST_TEAM_LENGTH_MAX], display_name: group.name[0..MATTERMOST_TEAM_LENGTH_MAX], type: group.public? ? 'O' : 'I' # Open vs Invite-only - }.merge(options) + } end end end diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index 235fa5c5188..d8181f73110 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -43,10 +43,17 @@ describe Groups::CreateService, '#execute', services: true do let!(:params) { group_params.merge(create_chat_team: true) } let!(:service) { described_class.new(user, params) } - it 'queues a background job' do - expect(Mattermost::CreateTeamWorker).to receive(:perform_async) + it 'triggers the service' do + expect_any_instance_of(Mattermost::CreateTeamService).to receive(:execute) subject end + + it 'create the chat team with the group' do + allow_any_instance_of(Mattermost::Team).to receive(:create) + .and_return({ 'name' => 'tanuki', 'id' => 'lskdjfwlekfjsdifjj' }) + + expect { subject }.to change { ChatTeam.count }.from(0).to(1) + end end end diff --git a/spec/workers/mattermost/create_team_worker_spec.rb b/spec/workers/mattermost/create_team_worker_spec.rb deleted file mode 100644 index d3230f535c2..00000000000 --- a/spec/workers/mattermost/create_team_worker_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'spec_helper' - -describe Mattermost::CreateTeamWorker do - let(:group) { create(:group, path: 'path', name: 'name') } - let(:admin) { create(:admin) } - - describe '.perform' do - subject { described_class.new.perform(group.id, admin.id) } - - context 'succesfull request to mattermost' do - before do - allow_any_instance_of(Mattermost::Team). - to receive(:create). - with(group, {}). - and_return('name' => 'my team', 'id' => 'sjfkdlwkdjfwlkfjwf') - end - - it 'creates a new chat team' do - expect { subject }.to change { ChatTeam.count }.from(0).to(1) - end - end - - context 'connection trouble' do - before do - allow_any_instance_of(Mattermost::Team). - to receive(:create). - with(group, {}). - and_raise(Mattermost::ClientError.new('Undefined error')) - end - - it 'does not rescue the error' do - expect { subject }.to raise_error(Mattermost::ClientError) - end - end - end -end -- cgit v1.2.3 From 50cfaf0ef668a4cd70f7eebbb30db166a7b8e486 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 20 Feb 2017 08:22:44 -0500 Subject: Remove markup showing in tooltip for renamed files in diff view --- app/views/projects/diffs/_file_header.html.haml | 4 ++-- changelogs/unreleased/28366-renamed-file-tooltip-contains-html.yml | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/28366-renamed-file-tooltip-contains-html.yml diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml index 1dbfe830d52..f809c52c367 100644 --- a/app/views/projects/diffs/_file_header.html.haml +++ b/app/views/projects/diffs/_file_header.html.haml @@ -10,10 +10,10 @@ - if diff_file.renamed_file - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path) - %strong.file-title-name.has-tooltip{ data: { title: old_path, container: 'body' } } + %strong.file-title-name.has-tooltip{ data: { title: diff_file.old_path, container: 'body' } } = old_path → - %strong.file-title-name.has-tooltip{ data: { title: new_path, container: 'body' } } + %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } } = new_path - else %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } } diff --git a/changelogs/unreleased/28366-renamed-file-tooltip-contains-html.yml b/changelogs/unreleased/28366-renamed-file-tooltip-contains-html.yml new file mode 100644 index 00000000000..faf1e89ed94 --- /dev/null +++ b/changelogs/unreleased/28366-renamed-file-tooltip-contains-html.yml @@ -0,0 +1,4 @@ +--- +title: Remove markup that was showing in tooltip for renamed files +merge_request: 9374 +author: -- cgit v1.2.3 From 479cdc2cac78241109d2cb9d8cfd22d36319d367 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Mon, 20 Feb 2017 14:50:32 +0100 Subject: Fix bad merge [ci skip] --- db/migrate/20170120131253_create_chat_teams.rb | 4 ++-- db/schema.rb | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/db/migrate/20170120131253_create_chat_teams.rb b/db/migrate/20170120131253_create_chat_teams.rb index 8f76b43960b..6476c239152 100644 --- a/db/migrate/20170120131253_create_chat_teams.rb +++ b/db/migrate/20170120131253_create_chat_teams.rb @@ -5,13 +5,13 @@ class CreateChatTeams < ActiveRecord::Migration def change create_table :chat_teams do |t| - t.integer :group_id, index: true + t.integer :namespace_id, index: true t.string :team_id t.string :name t.timestamps null: false end - add_foreign_key :chat_teams, :groups, on_delete: :cascade + add_foreign_key :chat_teams, :namespaces, on_delete: :cascade end end diff --git a/db/schema.rb b/db/schema.rb index 66863942b77..cb97e6d8a93 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -61,6 +61,7 @@ ActiveRecord::Schema.define(version: 20170215200045) do t.boolean "shared_runners_enabled", default: true, null: false t.integer "max_artifacts_size", default: 100, null: false t.string "runners_registration_token" + t.integer "max_pages_size", default: 100, null: false t.boolean "require_two_factor_authentication", default: false t.integer "two_factor_grace_period", default: 48 t.boolean "metrics_enabled", default: false @@ -109,7 +110,6 @@ ActiveRecord::Schema.define(version: 20170215200045) do t.boolean "html_emails_enabled", default: true t.string "plantuml_url" t.boolean "plantuml_enabled" - t.integer "max_pages_size", default: 100, null: false t.integer "terminal_max_session_time", default: 0, null: false end @@ -590,9 +590,9 @@ ActiveRecord::Schema.define(version: 20170215200045) 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", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree add_index "labels", ["title"], name: "index_labels_on_title", using: :btree + add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree create_table "lfs_objects", force: :cascade do |t| t.string "oid", null: false @@ -765,8 +765,8 @@ ActiveRecord::Schema.define(version: 20170215200045) 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 @@ -1285,8 +1285,8 @@ ActiveRecord::Schema.define(version: 20170215200045) 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" t.boolean "notified_of_own_activity", default: false, null: false end -- cgit v1.2.3 From f1cec0dfc0d0137a40226fada53add89ba641631 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 20 Feb 2017 10:41:51 -0600 Subject: upgrade Vue from v2.0.3 to v2.1.10 --- package.json | 2 +- yarn.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index ad0aaef1897..3a88d8c3e1f 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "stats-webpack-plugin": "^0.4.3", "timeago.js": "^2.0.5", "underscore": "^1.8.3", - "vue": "^2.0.3", + "vue": "^2.1.10", "vue-resource": "^0.9.3", "webpack": "^2.2.1" }, diff --git a/yarn.lock b/yarn.lock index ad4b5223d60..56d8eb7e6a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2627,7 +2627,7 @@ jquery-ujs@^1.2.1: dependencies: jquery ">=1.8.0" -jquery@^2.2.1, jquery@>=1.8.0: +jquery@>=1.8.0, jquery@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.2.1.tgz#3c3e16854ad3d2ac44ac65021b17426d22ad803f" @@ -2824,7 +2824,7 @@ loader-runner@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" -loader-utils@0.2.x, loader-utils@^0.2.11, loader-utils@^0.2.16, loader-utils@^0.2.5: +loader-utils@^0.2.11, loader-utils@^0.2.16, loader-utils@^0.2.5: version "0.2.16" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.16.tgz#f08632066ed8282835dff88dfb52704765adee6d" dependencies: @@ -3964,7 +3964,7 @@ source-map-support@^0.4.2: dependencies: source-map "^0.5.3" -source-map@0.1.x, source-map@^0.1.41: +source-map@^0.1.41: version "0.1.43" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" dependencies: @@ -4384,9 +4384,9 @@ vue-resource@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/vue-resource/-/vue-resource-0.9.3.tgz#ab46e1c44ea219142dcc28ae4043b3b04c80959d" -vue@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/vue/-/vue-2.0.3.tgz#3f7698f83d6ad1f0e35955447901672876c63fde" +vue@^2.1.10: + version "2.1.10" + resolved "https://registry.yarnpkg.com/vue/-/vue-2.1.10.tgz#c9235ca48c7925137be5807832ac4e3ac180427b" watchpack@^1.2.0: version "1.2.1" -- cgit v1.2.3 From cad8ab8204f25524611e46375f576d2fbc7759c0 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 20 Feb 2017 10:43:36 -0600 Subject: use CommonJS vue distribution --- config/webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/webpack.config.js b/config/webpack.config.js index 515569b2e97..df673f4b3bd 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -84,7 +84,7 @@ var config = { 'bootstrap/js': 'bootstrap-sass/assets/javascripts/bootstrap', 'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'), 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'), - 'vue$': IS_PRODUCTION ? 'vue/dist/vue.min.js' : 'vue/dist/vue.js', + 'vue$': 'vue/dist/vue.common.js', } } } -- cgit v1.2.3 From 1a0f70bf2297cb4551531ca6876c1988f78798e9 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 20 Feb 2017 10:54:42 -0600 Subject: add CHANGELOG.md entry for !9386 --- changelogs/unreleased/update-vue-2-1.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/update-vue-2-1.yml diff --git a/changelogs/unreleased/update-vue-2-1.yml b/changelogs/unreleased/update-vue-2-1.yml new file mode 100644 index 00000000000..acc42bf00b1 --- /dev/null +++ b/changelogs/unreleased/update-vue-2-1.yml @@ -0,0 +1,4 @@ +--- +title: update Vue to v2.1.10 +merge_request: 9386 +author: -- cgit v1.2.3 From 6c373e718161da77fa1ec02065307641f6e55003 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Mon, 20 Feb 2017 15:19:19 -0300 Subject: add index file to Pages docs --- doc/pages/index.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 doc/pages/index.md diff --git a/doc/pages/index.md b/doc/pages/index.md new file mode 100644 index 00000000000..3590cf5cfb1 --- /dev/null +++ b/doc/pages/index.md @@ -0,0 +1,39 @@ +# All you need to know about GitLab Pages + +## Product + +- [Product Webpage](https://pages.gitlab.io) +- [We're Bringing GitLab Pages to CE](https://about.gitlab.com/2016/12/24/were-bringing-gitlab-pages-to-community-edition/) +- [Pages Group - Templates](https://gitlab.com/pages) + +## Getting Started + +- Comprehensive Step-by-Step Guide: [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) +- GitLab Pages from A to Z + - [Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](https://gitlab.com/marcia/drafts/blob/master/pages/pages_part-1.md) + - [Part 2: Quick Start Guide - Setting Up GitLab Pages](https://gitlab.com/marcia/drafts/blob/master/pages/pages_part-2.md) + - Video tutorial: [How to Publish a Website with GitLab Pages on GitLab.com: from a forked project](#LINK) + - Video tutorial: [How to Publish a Website with GitLab Pages on GitLab.com: from scratch](#LINK) + - [Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](#LINK) +- Secure GitLab Pages Custom Domain with SSL/TLS Certificates + - [Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/) + - [CloudFlare](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) + - [StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/) +- Static Site Generators - Blog Posts Series + - [SSGs Part 1: Static vs Dynamic Websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) + - [SSGs Part 2: Modern Static Site Generators](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) + - [SSGs Part 3: Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) +- [Posting to your GitLab Pages blog from iOS](https://about.gitlab.com/2016/08/19/posting-to-your-gitlab-pages-blog-from-ios/) + +## Advanced Use + +- Blog Posts: + - [GitLab CI: Run jobs Sequentially, in Parallel, or Build a Custom Pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/) + - [GitLab CI: Deployment & Environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/) + - [Building a new GitLab Docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/) + - [Publish Code Coverage Report with GitLab Pages](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/) + +## General Documentation + +- [User docs](../user/project/pages/) +- [Admin docs](administration.html) -- cgit v1.2.3 From 962f9efeb470afd0627f509621de4ff87124d656 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 21 Feb 2017 02:25:35 +0800 Subject: Update error message and check with presence: true Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9219#note_23762243 https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9219#note_23762268 --- app/models/application_setting.rb | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 36832185b6f..b5ffc5f9ca9 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -80,6 +80,7 @@ class ApplicationSetting < ActiveRecord::Base presence: true, numericality: { only_integer: true, greater_than: 0 } + validates :default_artifacts_expire_in, presence: true validate :check_default_artifacts_expire_in validates :container_registry_token_expire_delay, @@ -222,6 +223,14 @@ class ApplicationSetting < ActiveRecord::Base create(defaults) end + def self.human_attribute_name(attr, _options = {}) + if attr == :default_artifacts_expire_in + 'Default artifacts expiration' + else + super + end + end + def home_page_url_column_exist ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url) end @@ -298,12 +307,8 @@ class ApplicationSetting < ActiveRecord::Base end def check_default_artifacts_expire_in - if default_artifacts_expire_in.blank? - errors.add(:default_artifacts_expiration, "is not presented") - else - ChronicDuration.parse(default_artifacts_expire_in) - end + ChronicDuration.parse(default_artifacts_expire_in) rescue ChronicDuration::DurationParseError - errors.add(:default_artifacts_expiration, "is invalid") + errors.add(:default_artifacts_expiration, "is not a correct duration") end end -- cgit v1.2.3 From ad44252424280857e3735a5e931413f4e99e9d5f Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 21 Feb 2017 02:28:21 +0800 Subject: Add screenshot for default artifacts expiration, and update screenshot for maximum artifacts size. --- .../img/admin_area_default_artifacts_expiration.png | Bin 0 -> 16484 bytes .../img/admin_area_maximum_artifacts_size.png | Bin 3447 -> 12917 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png diff --git a/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png b/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png new file mode 100644 index 00000000000..c6425ad32cd Binary files /dev/null and b/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.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 b7d6671902a..33fd29e2039 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 -- cgit v1.2.3 From 0d0842a42203edd3f9ff341f538c9cf3e92ad957 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 21 Feb 2017 02:36:11 +0800 Subject: Update doc according to the new syntax --- doc/user/admin_area/settings/continuous_integration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md index 0fd165fc095..64df7ee48bd 100644 --- a/doc/user/admin_area/settings/continuous_integration.md +++ b/doc/user/admin_area/settings/continuous_integration.md @@ -30,7 +30,7 @@ expiration. ![Admin area settings button](img/admin_area_settings_button.png) -1. Change the value of default expiration time (in days): +1. Change the value of default expiration time ([syntax][duration-syntax]): ![Admin area default artifacts expiration](img/admin_area_default_artifacts_expiration.png) -- cgit v1.2.3 From a2b7f8d1410bf97772a03c38df1396cce795b983 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Mon, 20 Feb 2017 16:14:48 -0300 Subject: add pages guide part 1 - static sites, domains, DNS, SSL/TLS --- doc/pages/index.md | 2 +- ...tes_domains_dns_records_ssl_tls_certificates.md | 159 +++++++++++++++++++++ 2 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md diff --git a/doc/pages/index.md b/doc/pages/index.md index 3590cf5cfb1..c3dd08b46a7 100644 --- a/doc/pages/index.md +++ b/doc/pages/index.md @@ -10,7 +10,7 @@ - Comprehensive Step-by-Step Guide: [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) - GitLab Pages from A to Z - - [Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](https://gitlab.com/marcia/drafts/blob/master/pages/pages_part-1.md) + - [Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](pages_static_sites_domains_dns_records_ssl_tls_certificates.html) - [Part 2: Quick Start Guide - Setting Up GitLab Pages](https://gitlab.com/marcia/drafts/blob/master/pages/pages_part-2.md) - Video tutorial: [How to Publish a Website with GitLab Pages on GitLab.com: from a forked project](#LINK) - Video tutorial: [How to Publish a Website with GitLab Pages on GitLab.com: from scratch](#LINK) diff --git a/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md b/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md new file mode 100644 index 00000000000..f76e02471e9 --- /dev/null +++ b/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md @@ -0,0 +1,159 @@ +# GitLab Pages from A to Z + +> Type: user guide +> +> Level: beginner + +- **Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates** +- _[Part 2: Quick Start Guide - Setting Up GitLab Pages](pages_quick_start_guide.html)_ +- _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](pages_creating_and_tweaking_gitlab-ci_.html)_ + +---- + +This is a comprehensive guide, made for those who want to publish a website with GitLab Pages but aren't familiar with the entire process involved. + +To **enable** GitLab Pages for GitLab CE (Community Edition) and GitLab EE (Enterprise Edition), please read the [admin documentation](https://docs.gitlab.com/ce/administration/pages/index.html), or watch this [video tutorial](#LINK). + +> For this guide, we assume you already have GitLab Pages server up and running for your GitLab instance. + +## What you need to know before getting started + +Before we begin, let's understand a few concepts first. + +### Static Sites + +GitLab Pages only supports static websites, meaning, your output files must be HTML, CSS, and JavaScript only. + +To create your static site, you can either hardcode in HTML, CSS, and JS, or use a [Static Site Generator (SSG)](https://www.staticgen.com/) to simplify your code and build the static site for you, which is highly recommendable and much faster than hardcoding. + +#### Further Reading + +- Read through this technical overview on [Static versus Dynamic Websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) +- Understand [how modern Static Site Generators work](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) and what you can add to your static site +- You can use [any SSG with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) +- Fork an [example project](https://gitlab.com/pages) to build your website based upon + +### GitLab Pages Domain + +If you set up a GitLab Pages project on GitLab.com, it will automatically be accessible under a [subdomain of `.pages.io`](https://docs.gitlab.com/ce/user/project/pages/). The `` is defined by your username on GitLab.com, or the group name you created this project under. + +> Note: If you use your own GitLab instance to deploy your site with GitLab Pages, check with your sysadmin what's your Pages wildcard domain. This guide is valid for any GitLab instance, you just need to replace Pages wildcard domain on GitLab.com (`*.gitlab.io`) with your own. + +#### Practical examples + +**Project Websites:** + +- You created a project called `blog` under your username `john`, therefore your project URL is `https://gitlab.com/john/blog/`. Once you enable GitLab Pages for this project, and build your site, it will be available under `https://john.gitlab.io/blog/`. +- You created a group for all your websites called `websites`, and a project within this group is called `blog`. Your project URL is `https://gitlab.com/websites/blog/`. Once you enable GitLab Pages for this project, the site will live under `https://websites.gitlab.io/blog/`. + +**User and Group Websites:** + +- Under your username, `john`, you created a project called `john.gitlab.io`. Your project URL will be `https://gitlab.com/john/john.gitlab.io`. Once you enable GitLab Pages for your project, your website will be published under `https://john.gitlab.io`. +- Under your group `websites`, you created a project called `websites.gitlab.io`. your project's URL will be `https://gitlab.com/websites/websites.gitlab.io`. Once you enable GitLab Pages for your project, your website will be published under `https://websites.gitlab.io`. + +**General example:** + +- On GitLab.com, a project site will always be available under `https://namespace.gitlab.io/project-name` +- On GitLab.com, a user or group website will be available under `https://namespace.gitlab.io/` +- On your GitLab instance, replace `gitlab.io` above with your Pages server domain. Ask your sysadmin for this information. + +### DNS Records + +A Domain Name System (DNS) web service routes visitors to websites by translating domain names (such as `www.example.com`) into the numeric IP addresses (such as `192.0.2.1`) that computers use to connect to each other. + +A DNS record is created to point a (sub)domain to a certain location, which can be an IP address or another domain. In case you want to use GitLab Pages with your own (sub)domain, you need to access your domain's registrar control panel to add a DNS record pointing it back to your GitLab Pages site. + +Note that **how to** add DNS records depends on which server your domain is hosted on. Every control panel has its own place to do it. If you are not an admin of your domain, and don't have access to your registrar, you'll need to ask for the technical support of your hosting service to do it for you. + +To help you out, we've gathered some instructions on how to do that for the most popular hosting services: + +- [Amazon](http://docs.aws.amazon.com/gettingstarted/latest/swh/getting-started-configure-route53.html) +- [Bluehost](https://my.bluehost.com/cgi/help/559) +- [CloudFlare](https://support.cloudflare.com/hc/en-us/articles/200169096-How-do-I-add-A-records-) +- [cPanel](https://documentation.cpanel.net/display/ALD/Edit+DNS+Zone) +- [DreamHost](https://help.dreamhost.com/hc/en-us/articles/215414867-How-do-I-add-custom-DNS-records-) +- [Go Daddy](https://www.godaddy.com/help/add-an-a-record-19238) +- [Hostgator](http://support.hostgator.com/articles/changing-dns-records) +- [Inmotion hosting](https://my.bluehost.com/cgi/help/559) +- [Media Temple](https://mediatemple.net/community/products/dv/204403794/how-can-i-change-the-dns-records-for-my-domain) +- [Microsoft](https://msdn.microsoft.com/en-us/library/bb727018.aspx) + +If your hosting service is not listed above, you can just try to search the web for "how to add dns record on ". + +#### DNS A record + +In case you want to point a root domain (`example.com`) to your GitLab Pages site, deployed to `namespace.gitlab.io`, you need to log into your domain's admin control panel and add a DNS `A` record pointing your domain to Pages' server IP address. For projects on GitLab.com, this IP is `104.208.235.32`. For projects leaving in other GitLab instances (CE or EE), please contact your sysadmin asking for this information (which IP address is Pages server running on your instance). + +**Practical Example:** + +![DNS A record pointing to GitLab.com Pages server]() + +#### DNS CNAME record + +In case you want to point a subdomain (`hello-world.example.com`) to your GitLab Pages site initially deployed to `namespace.gitlab.io`, you need to log into your domain's admin control panel and add a DNS `CNAME` record pointing your subdomain to your website URL (`namespace.gitlab.io`) address. + +Notice that, despite it's a user or project website, the `CNAME` should point to your Pages domain (`namespace.gitlab.io`), without any `/project-name`. + +**Practical Example:** + +![DNS CNAME record pointing to GitLab.com project]() + +#### TL;DR + +| From | DNS Record | To | +| ---- | ---------- | -- | +| domain.com | A | 104.208.235.32 | +| subdomain.domain.com | CNAME | namespace.gitlab.io | + +> **Notes**: +> +> - **Do not** use a CNAME record if you want to point your `domain.com` to your GitLab Pages site. Use an `A` record instead. +> - **Do not** add any special chars after the default Pages domain. E.g., **do not** point your `subdomain.domain.com` to `namespace.gitlab.io.` or `namespace.gitlab.io/`. + +### SSL/TLS Certificates + +Every GitLab Pages project on GitLab.com will be available under HTTPS for the default Pages domain (`*.gitlab.io`). Once you set up your Pages project with your custom (sub)domain, if you want it secured by HTTPS, you will have to issue a certificate for that (sub)domain and install it on your project. + +> Note: certificates are NOT required to add to your custom (sub)domain on your GitLab Pages project, though they are highly recommendable. + +The importance of having any website securely served under HTTPS is explained on the introductory section of the blog post [Secure GitLab Pages with StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/#https-a-quick-overview). + +The reason why certificates are so important is that they encrypt the connection between the **client** (you, me, your visitors) and the **server** (where you site lives), through a keychain of authentications and validations. + +### Issuing Certificates + +GitLab Pages accepts [PEM](https://support.quovadisglobal.com/kb/a37/what-is-pem-format.aspx) certificates issued by [Certificate Authorities (CA)](https://en.wikipedia.org/wiki/Certificate_authority) and self-signed certificates. Of course, [you'd rather issue a certificate than generate a self-signed](https://en.wikipedia.org/wiki/Self-signed_certificate), for security reasons and for having browsers trusting your site's certificate. + +There are several different kinds of certificates, each one with certain security level. A static personal website will not require the same security level as an online banking web app, for instance. There are a couple Certificate Authorities that offer free certificates, aiming to make the internet more secure to everyone. The most popular is [Let's Encrypt](https://letsencrypt.org/), which issues certificates trusted by most of browsers, it's open source, and free to use. Please read through this tutorial to understand [how to secure your GitLab Pages website with Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/). + +With the same popularity, there are [certificates issued by CloudFlare](https://www.cloudflare.com/ssl/), which also offers a [free CDN service](https://blog.cloudflare.com/cloudflares-free-cdn-and-you/). Their certs are valid up to 15 years. Read through the tutorial on [how to add a CloudFlare Certificate to your GitLab Pages website](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/). + +### Adding certificates to your project + +Regardless the CA you choose, the steps to add your certificate to your Pages project are the same. + +#### What do you need + +1. A PEM certificate +1. An intermediary certificate +1. A public key + +![Pages project - adding certificates]() + +These fields are found under your **Project**'s **Settings** > **Pages** > **New Domain**. + +#### What's what? + +- A PEM certificate is the certificate generated by the CA, which needs to be added to the field **Certificate (PEM)**. +- An [intermediary certificate][] (aka "root certificate") is the part of the encryption keychain that identifies the CA. Usually it's combined with the PEM certificate, but there are some cases in which you need to add them manually. [CloudFlare certs](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) are one of these cases. +- A public key is an encrypted key which validates your PEM against your domain. + +#### Now what? + +Now that you hopefully understand why you need all of this, it's simple: + +- Your PEM certificate needs to be added to the first field +- If your certificate is missing its intermediary, copy and paste the root certificate (usually available from your CA website) and paste it in the [same field as your PEM certificate](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/), just jumping a line between them. +- Copy your public key and paste it in the last field + +> Note: **do not** open certificates or encryption keys in regular text editors. Always use code editors (such as Sublime Text, Atom, Dreamweaver, Brackets, etc). -- cgit v1.2.3 From abb668f1e4ad82cd44d0d6c175a9dbee23e98f64 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 20 Feb 2017 13:54:29 -0600 Subject: remove require.context from boards_bundle --- app/assets/javascripts/boards/boards_bundle.js.es6 | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index 878ad1b6031..f6b75ac6196 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -2,15 +2,19 @@ /* global Vue */ /* global BoardService */ -function requireAll(context) { return context.keys().map(context); } - window.Vue = require('vue'); window.Vue.use(require('vue-resource')); -requireAll(require.context('./models', true, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./stores', true, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./services', true, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./mixins', true, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./filters', true, /^\.\/.*\.(js|es6)$/)); +require('./models/issue'); +require('./models/label'); +require('./models/list'); +require('./models/milestone'); +require('./models/user'); +require('./stores/boards_store'); +require('./stores/modal_store'); +require('./services/board_service'); +require('./mixins/modal_mixins'); +require('./mixins/sortable_default_options'); +require('./filters/due_date_filters'); require('./components/board'); require('./components/board_sidebar'); require('./components/new_list_dropdown'); -- cgit v1.2.3 From 6d9edfa60bf3f3fbf0aa7cb8c4dd9755effcdeef Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Mon, 20 Feb 2017 17:03:22 -0300 Subject: add closing section: further reading --- .../pages_static_sites_domains_dns_records_ssl_tls_certificates.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md b/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md index f76e02471e9..797e7d28469 100644 --- a/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md +++ b/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md @@ -1,4 +1,4 @@ -# GitLab Pages from A to Z +# GitLab Pages from A to Z: Part 1 > Type: user guide > @@ -157,3 +157,8 @@ Now that you hopefully understand why you need all of this, it's simple: - Copy your public key and paste it in the last field > Note: **do not** open certificates or encryption keys in regular text editors. Always use code editors (such as Sublime Text, Atom, Dreamweaver, Brackets, etc). + +## Further Reading + +- Read through GitLab Pages from A to Z _[Part 2: Quick Start Guide - Setting Up GitLab Pages](pages_quick_start_guide.html)_ +- Read through GitLab Pages from A to Z _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](pages_creating_and_tweaking_gitlab-ci_.html)_ -- cgit v1.2.3 From 41e36f2c0fc21a63bdc51383bbc3be03cb3497cf Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 20 Feb 2017 14:04:02 -0600 Subject: remove require.context from cycle_analytics_bundle --- .../cycle_analytics/cycle_analytics_bundle.js.es6 | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) 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 1ac715aab77..411ac7b24b2 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 @@ -4,10 +4,20 @@ window.Vue = require('vue'); window.Cookies = require('js-cookie'); - -function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('./svg', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('.', true, /^\.\/(?!cycle_analytics_bundle).*\.(js|es6)$/)); +require('./svg/icon_branch'); +require('./svg/icon_build_status'); +require('./svg/icon_commit'); +require('./components/stage_code_component'); +require('./components/stage_issue_component'); +require('./components/stage_plan_component'); +require('./components/stage_production_component'); +require('./components/stage_review_component'); +require('./components/stage_staging_component'); +require('./components/stage_test_component'); +require('./components/total_time_component'); +require('./cycle_analytics_service'); +require('./cycle_analytics_store'); +require('./default_event_objects'); $(() => { const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; -- cgit v1.2.3 From dec96c248d44b8f6f5615b7bcdb2ce9ad63ff32a Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 20 Feb 2017 14:11:43 -0600 Subject: remove unneeded eslint exceptions --- app/assets/javascripts/boards/boards_bundle.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index f6b75ac6196..0c5177ced2f 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren, import/newline-after-import, no-multi-spaces, max-len */ +/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */ /* global Vue */ /* global BoardService */ -- cgit v1.2.3 From 0aa685658319d837c090def9604cc2fba1dd5cf5 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Mon, 20 Feb 2017 17:13:45 -0300 Subject: add part 2: quick start guide --- doc/pages/pages_quick_start_guide.md | 112 +++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 doc/pages/pages_quick_start_guide.md diff --git a/doc/pages/pages_quick_start_guide.md b/doc/pages/pages_quick_start_guide.md new file mode 100644 index 00000000000..804b34a8e75 --- /dev/null +++ b/doc/pages/pages_quick_start_guide.md @@ -0,0 +1,112 @@ +# GitLab Pages from A to Z: Part 2 + +> Type: user guide +> +> Level: beginner + +- **Part 2: Quick Start Guide - Setting Up GitLab Pages** +- _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](pages_static_sites_domains_dns_records_ssl_tls_certificates.html)_ +- _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](pages_creating_and_tweaking_gitlab-ci_.html)_ + +---- + +## Setting Up GitLab Pages + +For a complete step-by-step tutorial, please read the blog post [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/). The following sections will explain what do you need and why do you need them. + + + +### What You Need to Get Started + +1. A project +1. A configuration file (`.gitlab-ci.yml`) to deploy your site +1. A specific `job` called `pages` in the configuration file that will make GitLab aware that you are deploying a GitLab Pages website + +#### Optional Features + +1. A custom domain or subdomain +1. A DNS pointing your (sub)domain to your Pages site + 1. **Optional**: an SSL/TLS certificate so your custom domain is accessible under HTTPS. + +### Project + +Your GitLab Pages project is a regular project created the same way you do for the other ones. To get started with GitLab Pages, you have two ways: + +- Fork one of the templates from Page Examples, or +- Create a new project from scratch + +Let's go over both options. + +#### Fork a Project to Get Started From + +To make things easy for you, we've created this [group](https://gitlab.com/pages) of default projects containing the most popular SSGs templates. + +Watch the [video tutorial](#LINK) we've created for the steps below. + +1. Choose your SSG template +1. Fork a project from the [Pages group](https://gitlab.com/pages) +1. Remove the fork relationship by navigating to your **Project**'s **Settings** > **Edit Project** + + ![remove fork relashionship]() + +1. Enable Shared Runners for your fork: navigate to your **Project**'s **Settings** > **CI/CD Pipelines** +1. Trigger a build (push a change to any file) +1. As soon as the build passes, your website will have been deployed with GitLab Pages. Your website URL will be available under your **Project**'s **Settings** > **Pages** + +To turn a **project website** forked from the Pages group into a **user/group** website, you'll need to: + +- Rename it to `namespace.gitlab.io`: navigate to **Project**'s **Settings** > **Edit Project** > **Rename repository** +- Adjust your SSG's [base URL](#urls-and-baseurls) to from `"project-name"` to `""`. This setting will be at a different place for each SSG, as each of them have their own structure and file tree. Most likelly, it will be in the SSG's config file. + +> **Notes:** +> +>1. Why do I need to remove the fork relationship? +> +> Unless you want to contribute to the original project, you won't need it connected to the upstream. A [fork](https://about.gitlab.com/2016/12/01/how-to-keep-your-fork-up-to-date-with-its-origin/#fork) is useful for submitting merge requests to the upstream. +> +> 2. Why do I need to enable Shared Runners? +> +> Shared Runners will run the script set by your GitLab CI configuration file. They're enabled by default to new projects, but not to forks. + +#### Create a Project from Scratch + +1. From your **Project**'s **[Dashboard](https://gitlab.com/dashboard/projects)**, click **New project**, and name it considering the [examples above](#practical-examples). +1. Clone it to your local computer, add your website files to your project, add, commit and push to GitLab. +1. From the your **Project**'s page, click **Set up CI**: + + ![add new file]() + +1. Choose one of the templates from the dropbox menu. Pick up the template corresponding to the SSG you're using (or plain HTML). + + ![gitlab-ci templates]() + +Once you have both site files and `.gitlab-ci.yml` in your project's root, GitLab CI will build your site and deploy it with Pages. Once the first build passes, you see your site is live by navigating to your **Project**'s **Settings** > **Pages**, where you'll find its default URL. + +> **Notes:** +> +> - GitLab Pages [supports any SSG](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/), but, if you don't find yours among the templates, you'll need to configure your own `.gitlab-ci.yml`. Do do that, please read through the article [Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](pages_creating_and_tweaking_gitlab-ci_.html). New SSGs are very welcome among the [example projects](https://gitlab.com/pages). If you set up a new one, please [contribute](https://gitlab.com/pages/pages.gitlab.io/blob/master/CONTRIBUTING.md) to our examples. +> +> - The second step _"Clone it to your local computer"_, can be done differently, achieving the same results: instead of cloning the bare repository to you local computer and moving your site files into it, you can run `git init` in your local website directory, add the remote URL: `git remote add origin git@gitlab.com:namespace/project-name.git`, then add, commit, and push. + +### URLs and Baseurls + + + +Every Static Site Generator (SSG) default configuration expects to find your website under a (sub)domain (`example.com`), not in a subdirectory of that domain (`example.com/subdir`). Therefore, whenever you publish a project website (`namespace.gitlab.io/project-name`), you'll have to look for this configuration (base URL) on your SSG's documentation and set it up to reflect this pattern. + +For example, for a Jekyll site, the `baseurl` is defined in the Jekyll configuration file, `_config.yml`. If your website URL is `https://john.gitlab.io/blog/`, you need to add this line to `_config.yml`: + +```yaml +baseurl: "/blog" +``` + +On the contrary, if you deploy your website after forking one of our [default examples](https://gitlab.com/pages), the baseurl will already be configured this way, as all examples there are project websites. If you decide to make yours a user or group website, you'll have to remove this configuration from your project. For the Jekyll example we've just mentioned, you'd have to change Jekyll's `_config.yml` to: + +```yaml +baseurl: "" +``` + +## Further Reading + +- Read through _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](pages_static_sites_domains_dns_records_ssl_tls_certificates.html)_ +- Read through _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](pages_creating_and_tweaking_gitlab-ci_.html)_ -- cgit v1.2.3 From 8ba08213a16aaa9d00b5d7df308212550d90d28e Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 20 Feb 2017 14:29:09 -0600 Subject: remove require.context from diff_notes_bundle --- .../javascripts/diff_notes/diff_notes_bundle.js.es6 | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) 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 190461451d5..cadf8b96b87 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 @@ -1,14 +1,18 @@ -/* eslint-disable func-names, comma-dangle, new-cap, no-new, import/newline-after-import, no-multi-spaces, max-len */ +/* eslint-disable func-names, comma-dangle, new-cap, no-new, max-len */ /* global Vue */ /* global ResolveCount */ -function requireAll(context) { return context.keys().map(context); } const Vue = require('vue'); -requireAll(require.context('./models', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./stores', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./services', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./mixins', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./components', false, /^\.\/.*\.(js|es6)$/)); +require('./models/discussion'); +require('./models/note'); +require('./stores/comments'); +require('./services/resolve'); +require('./mixins/discussion'); +require('./components/comment_resolve_btn'); +require('./components/jump_to_discussion'); +require('./components/resolve_btn'); +require('./components/resolve_count'); +require('./components/resolve_discussion_btn'); $(() => { const projectPath = document.querySelector('.merge-request').dataset.projectPath; -- cgit v1.2.3 From 2a7e2a6447842a0ab152a791b93fe7b980bc7973 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Mon, 20 Feb 2017 17:34:54 -0300 Subject: fix links --- doc/pages/pages_quick_start_guide.md | 4 ++-- .../pages_static_sites_domains_dns_records_ssl_tls_certificates.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/pages/pages_quick_start_guide.md b/doc/pages/pages_quick_start_guide.md index 804b34a8e75..f7b0c3c9520 100644 --- a/doc/pages/pages_quick_start_guide.md +++ b/doc/pages/pages_quick_start_guide.md @@ -6,7 +6,7 @@ - **Part 2: Quick Start Guide - Setting Up GitLab Pages** - _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](pages_static_sites_domains_dns_records_ssl_tls_certificates.html)_ -- _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](pages_creating_and_tweaking_gitlab-ci_.html)_ +- _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](pages_creating_and_tweaking_gitlab-ci.html)_ ---- @@ -109,4 +109,4 @@ baseurl: "" ## Further Reading - Read through _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](pages_static_sites_domains_dns_records_ssl_tls_certificates.html)_ -- Read through _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](pages_creating_and_tweaking_gitlab-ci_.html)_ +- Read through _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](pages_creating_and_tweaking_gitlab-ci.html)_ diff --git a/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md b/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md index 797e7d28469..bb6268bacb9 100644 --- a/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md +++ b/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md @@ -6,7 +6,7 @@ - **Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates** - _[Part 2: Quick Start Guide - Setting Up GitLab Pages](pages_quick_start_guide.html)_ -- _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](pages_creating_and_tweaking_gitlab-ci_.html)_ +- _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](pages_creating_and_tweaking_gitlab-ci.html)_ ---- @@ -161,4 +161,4 @@ Now that you hopefully understand why you need all of this, it's simple: ## Further Reading - Read through GitLab Pages from A to Z _[Part 2: Quick Start Guide - Setting Up GitLab Pages](pages_quick_start_guide.html)_ -- Read through GitLab Pages from A to Z _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](pages_creating_and_tweaking_gitlab-ci_.html)_ +- Read through GitLab Pages from A to Z _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](pages_creating_and_tweaking_gitlab-ci.html)_ -- cgit v1.2.3 From 0b1636daf69fc5eea8dfc5f9cd6426c84ee5e3db Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Mon, 20 Feb 2017 17:35:31 -0300 Subject: add part 3: creating and tweaking CI for Pages --- doc/pages/index.md | 4 +- doc/pages/pages_creating_and_tweaking_gitlab-ci.md | 279 +++++++++++++++++++++ 2 files changed, 281 insertions(+), 2 deletions(-) create mode 100644 doc/pages/pages_creating_and_tweaking_gitlab-ci.md diff --git a/doc/pages/index.md b/doc/pages/index.md index c3dd08b46a7..047fe0477cf 100644 --- a/doc/pages/index.md +++ b/doc/pages/index.md @@ -11,10 +11,10 @@ - Comprehensive Step-by-Step Guide: [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) - GitLab Pages from A to Z - [Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](pages_static_sites_domains_dns_records_ssl_tls_certificates.html) - - [Part 2: Quick Start Guide - Setting Up GitLab Pages](https://gitlab.com/marcia/drafts/blob/master/pages/pages_part-2.md) + - [Part 2: Quick Start Guide - Setting Up GitLab Pages](pages_quick_start_guide.html) - Video tutorial: [How to Publish a Website with GitLab Pages on GitLab.com: from a forked project](#LINK) - Video tutorial: [How to Publish a Website with GitLab Pages on GitLab.com: from scratch](#LINK) - - [Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](#LINK) + - [Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](pages_creating_and_tweaking_gitlab-ci.html) - Secure GitLab Pages Custom Domain with SSL/TLS Certificates - [Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/) - [CloudFlare](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) diff --git a/doc/pages/pages_creating_and_tweaking_gitlab-ci.md b/doc/pages/pages_creating_and_tweaking_gitlab-ci.md new file mode 100644 index 00000000000..8e1877c4981 --- /dev/null +++ b/doc/pages/pages_creating_and_tweaking_gitlab-ci.md @@ -0,0 +1,279 @@ +# GitLab Pages from A to Z: Part 3 + +> Type: user guide +> +> Level: intermediate + +- **Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages** +- _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](pages_static_sites_domains_dns_records_ssl_tls_certificates.html)_ +- _[Part 2: Quick Start Guide - Setting Up GitLab Pages](pages_quick_start_guide.html)_ + +---- + +### Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages + +[GitLab CI](https://about.gitlab.com/gitlab-ci/) serves inumerous purposes, to build, test, and deploy your app from GitLab through [Continuous Integration, Continuous Delivery, and Continuous Deployment](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) methods. You will need it to build your website with GitLab Pages, and deploy it to the Pages server. + +What this file actually does is telling the [GitLab Runner](https://docs.gitlab.com/runner/) to run scripts as you would do from the command line. The Runner acts as your terminal. GitLab CI tells the Runner which commands to run. Both are built-in in GitLab, and you don't need to set up anything for them to work. + +Explaining [every detail of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html) and GitLab Runner is out of the scope of this guide, but we'll need to understand just a few things to be able to write our own `.gitlab-ci.yml` or tweak an existing one. It's an [Yaml](http://docs.ansible.com/ansible/YAMLSyntax.html) file, with its own syntax. You can always check your CI syntax with the [GitLab CI Lint Tool](https://gitlab.com/ci/lint). + +**Practical Example:** + +Let's consider you have a [Jekyll](https://jekyllrb.com/) site. To build it locally, you would open your terminal, and run `jekyll build`. Of course, before building it, you had to install Jekyll in your computer. For that, you had to open your terminal and run `gem install jekyll`. Right? GitLab CI + GitLab Runner do the same thing. But you need to write in the `.gitlab-ci.yml` the script you want to run so the GitLab Runner will do it for you. It looks more complicated then it is. What you need to tell the Runner: + +``` +$ gem install jekyll +$ jekyll build +``` + +#### Script + +To transpose this script to Yaml, it would be like this: + +```yaml +script: + - gem install jekyll + - jekyll build +``` + +#### Job + +So far so good. Now, each `script`, in GitLab is organized by a `job`, which is a bunch of scripts and settings you want to apply to that specific task. + +```yaml +job: + script: + - gem install jekyll + - jekyll build +``` + +For GitLab Pages, this `job` has a specific name, called `pages`, which tells the Runner you want that task to deploy your website with GitLab Pages: + +```yaml +pages: + script: + - gem install jekyll + - jekyll build +``` + +#### `public` Dir + +We also need to tell Jekyll where do you want the website to build, and GitLab Pages will only consider files in a directory called `public`. To do that with Jekyll, we need to add a flag specifying the [destination (`-d`)](https://jekyllrb.com/docs/usage/) of the built website: `jekyll build -d public`. Of course, we need to tell this to our Runner: + +```yaml +pages: + script: + - gem install jekyll + - jekyll build -d public +``` + +#### Artifacts + +We also need to tell the Runner that this _job_ generates _artifacts_, which is the site built by Jekyll. Where are these artifacts stored? In the `public` directory: + +```yaml +pages: + script: + - gem install jekyll + - jekyll build -d public + artifacts: + paths: + - public +``` + +The script above would be enough to build your Jekyll site with GitLab Pages. But, from Jekyll 3.4.0 on, its default template originated by `jekyll new project` requires [Bundler](http://bundler.io/) to install Jekyll dependencies and the default theme. To adjust our script to meet these new requirements, we only need to install and build Jekyll with Bundler: + +```yaml +pages: + script: + - bundle install + - bundle exec jekyll build -d public + artifacts: + paths: + - public +``` + +That's it! A `.gitlab-ci.yml` with the content above would deploy your Jekyll 3.4.0 site with GitLab Pages. This is the minimum configuration for our example. On the steps below, we'll refine the script by adding extra options to our GitLab CI. + +#### Image + +At this point, you probably ask yourself: "okay, but to install Jekyll I need Ruby. Where is Ruby on that script?". The answer is simple: the first thing GitLab Runner will look for in your `.gitlab-ci.yml` is a [Docker](https://www.docker.com/) image specifying what do you need in your container to run that script: + +```yaml +image: ruby:2.3 + +pages: + script: + - bundle install + - bundle exec jekyll build -d public + artifacts: + paths: + - public +``` + +In this case, you're telling the Runner to pull this image, which contains Ruby 2.3 as part of its file system. When you don't specify this image in your configuration, the Runner will use a default image, which is Ruby 2.1. + +If your SSG needs [NodeJS](https://nodejs.org/) to build, you'll need to specify which image you want to use, and this image should contain NodeJS as part of its file system. E.g., for a [Hexo](https://gitlab.com/pages/hexo) site, you can use `image: node:4.2.2`. + +> Note: we're not trying to explain what a Docker image is, we just need to introduce the concept with a minimum viable explanation. To know more about Docker images, please visit their website or take a look at a [summarized explanation](http://paislee.io/how-to-automate-docker-deployments/) here. + +Let's go a little further. + +#### Branching + +If you use GitLab as a version control platform, you will have your branching strategy to work on your project. Meaning, you will have other branches in your project, but you'll want only pushes to the default branch (usually `master`) to be deployed to your website. To do that, we need to add another line to our CI, telling the Runner to only perform that _job_ called `pages` on the `master` branch `only`: + +```yaml +image: ruby:2.3 + +pages: + script: + - bundle install + - bundle exec jekyll build -d public + artifacts: + paths: + - public + only: + - master +``` + +#### Stages + +Another interesting concept to keep in mind are build stages. Your web app can pass through a lot of tests and other tasks until it's deployed to staging or production environments. There are three default stages on GitLab CI: build, test, and deploy. To specify which stage your _job_ is running, simply add another line to your CI: + +```yaml +image: ruby:2.3 + +pages: + stage: deploy + script: + - bundle install + - bundle exec jekyll build -d public + artifacts: + paths: + - public + only: + - master +``` + +You might ask yourself: "why should I bother with stages at all?" Well, let's say you want to be able to test your script and check the built site before deploying your site to production. You want to run the test exactly as your script will do when you push to `master`. It's simple, let's add another task (_job_) to our CI, telling it to test every push to other branches, `except` the `master` branch: + +```yaml +image: ruby:2.3 + +pages: + stage: deploy + script: + - bundle install + - bundle exec jekyll build -d public + artifacts: + paths: + - public + only: + - master + +test: + stage: test + script: + - bundle install + - bundle exec jekyll build -d test + artifacts: + paths: + - test + except: + - master +``` + +The `test` job is running on the stage `test`, Jekyll will build the site in a directory called `test`, and this job will affect all the branches except `master`. + +The best benefit of applying _stages_ to different _jobs_ is that every job in the same stage builds in parallel. So, if your web app needs more than one test before being deployed, you can run all your test at the same time, it's not necessary to wait on test to finish to run the other. Of course, this is just a brief introduction of GitLab CI and GitLab Runner, which are tools much more powerful than that. This is what you need to be able to create and tweak your builds for your GitLab Pages site. + +#### Before Script + +To avoid running the same script multiple times across your _jobs_, you can add the parameter `before_script`, in which you specify which commands you want to run for every single _job_. In our example, notice that we run `bundle install` for both jobs, `pages` and `test`. We don't need to repeat it: + +```yaml +image: ruby:2.3 + +before_script: + - bundle install + +pages: + stage: deploy + script: + - bundle exec jekyll build -d public + artifacts: + paths: + - public + only: + - master + +test: + stage: test + script: + - bundle exec jekyll build -d test + artifacts: + paths: + - test + except: + - master +``` + +#### Caching Dependencies + +If you want to cache the installation files for your projects dependencies, for building faster, you can use the parameter `cache`. For this example, we'll cache Jekyll dependencies in a `vendor` directory when we run `bundle install`: + +```yaml +image: ruby:2.3 + +cache: + paths: + - vendor/ + +before_script: + - bundle install --path vendor + +pages: + stage: deploy + script: + - bundle exec jekyll build -d public + artifacts: + paths: + - public + only: + - master + +test: + stage: test + script: + - bundle exec jekyll build -d test + artifacts: + paths: + - test + except: + - master +``` + +For this specific case, we need to exclude `/vendor` from Jekyll `_config.yml` file, otherwise Jekyll will understand it as a regular directory to build together with the site: + +```yml +exclude: + - vendor +``` + +There we go! Now our GitLab CI not only builds our website, but also **continuously test** pushes to feature-branches, **caches** dependencies installed with Bundler, and **continuously deploy** every push to the `master` branch. + +### Advanced GitLab CI for GitLab Pages + +What you can do with GitLab CI is pretty much up to your creativity. Once you get used to it, you start creating awesome scripts that automate most of tasks you'd do manually in the past. Read through the [documentation of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html) to understand how to go even further on your scripts. + +- On this blog post, understand the concept of [using GitLab CI `environments` to deploy your web app to staging and production](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/). +- On this post, learn [how to run jobs sequentially, in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/) +- On this blog post, we go through the process of [pulling specific directories from different projects](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/) to deploy this website you're looking at, docs.gitlab.com. +- On this blog post, we teach you [how to use GitLab Pages to produce a code coverage report](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/). + +## Further Reading + +- Read through _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](pages_static_sites_domains_dns_records_ssl_tls_certificates.html)_ +- Read through GitLab Pages from A to Z _[Part 2: Quick Start Guide - Setting Up GitLab Pages](pages_quick_start_guide.html)_ -- cgit v1.2.3 From 625fc65b98a7c346afa7aa689566af33c02ff799 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Mon, 20 Feb 2017 19:01:05 -0300 Subject: add link to Matija's video --- .../pages_static_sites_domains_dns_records_ssl_tls_certificates.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md b/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md index bb6268bacb9..56ad4805eda 100644 --- a/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md +++ b/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md @@ -12,7 +12,7 @@ This is a comprehensive guide, made for those who want to publish a website with GitLab Pages but aren't familiar with the entire process involved. -To **enable** GitLab Pages for GitLab CE (Community Edition) and GitLab EE (Enterprise Edition), please read the [admin documentation](https://docs.gitlab.com/ce/administration/pages/index.html), or watch this [video tutorial](#LINK). +To **enable** GitLab Pages for GitLab CE (Community Edition) and GitLab EE (Enterprise Edition), please read the [admin documentation](https://docs.gitlab.com/ce/administration/pages/index.html), or watch this [video tutorial](https://youtu.be/b_qjiabYhmQ). > For this guide, we assume you already have GitLab Pages server up and running for your GitLab instance. -- cgit v1.2.3 From cac59408a58a8c6d2c3ce4fa5a27161a186b5ed5 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 20 Feb 2017 15:01:56 -0600 Subject: add production asset compile step to CI config --- .gitlab-ci.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 433b3119fba..becf2db85fb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -240,6 +240,18 @@ rake db:seed_fu: paths: - log/development.log +rake gitlab:assets:compile: + stage: test + <<: *dedicated-runner + dependencies: [] + variables: + NODE_ENV: "production" + RAILS_ENV: "production" + SETUP_DB: "false" + USE_DB: "false" + SKIP_STORAGE_VALIDATION: "true" + script: bundle exec rake yarn:install gitlab:assets:compile + rake karma: cache: paths: -- cgit v1.2.3 From 3a0f668f1c01b72ee4729e6b5a5b148ada596b97 Mon Sep 17 00:00:00 2001 From: William Abernathy Date: Tue, 21 Feb 2017 00:48:26 +0000 Subject: Replace create_new_project_button.png --- doc/gitlab-basics/img/create_new_project_button.png | Bin 4196 -> 6978 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/gitlab-basics/img/create_new_project_button.png b/doc/gitlab-basics/img/create_new_project_button.png index a19f0e57b56..8d7a69e55ed 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 -- cgit v1.2.3 From 2a12cbe6d6d5c7c78c6fac64e7d5a6af6742462a Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Tue, 21 Feb 2017 12:07:50 +1100 Subject: Improved specs --- spec/features/issuables/issuable_list_spec.rb | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb index ce4dca1175f..4ea801cd1ac 100644 --- a/spec/features/issuables/issuable_list_spec.rb +++ b/spec/features/issuables/issuable_list_spec.rb @@ -33,6 +33,7 @@ describe 'issuable list', feature: true do it "counts merge requests closing issues icons for each issue" do visit_issuable_list(:issue) + expect(page).to have_selector('.icon-merge-request-unmerged', count: 1) expect(first('.icon-merge-request-unmerged').find(:xpath, '..')).to have_content(1) end @@ -48,13 +49,6 @@ describe 'issuable list', feature: true do 3.times do if issuable_type == :issue issuable = create(:issue, project: project, author: user) - merge_request = create(:merge_request, - title: FFaker::Lorem.sentence, - description: "Closes #{issuable.to_reference}", - source_project: project, - source_branch: FFaker::Name.name) - - MergeRequestsClosingIssues.create!(issue: issuable, merge_request: merge_request) else issuable = create(:merge_request, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name) end @@ -65,6 +59,16 @@ describe 'issuable list', feature: true do create(:award_emoji, :downvote, awardable: issuable) create(:award_emoji, :upvote, awardable: issuable) + + if issuable_type == :issue + issue = Issue.reorder(:iid).first + merge_request = create(:merge_request, + title: FFaker::Lorem.sentence, + source_project: project, + source_branch: FFaker::Name.name) + + MergeRequestsClosingIssues.create!(issue: issue, merge_request: merge_request) if MergeRequestsClosingIssues.count.zero? + end end end end -- cgit v1.2.3 From 1eb72a71f54da310b2277e5890dce27c15e11036 Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Tue, 21 Feb 2017 12:45:08 +1100 Subject: Refactored count_for_collection() for using pluck instead of select --- app/controllers/concerns/issuable_collections.rb | 4 ++-- app/models/merge_requests_closing_issues.rb | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index d7d781cbe72..85ae4985e58 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -28,13 +28,13 @@ module IssuableCollections downvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.downvote? } upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? } notes = issuable_note_count.find { |notes| notes.noteable_id == id } - merge_requests = issuable_merge_requests_count.find { |mr| mr.issue_id == id } + merge_requests = issuable_merge_requests_count.find { |mr| mr.first == id } issuable_meta[id] = Issuable::IssuableMeta.new( upvotes.try(:count).to_i, downvotes.try(:count).to_i, notes.try(:count).to_i, - merge_requests.try(:count).to_i + merge_requests.try(:last).to_i ) end end diff --git a/app/models/merge_requests_closing_issues.rb b/app/models/merge_requests_closing_issues.rb index 1ecdfd1dfdb..97210900bd5 100644 --- a/app/models/merge_requests_closing_issues.rb +++ b/app/models/merge_requests_closing_issues.rb @@ -7,9 +7,9 @@ class MergeRequestsClosingIssues < ActiveRecord::Base class << self def count_for_collection(ids) - select('issue_id', 'COUNT(*) as count'). - group(:issue_id). - where(issue_id: ids) + group(:issue_id). + where(issue_id: ids). + pluck('issue_id', 'COUNT(*) as count') end end end -- cgit v1.2.3 From 2807cafe41de14d343f1c97edecd2e531e0f1fd2 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Mon, 20 Feb 2017 23:05:52 -0300 Subject: add video tutorial - Pages admin --- doc/pages/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/pages/index.md b/doc/pages/index.md index 047fe0477cf..11b2582841f 100644 --- a/doc/pages/index.md +++ b/doc/pages/index.md @@ -37,3 +37,4 @@ - [User docs](../user/project/pages/) - [Admin docs](administration.html) + - Video tutorial - [How to Enable GitLab Pages for GitLab CE and EE](https://youtu.be/dD8c7WNcc6s) -- cgit v1.2.3 From b90a1656e486deddb29dbc4bbfce3f663f7484bc Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Mon, 20 Feb 2017 23:06:00 -0300 Subject: update link --- .../pages_static_sites_domains_dns_records_ssl_tls_certificates.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md b/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md index 56ad4805eda..1a4e798df66 100644 --- a/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md +++ b/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md @@ -12,7 +12,7 @@ This is a comprehensive guide, made for those who want to publish a website with GitLab Pages but aren't familiar with the entire process involved. -To **enable** GitLab Pages for GitLab CE (Community Edition) and GitLab EE (Enterprise Edition), please read the [admin documentation](https://docs.gitlab.com/ce/administration/pages/index.html), or watch this [video tutorial](https://youtu.be/b_qjiabYhmQ). +To **enable** GitLab Pages for GitLab CE (Community Edition) and GitLab EE (Enterprise Edition), please read the [admin documentation](https://docs.gitlab.com/ce/administration/pages/index.html), or watch this [video tutorial](https://youtu.be/dD8c7WNcc6s). > For this guide, we assume you already have GitLab Pages server up and running for your GitLab instance. -- cgit v1.2.3 From 7e648110ed09bd12e1d405fbeba8dcb0590de602 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 10 Feb 2017 16:13:13 +0000 Subject: Added tooltip to add issues button on issue boards Closes #27985 --- app/assets/javascripts/boards/boards_bundle.js.es6 | 42 ++++++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index 878ad1b6031..d194dc9b188 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -93,17 +93,53 @@ $(() => { modal: ModalStore.store, store: Store.state, }, + watch: { + disabled() { + this.updateTooltip(); + }, + }, computed: { disabled() { return !this.store.lists.filter(list => list.type !== 'blank' && list.type !== 'done').length; }, + tooltipTitle() { + if (this.disabled) { + return 'Please add a list to your board first'; + } + + return ''; + }, + }, + methods: { + updateTooltip() { + const $tooltip = $(this.$el); + + this.$nextTick(() => { + if (this.disabled) { + $tooltip.tooltip(); + } else { + $tooltip.tooltip('destroy'); + } + }); + }, + openModal() { + if (!this.disabled) { + this.toggleModal(true); + } + }, + }, + mounted() { + this.updateTooltip(); }, template: ` `, -- cgit v1.2.3 From 216d3ad7cba15016ffd038645fe0db0d91ce84f3 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 10 Feb 2017 16:19:00 +0000 Subject: Added CHANGELOG --- changelogs/unreleased/add-issues-tooltip.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/add-issues-tooltip.yml diff --git a/changelogs/unreleased/add-issues-tooltip.yml b/changelogs/unreleased/add-issues-tooltip.yml new file mode 100644 index 00000000000..58adb6c6b5a --- /dev/null +++ b/changelogs/unreleased/add-issues-tooltip.yml @@ -0,0 +1,4 @@ +--- +title: Disabled tooltip on add issues button in usse boards +merge_request: +author: -- cgit v1.2.3 From 8ed25c80003b327b359079d600f7207db46846b4 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 10 Feb 2017 16:38:11 +0000 Subject: Added tests --- spec/features/boards/add_issues_modal_spec.rb | 6 ++++++ spec/features/boards/boards_spec.rb | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb index 2875fc1e533..a3e24bb5ffa 100644 --- a/spec/features/boards/add_issues_modal_spec.rb +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -49,6 +49,12 @@ describe 'Issue Boards add issue modal', :feature, :js do expect(page).not_to have_selector('.add-issues-modal') end + + it 'does not show tooltip on add issues button' do + button = page.find('.issue-boards-search button', text: 'Add issues') + + expect(button[:title]).not_to eq("Please add a list to your board first") + end end context 'issues list' do diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 1b25b51cfb2..e247bfa2980 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -28,10 +28,10 @@ describe 'Issue Boards', feature: true, js: true do expect(page).to have_content('Welcome to your Issue Board!') end - it 'disables add issues button by default' do + it 'shows tooltip on add issues button' do button = page.find('.issue-boards-search button', text: 'Add issues') - expect(button[:disabled]).to eq true + expect(button[:"data-original-title"]).to eq("Please add a list to your board first") end it 'hides the blank state when clicking nevermind button' do -- cgit v1.2.3 From 1f244284284607946b2758c4e9ef26559cd8d71c Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 21 Feb 2017 17:08:35 +0800 Subject: Fix build attributes test, see: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9021#note_23782188 This is discovered by https://gitlab.com/gitlab-org/gitlab-ce/builds/10815456 I removed `gl_project_id` and this is failing. I took some look, realizing that: * `trace` is duplicated in `attributes` * `tag_list` is not included in `build.attributes` * `artifacts_expire_at` is missing in `attributes` So we need to: * Remove duplicated `trace` in `attributes` (40 -> 39) * Remove `tag_list` in `attributes` (39 -> 38) * Add `artifacts_expire_at` to `attributes` (38 -> 39) * Add `gl_project_id` to `attributes` (39 -> 40) --- app/services/ci/retry_build_service.rb | 7 ++++--- spec/services/ci/retry_build_service_spec.rb | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb index 4b47ee489cf..4ce9cabeda3 100644 --- a/app/services/ci/retry_build_service.rb +++ b/app/services/ci/retry_build_service.rb @@ -1,16 +1,17 @@ module Ci class RetryBuildService < ::BaseService - CLONE_ATTRIBUTES = %i[pipeline ref tag options commands tag_list name + CLONE_ATTRIBUTES = %i[pipeline ref tag options commands name allow_failure stage stage_idx trigger_request yaml_variables when environment coverage_regex] .freeze REJECT_ATTRIBUTES = %i[id status user token coverage trace runner - artifacts_file artifacts_metadata artifacts_size + artifacts_expire_at artifacts_file + artifacts_metadata artifacts_size created_at updated_at started_at finished_at queued_at erased_by erased_at].freeze - IGNORE_ATTRIBUTES = %i[trace type lock_version project target_url + IGNORE_ATTRIBUTES = %i[type lock_version project gl_project_id target_url deploy job_id description].freeze def execute(build) diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index 93147870afe..d03f7505eac 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -12,7 +12,7 @@ describe Ci::RetryBuildService, :services do shared_examples 'build duplication' do let(:build) do - create(:ci_build, :failed, :artifacts, :erased, :trace, + create(:ci_build, :failed, :artifacts_expired, :erased, :trace, :queued, :coverage, pipeline: pipeline) end @@ -38,7 +38,7 @@ describe Ci::RetryBuildService, :services do described_class::IGNORE_ATTRIBUTES + described_class::REJECT_ATTRIBUTES - expect(attributes.size).to eq build.attributes.size + expect(build.attributes.size).to eq(attributes.size) end end -- cgit v1.2.3 From 9e6b2c5dd4a1a58ca62650b6f529213bc730f64f Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 21 Feb 2017 18:36:50 +0800 Subject: We actually want to clone project and remove gl_project_id in the future. Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9409#note_23859361 --- app/services/ci/retry_build_service.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb index 4ce9cabeda3..38ef323f6e5 100644 --- a/app/services/ci/retry_build_service.rb +++ b/app/services/ci/retry_build_service.rb @@ -1,6 +1,6 @@ module Ci class RetryBuildService < ::BaseService - CLONE_ATTRIBUTES = %i[pipeline ref tag options commands name + CLONE_ATTRIBUTES = %i[pipeline project ref tag options commands name allow_failure stage stage_idx trigger_request yaml_variables when environment coverage_regex] .freeze @@ -11,7 +11,7 @@ module Ci created_at updated_at started_at finished_at queued_at erased_by erased_at].freeze - IGNORE_ATTRIBUTES = %i[type lock_version project gl_project_id target_url + IGNORE_ATTRIBUTES = %i[type lock_version gl_project_id target_url deploy job_id description].freeze def execute(build) -- cgit v1.2.3 From 2b0426a4971480c6d9868ac6865d3827485fe8d1 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 21 Feb 2017 18:46:52 +0800 Subject: Fix tests due to error key changed --- app/models/application_setting.rb | 2 +- spec/models/application_setting_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index b5ffc5f9ca9..1349587fb0a 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -309,6 +309,6 @@ class ApplicationSetting < ActiveRecord::Base def check_default_artifacts_expire_in ChronicDuration.parse(default_artifacts_expire_in) rescue ChronicDuration::DurationParseError - errors.add(:default_artifacts_expiration, "is not a correct duration") + errors.add(:default_artifacts_expire_in, "is not a correct duration") end end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index f8c38ed8569..b3eb206612e 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -59,7 +59,7 @@ describe ApplicationSetting, models: true do def expect_invalid expect(setting).to be_invalid expect(setting.errors.messages) - .to have_key(:default_artifacts_expiration) + .to have_key(:default_artifacts_expire_in) end end -- cgit v1.2.3 From 64f20dc1b5fddd56d05cf5a8e59f82cbd231b10c Mon Sep 17 00:00:00 2001 From: heapifyman Date: Tue, 21 Feb 2017 11:46:31 +0000 Subject: Update manage_large_binaries_with_git_lfs.md Add note to include .gitattributes file in repository - see instructions here: https://git-lfs.github.com/ --- doc/workflow/lfs/manage_large_binaries_with_git_lfs.md | 6 ++++++ 1 file changed, 6 insertions(+) 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 8c5020bee37..3e48c88f55f 100644 --- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md +++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md @@ -63,6 +63,12 @@ git commit -am "Added Debian iso" # commit the file meta data git push origin master # sync the git repo and large file to the GitLab server ``` +>**Note**: Make sure that ```.gitattributes``` is tracked by git. Otherwise Git + LFS will not be working properly for people cloning the project. + ```bash + git add .gitattributes + ``` + Cloning the repository works the same as before. Git automatically detects the LFS-tracked files and clones them via HTTP. If you performed the git clone command with a SSH URL, you have to enter your GitLab credentials for HTTP -- cgit v1.2.3 From 63bd79f594272d98ce022685f26a47ce0afae9d4 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Tue, 21 Feb 2017 09:53:44 +0100 Subject: Chat slash commands show labels correctly --- changelogs/unreleased/zj-fix-slash-command-labels.yml | 4 ++++ lib/gitlab/chat_commands/presenters/issue_base.rb | 2 +- .../gitlab/chat_commands/presenters/issue_show_spec.rb | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/zj-fix-slash-command-labels.yml diff --git a/changelogs/unreleased/zj-fix-slash-command-labels.yml b/changelogs/unreleased/zj-fix-slash-command-labels.yml new file mode 100644 index 00000000000..93b7194dd4e --- /dev/null +++ b/changelogs/unreleased/zj-fix-slash-command-labels.yml @@ -0,0 +1,4 @@ +--- +title: Chat slash commands show labels correctly +merge_request: +author: diff --git a/lib/gitlab/chat_commands/presenters/issue_base.rb b/lib/gitlab/chat_commands/presenters/issue_base.rb index a0058407fb2..054f7f4be0c 100644 --- a/lib/gitlab/chat_commands/presenters/issue_base.rb +++ b/lib/gitlab/chat_commands/presenters/issue_base.rb @@ -32,7 +32,7 @@ module Gitlab }, { title: "Labels", - value: @resource.labels.any? ? @resource.label_names : "_None_", + value: @resource.labels.any? ? @resource.label_names.join(', ') : "_None_", short: true } ] diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb index 5b678d31fce..3916fc704a4 100644 --- a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb +++ b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb @@ -26,6 +26,21 @@ describe Gitlab::ChatCommands::Presenters::IssueShow do end end + context 'with labels' do + let(:label) { create(:label, project: project, title: 'mep') } + let(:label1) { create(:label, project: project, title: 'mop') } + + before do + issue.labels << [label, label1] + end + + it 'shows the labels' do + labels = attachment[:fields].find { |f| f[:title] == 'Labels' } + + expect(labels[:value]).to eq("mep, mop") + end + end + context 'confidential issue' do let(:issue) { create(:issue, project: project) } -- cgit v1.2.3 From 85c87be361e804977e7924ea9e464d0752cd2f24 Mon Sep 17 00:00:00 2001 From: George Andrinopoulos Date: Tue, 21 Feb 2017 15:44:21 +0200 Subject: Add spec for todo with target_type Commit --- .../22951-fix-todos-api-endpoint-error-for-commits.yml | 4 ++++ lib/api/entities.rb | 7 ++----- spec/factories/todos.rb | 9 +++++++++ spec/requests/api/todos_spec.rb | 4 ++-- 4 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 changelogs/unreleased/22951-fix-todos-api-endpoint-error-for-commits.yml diff --git a/changelogs/unreleased/22951-fix-todos-api-endpoint-error-for-commits.yml b/changelogs/unreleased/22951-fix-todos-api-endpoint-error-for-commits.yml new file mode 100644 index 00000000000..a53e7d77c16 --- /dev/null +++ b/changelogs/unreleased/22951-fix-todos-api-endpoint-error-for-commits.yml @@ -0,0 +1,4 @@ +--- +title: Add spec for todo with target_type Commit +merge_request: 9351 +author: George Andrinopoulos diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 4484ea92d48..536d3ee49d4 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -397,11 +397,8 @@ module API expose :target_type expose :target do |todo, options| - if todo.target_type == 'Commit' - Entities.const_get('RepoCommit').represent(todo.target, options) - else - Entities.const_get(todo.target_type).represent(todo.target, options) - end + target = todo.target_type == 'Commit' ? 'RepoCommit' : todo.target_type + Entities.const_get(target).represent(todo.target, options) end expose :target_url do |todo, options| diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb index b4e4cd97780..a2cedda0eec 100644 --- a/spec/factories/todos.rb +++ b/spec/factories/todos.rb @@ -44,4 +44,13 @@ FactoryGirl.define do state :done end end + + factory :on_commit_todo, class: Todo do + project factory: :empty_project + author + user + action { Todo::ASSIGNED } + commit_id RepoHelpers.sample_commit.id + target_type "Commit" + end end diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb index 56dc017ce54..baf6e4d98d3 100644 --- a/spec/requests/api/todos_spec.rb +++ b/spec/requests/api/todos_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe API::Todos, api: true do include ApiHelpers - let(:project_1) { create(:empty_project) } + let(:project_1) { create(:empty_project, :test_repo) } let(:project_2) { create(:empty_project) } let(:author_1) { create(:user) } let(:author_2) { create(:user) } @@ -11,7 +11,7 @@ describe API::Todos, api: true do let(:merge_request) { create(:merge_request, source_project: project_1) } let!(:pending_1) { create(:todo, :mentioned, project: project_1, author: author_1, user: john_doe) } let!(:pending_2) { create(:todo, project: project_2, author: author_2, user: john_doe) } - let!(:pending_3) { create(:todo, project: project_1, author: author_2, user: john_doe) } + let!(:pending_3) { create(:on_commit_todo, project: project_1, author: author_2, user: john_doe) } let!(:done) { create(:todo, :done, project: project_1, author: author_1, user: john_doe) } before do -- cgit v1.2.3 From 6a2ee01b552493da7753527b0de6cfb83b498622 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 21 Feb 2017 16:00:33 +0000 Subject: Removes label from previous list When dragging an issue to a list that it already exists in it was previously not removing the label for the list it was moving from. This changes that to make that API call. Closes #28484 --- app/assets/javascripts/boards/models/list.js.es6 | 12 ++++++++---- .../javascripts/boards/stores/boards_store.js.es6 | 5 ++++- .../moving-issue-with-two-list-labels.yml | 4 ++++ spec/javascripts/boards/list_spec.js.es6 | 21 +++++++++++++++++++++ 4 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 changelogs/unreleased/moving-issue-with-two-list-labels.yml diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index 5152be56b66..8158ed4ec2c 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -123,14 +123,18 @@ class List { if (listFrom) { this.issuesSize += 1; - gl.boardService.moveIssue(issue.id, listFrom.id, this.id) - .then(() => { - listFrom.getIssues(false); - }); + this.updateIssueLabel(issue, listFrom); } } } + updateIssueLabel(issue, listFrom) { + gl.boardService.moveIssue(issue.id, listFrom.id, this.id) + .then(() => { + listFrom.getIssues(false); + }); + } + findIssue (id) { return this.issues.filter(issue => issue.id === id)[0]; } diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index 50842ecbaaa..56436c8fdc7 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -92,9 +92,12 @@ const issueLists = issue.getLists(); const listLabels = issueLists.map(listIssue => listIssue.label); - // Add to new lists issues if it doesn't already exist if (!issueTo) { + // Add to new lists issues if it doesn't already exist listTo.addIssue(issue, listFrom, newIndex); + } else { + listTo.updateIssueLabel(issue, listFrom); + issueTo.removeLabel(listFrom.label); } if (listTo.type === 'done') { diff --git a/changelogs/unreleased/moving-issue-with-two-list-labels.yml b/changelogs/unreleased/moving-issue-with-two-list-labels.yml new file mode 100644 index 00000000000..d5ea81e3810 --- /dev/null +++ b/changelogs/unreleased/moving-issue-with-two-list-labels.yml @@ -0,0 +1,4 @@ +--- +title: Removes label when moving issue to another list that it is currently in +merge_request: +author: diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6 index 4397a32fedc..c8a18af7198 100644 --- a/spec/javascripts/boards/list_spec.js.es6 +++ b/spec/javascripts/boards/list_spec.js.es6 @@ -3,7 +3,9 @@ /* global boardsMockInterceptor */ /* global BoardService */ /* global List */ +/* global ListIssue */ /* global listObj */ +/* global listObjDuplicate */ require('~/lib/utils/url_utility'); require('~/boards/models/issue'); @@ -84,4 +86,23 @@ describe('List model', () => { done(); }, 0); }); + + it('sends service request to update issue label', () => { + const listDup = new List(listObjDuplicate); + const issue = new ListIssue({ + title: 'Testing', + iid: 1, + confidential: false, + labels: [list.label, listDup.label] + }); + + list.issues.push(issue); + listDup.issues.push(issue); + + spyOn(gl.boardService, 'moveIssue').and.callThrough(); + + listDup.updateIssueLabel(list, issue); + + expect(gl.boardService.moveIssue).toHaveBeenCalledWith(issue.id, list.id, listDup.id); + }); }); -- cgit v1.2.3 From c468ad75f4dba1833b139e7949dc0fa0c75c74dc Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Tue, 21 Feb 2017 13:20:38 -0600 Subject: Keep right padding the same whether sidebar is open or not --- app/assets/stylesheets/framework/sidebar.scss | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index d09b1c9d7f5..040a7ce0c16 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -29,14 +29,16 @@ } } +@media (min-width: $screen-sm-min) { + .content-wrapper { + padding-right: $gutter_collapsed_width; + } +} + .right-sidebar-collapsed { padding-right: 0; @media (min-width: $screen-sm-min) { - .content-wrapper { - padding-right: $gutter_collapsed_width; - } - .merge-request-tabs-holder.affix { right: $gutter_collapsed_width; } @@ -54,12 +56,6 @@ .right-sidebar-expanded { padding-right: 0; - @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { - &:not(.build-sidebar):not(.wiki-sidebar) { - padding-right: $gutter_collapsed_width; - } - } - @media (min-width: $screen-md-min) { .content-wrapper { padding-right: $gutter_width; -- cgit v1.2.3 From 53312250054a28fc5e5a7df6ef971995c20697dd Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Tue, 21 Feb 2017 19:35:08 +0000 Subject: Revert "Merge branch '27934-left-align-nav' into 'master'" This reverts merge request !9338 --- app/assets/stylesheets/framework/header.scss | 26 +++++- app/assets/stylesheets/framework/nav.scss | 10 ++- app/assets/stylesheets/pages/projects.scss | 30 ++++--- app/views/layouts/_page.html.haml | 2 +- app/views/layouts/header/_default.html.haml | 4 +- app/views/projects/show.html.haml | 113 ++++++++++++------------- changelogs/unreleased/27934-left-align-nav.yml | 4 - 7 files changed, 105 insertions(+), 84 deletions(-) delete mode 100644 changelogs/unreleased/27934-left-align-nav.yml diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 78434b99b62..3945a789c82 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -148,11 +148,16 @@ header { } .header-logo { - display: inline-block; - margin: 0 8px 0 3px; - position: relative; + position: absolute; + left: 50%; top: 7px; transition-duration: .3s; + z-index: 999; + + #logo { + position: relative; + left: -50%; + } svg, img { @@ -162,6 +167,15 @@ header { &:hover { cursor: pointer; } + + @media (max-width: $screen-xs-max) { + right: 20px; + left: auto; + + #logo { + left: auto; + } + } } .title { @@ -169,7 +183,7 @@ header { padding-right: 20px; margin: 0; font-size: 18px; - max-width: 450px; + max-width: 385px; display: inline-block; line-height: $header-height; font-weight: normal; @@ -179,6 +193,10 @@ header { vertical-align: top; white-space: nowrap; + @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + max-width: 300px; + } + @media (max-width: $screen-xs-max) { max-width: 190px; } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 7d4a814a36c..674d3bb45aa 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -1,7 +1,7 @@ @mixin fade($gradient-direction, $gradient-color) { visibility: hidden; opacity: 0; - z-index: 1; + z-index: 2; position: absolute; bottom: 12px; width: 43px; @@ -18,7 +18,7 @@ .fa { position: relative; - top: 6px; + top: 5px; font-size: 18px; } } @@ -79,6 +79,7 @@ } &.sub-nav { + text-align: center; background-color: $gray-normal; .container-fluid { @@ -286,6 +287,7 @@ background: $gray-light; border-bottom: 1px solid $border-color; transition: padding $sidebar-transition-duration; + text-align: center; .container-fluid { position: relative; @@ -351,7 +353,7 @@ right: -5px; .fa { - right: -28px; + right: -7px; } } @@ -381,7 +383,7 @@ left: 0; .fa { - left: -4px; + left: 10px; } } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 8c0de314420..67110813abb 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -652,23 +652,29 @@ pre.light-well { } } -.container-fluid.project-stats-container { - @media (max-width: $screen-xs-max) { - padding: 12px 0; - } -} - .project-last-commit { - background-color: $gray-light; - padding: 12px $gl-padding; - border: 1px solid $border-color; - @media (min-width: $screen-sm-min) { margin-top: $gl-padding; } - @media (min-width: $screen-sm-min) { - border-radius: $border-radius-base; + &.container-fluid { + padding-top: 12px; + padding-bottom: 12px; + background-color: $gray-light; + border: 1px solid $border-color; + border-right-width: 0; + border-left-width: 0; + + @media (min-width: $screen-sm-min) { + border-right-width: 1px; + border-left-width: 1px; + } + } + + &.container-limited { + @media (min-width: 1281px) { + border-radius: $border-radius-base; + } } .ci-status { diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 1717ed6b365..a35a918d501 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,7 +1,7 @@ .page-with-sidebar{ class: page_gutter_class } - if defined?(nav) && nav .layout-nav - %div{ class: container_class } + .container-fluid = render "layouts/nav/#{nav}" .content-wrapper{ class: "#{layout_nav_class}" } = yield :sub_nav diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 60b9b8bdbc4..ddf50d6667f 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -61,12 +61,12 @@ %div = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success' + %h1.title= title + .header-logo = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do = brand_header_logo - %h1.title= title - = yield :header_content = render 'shared/outdated_browser' diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index f7419728719..80d4081dd7b 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -13,70 +13,69 @@ = render "home_panel" - if current_user && can?(current_user, :download_code, @project) - .project-stats-container{ class: container_class } - %nav.project-stats - %ul.nav - %li - = link_to project_files_path(@project) do - Files (#{storage_counter(@project.statistics.total_repository_size)}) - %li - = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do - #{'Commit'.pluralize(@project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)}) - %li - = link_to namespace_project_branches_path(@project.namespace, @project) do - #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)}) - %li - = link_to namespace_project_tags_path(@project.namespace, @project) do - #{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)}) + %nav.project-stats{ class: container_class } + %ul.nav + %li + = link_to project_files_path(@project) do + Files (#{storage_counter(@project.statistics.total_repository_size)}) + %li + = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do + #{'Commit'.pluralize(@project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)}) + %li + = link_to namespace_project_branches_path(@project.namespace, @project) do + #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)}) + %li + = link_to namespace_project_tags_path(@project.namespace, @project) do + #{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)}) - - if default_project_view != 'readme' && @repository.readme - %li - = link_to 'Readme', readme_path(@project) + - if default_project_view != 'readme' && @repository.readme + %li + = link_to 'Readme', readme_path(@project) - - if @repository.changelog - %li - = link_to 'Changelog', changelog_path(@project) + - if @repository.changelog + %li + = link_to 'Changelog', changelog_path(@project) - - if @repository.license_blob - %li - = link_to license_short_name(@project), license_path(@project) + - if @repository.license_blob + %li + = link_to license_short_name(@project), license_path(@project) - - if @repository.contribution_guide - %li - = link_to 'Contribution guide', contribution_guide_path(@project) + - if @repository.contribution_guide + %li + = link_to 'Contribution guide', contribution_guide_path(@project) - - if @repository.gitlab_ci_yml - %li - = link_to 'CI configuration', ci_configuration_path(@project) + - if @repository.gitlab_ci_yml + %li + = link_to 'CI configuration', ci_configuration_path(@project) - - if current_user && can_push_branch?(@project, @project.default_branch) - - unless @repository.changelog - %li.missing - = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do - Add Changelog - - unless @repository.license_blob - %li.missing - = link_to add_special_file_path(@project, file_name: 'LICENSE') do - Add License - - unless @repository.contribution_guide - %li.missing - = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do - Add Contribution guide - - unless @repository.gitlab_ci_yml - %li.missing - = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do - Set up CI - - if koding_enabled? && @repository.koding_yml.blank? - %li.missing - = link_to 'Set up Koding', add_koding_stack_path(@project) - - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present? - %li.missing - = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', target_branch: 'auto-deploy', context: 'autodeploy') do - Set up auto deploy + - if current_user && can_push_branch?(@project, @project.default_branch) + - unless @repository.changelog + %li.missing + = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do + Add Changelog + - unless @repository.license_blob + %li.missing + = link_to add_special_file_path(@project, file_name: 'LICENSE') do + Add License + - unless @repository.contribution_guide + %li.missing + = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do + Add Contribution guide + - unless @repository.gitlab_ci_yml + %li.missing + = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do + Set up CI + - if koding_enabled? && @repository.koding_yml.blank? + %li.missing + = link_to 'Set up Koding', add_koding_stack_path(@project) + - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present? + %li.missing + = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', target_branch: 'auto-deploy', context: 'autodeploy') do + Set up auto deploy - - if @repository.commit - .project-last-commit - = render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project + - if @repository.commit + .project-last-commit{ class: container_class } + = render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project %div{ class: container_class } - if @project.archived? diff --git a/changelogs/unreleased/27934-left-align-nav.yml b/changelogs/unreleased/27934-left-align-nav.yml deleted file mode 100644 index 6c45e4ce175..00000000000 --- a/changelogs/unreleased/27934-left-align-nav.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Left align navigation -merge_request: -author: -- cgit v1.2.3 From 334f3283f53905c11b9eafc6b3de8f12f661adaf Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Tue, 21 Feb 2017 16:42:12 -0300 Subject: add link to video: pages quick start from forked repo --- doc/pages/index.md | 2 +- doc/pages/pages_quick_start_guide.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/pages/index.md b/doc/pages/index.md index 11b2582841f..54a951f0a2a 100644 --- a/doc/pages/index.md +++ b/doc/pages/index.md @@ -12,7 +12,7 @@ - GitLab Pages from A to Z - [Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](pages_static_sites_domains_dns_records_ssl_tls_certificates.html) - [Part 2: Quick Start Guide - Setting Up GitLab Pages](pages_quick_start_guide.html) - - Video tutorial: [How to Publish a Website with GitLab Pages on GitLab.com: from a forked project](#LINK) + - Video tutorial: [How to Publish a Website with GitLab Pages on GitLab.com: from a forked project](https://youtu.be/TWqh9MtT4Bg) - Video tutorial: [How to Publish a Website with GitLab Pages on GitLab.com: from scratch](#LINK) - [Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](pages_creating_and_tweaking_gitlab-ci.html) - Secure GitLab Pages Custom Domain with SSL/TLS Certificates diff --git a/doc/pages/pages_quick_start_guide.md b/doc/pages/pages_quick_start_guide.md index f7b0c3c9520..945e350e235 100644 --- a/doc/pages/pages_quick_start_guide.md +++ b/doc/pages/pages_quick_start_guide.md @@ -41,7 +41,7 @@ Let's go over both options. To make things easy for you, we've created this [group](https://gitlab.com/pages) of default projects containing the most popular SSGs templates. -Watch the [video tutorial](#LINK) we've created for the steps below. +Watch the [video tutorial](https://youtu.be/TWqh9MtT4Bg) we've created for the steps below. 1. Choose your SSG template 1. Fork a project from the [Pages group](https://gitlab.com/pages) -- cgit v1.2.3 From 69b2bc4d5987fea42da13db5f7d6b1e89d44ebf6 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Tue, 21 Feb 2017 16:46:00 -0300 Subject: minor change [ci skip] --- .../pages_static_sites_domains_dns_records_ssl_tls_certificates.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md b/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md index 1a4e798df66..d32c8b76558 100644 --- a/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md +++ b/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md @@ -12,7 +12,7 @@ This is a comprehensive guide, made for those who want to publish a website with GitLab Pages but aren't familiar with the entire process involved. -To **enable** GitLab Pages for GitLab CE (Community Edition) and GitLab EE (Enterprise Edition), please read the [admin documentation](https://docs.gitlab.com/ce/administration/pages/index.html), or watch this [video tutorial](https://youtu.be/dD8c7WNcc6s). +To **enable** GitLab Pages for GitLab CE (Community Edition) and GitLab EE (Enterprise Edition), please read the [admin documentation](https://docs.gitlab.com/ce/administration/pages/index.html), and/or watch this [video tutorial](https://youtu.be/dD8c7WNcc6s). > For this guide, we assume you already have GitLab Pages server up and running for your GitLab instance. -- cgit v1.2.3 From 601b5adf5768b548a5810ffe890b38f7f4980d89 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 21 Feb 2017 19:34:03 +0000 Subject: Use a btn-group to group all action buttons --- .../components/environment_actions.js.es6 | 38 +++++++++++----------- .../components/environment_item.js.es6 | 25 ++++---------- app/assets/stylesheets/pages/environments.scss | 18 +++++++++- 3 files changed, 42 insertions(+), 39 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6 index c5a714d9673..978d4dd8b6b 100644 --- a/app/assets/javascripts/environments/components/environment_actions.js.es6 +++ b/app/assets/javascripts/environments/components/environment_actions.js.es6 @@ -15,29 +15,29 @@ module.exports = Vue.component('actions-component', { }, template: ` -
- + + {{action.name}} + + + + + +
`, }); diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 24fd58a301a..ad9d1d21a79 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -505,39 +505,26 @@ module.exports = Vue.component('environment-item', {
-
- + -
-
- -
-
- -
-
- -
-
- diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 181dcb7721f..f789ae1ccd3 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -35,7 +35,6 @@ display: table-cell; } - .environments-name, .environments-commit, .environments-actions { width: 20%; @@ -45,6 +44,7 @@ width: 10%; } + .environments-name, .environments-deploy, .environments-build { width: 15%; @@ -62,6 +62,22 @@ } } + .btn-group { + + > a { + color: $gl-text-color-secondary; + } + + svg path { + fill: $gl-text-color-secondary; + } + + .dropdown { + outline: none; + } + } + + .commit-title { margin: 0; } -- cgit v1.2.3 From 1e73657e5b2f07fc5a904f24f979be244a054f97 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Tue, 21 Feb 2017 16:58:09 -0300 Subject: add video to admin docs --- doc/administration/pages/index.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 8de0cc5af5c..1c444cf0d50 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -102,6 +102,8 @@ The Pages daemon doesn't listen to the outside world. 1. [Reconfigure GitLab][reconfigure] +Watch the [video tutorial][video-admin] for this configuration. + ### Wildcard domains with TLS support >**Requirements:** @@ -270,3 +272,4 @@ latest previous version. [reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure [restart]: ../restart_gitlab.md#installations-from-source [gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4 +[video-admin]: https://youtu.be/dD8c7WNcc6s -- cgit v1.2.3 From e27da0ffd7afe535a7b421885560b9f6f2e2cfc6 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Tue, 21 Feb 2017 17:07:08 -0300 Subject: link index-guide to pages user doc --- doc/user/project/pages/index.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md index 4c4f15aad40..ff42e383880 100644 --- a/doc/user/project/pages/index.md +++ b/doc/user/project/pages/index.md @@ -14,6 +14,8 @@ deploy static pages for your individual projects, your user or your group. Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlab-com) for specific information, if you are using GitLab.com to host your website. +Read through [All you Need to Know About GitLab Pages][pages-index-guide] for a list of all learning materials we have prepared for GitLab Pages (webpages, articles, guides, blog posts, video tutorials). + ## Getting started with GitLab Pages > **Note:** @@ -435,3 +437,4 @@ For a list of known issues, visit GitLab's [public issue tracker]. [public issue tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Pages [ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605 [quick start guide]: ../../../ci/quick_start/README.md +[pages-index-guide]: ../../../pages/ -- cgit v1.2.3 From 4d6d5ce64fd5bce8fa3db6dcb14deb930589a008 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Tue, 21 Feb 2017 17:14:18 -0300 Subject: add quick start section to user docs --- doc/user/project/pages/index.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md index ff42e383880..e07b7e83f81 100644 --- a/doc/user/project/pages/index.md +++ b/doc/user/project/pages/index.md @@ -98,6 +98,13 @@ The steps to create a project page for a user or a group are identical: A user's project will be served under `http(s)://username.example.io/projectname` whereas a group's project under `http(s)://groupname.example.io/projectname`. +## Quick Start + +Read through [GitLab Pages Quick Start Guide][pages-quick] or watch the video tutorial on +[how to publish a website with GitLab Pages on GitLab.com from a forked project][video-pages-fork]. + +See also [All you Need to Know About GitLab Pages][pages-index-guide] for a list with all the resources we have for GitLab Pages. + ### Explore the contents of `.gitlab-ci.yml` The key thing about GitLab Pages is the `.gitlab-ci.yml` file, something that @@ -438,3 +445,5 @@ For a list of known issues, visit GitLab's [public issue tracker]. [ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605 [quick start guide]: ../../../ci/quick_start/README.md [pages-index-guide]: ../../../pages/ +[pages-quick]: ../../../pages/pages_quick_start_guide.html +[video-pages-fork]: https://youtu.be/TWqh9MtT4Bg -- cgit v1.2.3 From dde136ad8ec02e7a61d1197cbb209117b252dd8f Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Tue, 21 Feb 2017 18:24:08 -0300 Subject: add images --- doc/pages/images/add_certificate_to_pages.png | Bin 0 -> 58581 bytes doc/pages/images/choose_ci_template.png | Bin 0 -> 75580 bytes doc/pages/images/dns_a_record_example.png | Bin 0 -> 10681 bytes doc/pages/images/dns_cname_record_example.png | Bin 0 -> 11972 bytes doc/pages/images/remove_fork_relashionship.png | Bin 0 -> 39941 bytes doc/pages/images/setup_ci.png | Bin 0 -> 27380 bytes doc/pages/pages_quick_start_guide.md | 6 +++--- ...ic_sites_domains_dns_records_ssl_tls_certificates.md | 6 +++--- 8 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 doc/pages/images/add_certificate_to_pages.png create mode 100644 doc/pages/images/choose_ci_template.png create mode 100644 doc/pages/images/dns_a_record_example.png create mode 100644 doc/pages/images/dns_cname_record_example.png create mode 100644 doc/pages/images/remove_fork_relashionship.png create mode 100644 doc/pages/images/setup_ci.png diff --git a/doc/pages/images/add_certificate_to_pages.png b/doc/pages/images/add_certificate_to_pages.png new file mode 100644 index 00000000000..94d48fafa6e Binary files /dev/null and b/doc/pages/images/add_certificate_to_pages.png differ diff --git a/doc/pages/images/choose_ci_template.png b/doc/pages/images/choose_ci_template.png new file mode 100644 index 00000000000..dea4ad2ed28 Binary files /dev/null and b/doc/pages/images/choose_ci_template.png differ diff --git a/doc/pages/images/dns_a_record_example.png b/doc/pages/images/dns_a_record_example.png new file mode 100644 index 00000000000..f035d30984c Binary files /dev/null and b/doc/pages/images/dns_a_record_example.png differ diff --git a/doc/pages/images/dns_cname_record_example.png b/doc/pages/images/dns_cname_record_example.png new file mode 100644 index 00000000000..93e011d743d Binary files /dev/null and b/doc/pages/images/dns_cname_record_example.png differ diff --git a/doc/pages/images/remove_fork_relashionship.png b/doc/pages/images/remove_fork_relashionship.png new file mode 100644 index 00000000000..8c4fab92990 Binary files /dev/null and b/doc/pages/images/remove_fork_relashionship.png differ diff --git a/doc/pages/images/setup_ci.png b/doc/pages/images/setup_ci.png new file mode 100644 index 00000000000..5569f6aacf5 Binary files /dev/null and b/doc/pages/images/setup_ci.png differ diff --git a/doc/pages/pages_quick_start_guide.md b/doc/pages/pages_quick_start_guide.md index 945e350e235..b86b451ba5e 100644 --- a/doc/pages/pages_quick_start_guide.md +++ b/doc/pages/pages_quick_start_guide.md @@ -47,7 +47,7 @@ Watch the [video tutorial](https://youtu.be/TWqh9MtT4Bg) we've created for the s 1. Fork a project from the [Pages group](https://gitlab.com/pages) 1. Remove the fork relationship by navigating to your **Project**'s **Settings** > **Edit Project** - ![remove fork relashionship]() + ![remove fork relashionship](images/remove_fork_relashionship.png) 1. Enable Shared Runners for your fork: navigate to your **Project**'s **Settings** > **CI/CD Pipelines** 1. Trigger a build (push a change to any file) @@ -74,11 +74,11 @@ To turn a **project website** forked from the Pages group into a **user/group** 1. Clone it to your local computer, add your website files to your project, add, commit and push to GitLab. 1. From the your **Project**'s page, click **Set up CI**: - ![add new file]() + ![setup GitLab CI](images/setup_ci.png) 1. Choose one of the templates from the dropbox menu. Pick up the template corresponding to the SSG you're using (or plain HTML). - ![gitlab-ci templates]() + ![gitlab-ci templates](images/choose_ci_template.png) Once you have both site files and `.gitlab-ci.yml` in your project's root, GitLab CI will build your site and deploy it with Pages. Once the first build passes, you see your site is live by navigating to your **Project**'s **Settings** > **Pages**, where you'll find its default URL. diff --git a/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md b/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md index d32c8b76558..c6e2cab0bc3 100644 --- a/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md +++ b/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md @@ -86,7 +86,7 @@ In case you want to point a root domain (`example.com`) to your GitLab Pages sit **Practical Example:** -![DNS A record pointing to GitLab.com Pages server]() +![DNS A record pointing to GitLab.com Pages server](images/dns_a_record_example.png) #### DNS CNAME record @@ -96,7 +96,7 @@ Notice that, despite it's a user or project website, the `CNAME` should point to **Practical Example:** -![DNS CNAME record pointing to GitLab.com project]() +![DNS CNAME record pointing to GitLab.com project](images/dns_cname_record_example.png) #### TL;DR @@ -138,7 +138,7 @@ Regardless the CA you choose, the steps to add your certificate to your Pages pr 1. An intermediary certificate 1. A public key -![Pages project - adding certificates]() +![Pages project - adding certificates](images/add_certificate_to_pages.png) These fields are found under your **Project**'s **Settings** > **Pages** > **New Domain**. -- cgit v1.2.3 From 93e3cfec3abc7c0eed741ba94779bf014cd9bcb7 Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Wed, 22 Feb 2017 08:52:49 +1100 Subject: Added space indentation in models/merge_requests_closing_issues.rb --- app/models/merge_requests_closing_issues.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/merge_requests_closing_issues.rb b/app/models/merge_requests_closing_issues.rb index 97210900bd5..daafb137be4 100644 --- a/app/models/merge_requests_closing_issues.rb +++ b/app/models/merge_requests_closing_issues.rb @@ -8,8 +8,8 @@ class MergeRequestsClosingIssues < ActiveRecord::Base class << self def count_for_collection(ids) group(:issue_id). - where(issue_id: ids). - pluck('issue_id', 'COUNT(*) as count') + where(issue_id: ids). + pluck('issue_id', 'COUNT(*) as count') end end end -- cgit v1.2.3 From 5cc838d325fb3fcdd7a86a43c67a442ce5feb3d9 Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Wed, 22 Feb 2017 09:00:03 +1100 Subject: Refactored specs --- spec/features/issuables/issuable_list_spec.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb index 4ea801cd1ac..1fc1c20b8a3 100644 --- a/spec/features/issuables/issuable_list_spec.rb +++ b/spec/features/issuables/issuable_list_spec.rb @@ -60,15 +60,15 @@ describe 'issuable list', feature: true do create(:award_emoji, :downvote, awardable: issuable) create(:award_emoji, :upvote, awardable: issuable) - if issuable_type == :issue - issue = Issue.reorder(:iid).first - merge_request = create(:merge_request, - title: FFaker::Lorem.sentence, - source_project: project, - source_branch: FFaker::Name.name) + if issuable_type == :issue + issue = Issue.reorder(:iid).first + merge_request = create(:merge_request, + title: FFaker::Lorem.sentence, + source_project: project, + source_branch: FFaker::Name.name) - MergeRequestsClosingIssues.create!(issue: issue, merge_request: merge_request) if MergeRequestsClosingIssues.count.zero? - end + MergeRequestsClosingIssues.create!(issue: issue, merge_request: merge_request) if MergeRequestsClosingIssues.count.zero? + end end end end -- cgit v1.2.3 From 0494930d7a939297a071bb817ad0227da17dda1b Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Wed, 22 Feb 2017 09:08:20 +1100 Subject: Refactored specs --- spec/features/issuables/issuable_list_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb index 1fc1c20b8a3..0bf7977fb02 100644 --- a/spec/features/issuables/issuable_list_spec.rb +++ b/spec/features/issuables/issuable_list_spec.rb @@ -59,6 +59,7 @@ describe 'issuable list', feature: true do create(:award_emoji, :downvote, awardable: issuable) create(:award_emoji, :upvote, awardable: issuable) + end if issuable_type == :issue issue = Issue.reorder(:iid).first @@ -67,8 +68,7 @@ describe 'issuable list', feature: true do source_project: project, source_branch: FFaker::Name.name) - MergeRequestsClosingIssues.create!(issue: issue, merge_request: merge_request) if MergeRequestsClosingIssues.count.zero? - end + MergeRequestsClosingIssues.create!(issue: issue, merge_request: merge_request) end end end -- cgit v1.2.3 From 9a11acabb90ca0bd0826c5c21b09f96015337537 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 20 Feb 2017 15:42:51 -0600 Subject: add webpack bundle analyzer to production output --- .gitignore | 1 + .gitlab-ci.yml | 11 ++++++++++- config/webpack.config.js | 14 +++++++++++++ package.json | 3 ++- yarn.lock | 51 +++++++++++++++++++++++++++++++++++++++++++----- 5 files changed, 73 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 0b602d613c7..680651986e8 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,4 @@ eslint-report.html /builds/* /shared/* /.gitlab_workhorse_secret +/webpack-report/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index becf2db85fb..e9a1cbc29f4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -250,7 +250,14 @@ rake gitlab:assets:compile: SETUP_DB: "false" USE_DB: "false" SKIP_STORAGE_VALIDATION: "true" - script: bundle exec rake yarn:install gitlab:assets:compile + WEBPACK_REPORT: "true" + script: + - bundle exec rake yarn:install gitlab:assets:compile + artifacts: + name: webpack-report + expire_in: 31d + paths: + - webpack-report/ rake karma: cache: @@ -400,6 +407,7 @@ pages: dependencies: - coverage - rake karma + - rake gitlab:assets:compile - lint:javascript:report script: - mv public/ .public/ @@ -407,6 +415,7 @@ pages: - mv coverage/ public/coverage-ruby/ || true - mv coverage-javascript/ public/coverage-javascript/ || true - mv eslint-report.html public/ || true + - mv webpack-report/ public/webpack-report/ || true artifacts: paths: - public diff --git a/config/webpack.config.js b/config/webpack.config.js index 15899993874..e754f68553a 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -5,12 +5,14 @@ var path = require('path'); var webpack = require('webpack'); var StatsPlugin = require('stats-webpack-plugin'); var CompressionPlugin = require('compression-webpack-plugin'); +var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; var ROOT_PATH = path.resolve(__dirname, '..'); var IS_PRODUCTION = process.env.NODE_ENV === 'production'; var IS_DEV_SERVER = process.argv[1].indexOf('webpack-dev-server') !== -1; var DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808; var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false'; +var WEBPACK_REPORT = process.env.WEBPACK_REPORT; var config = { context: path.join(ROOT_PATH, 'app/assets/javascripts'), @@ -120,4 +122,16 @@ if (IS_DEV_SERVER) { config.output.publicPath = '//localhost:' + DEV_SERVER_PORT + config.output.publicPath; } +if (WEBPACK_REPORT) { + config.plugins.push( + new BundleAnalyzerPlugin({ + analyzerMode: 'static', + generateStatsFile: true, + openAnalyzer: false, + reportFilename: path.join(ROOT_PATH, 'webpack-report/index.html'), + statsFilename: path.join(ROOT_PATH, 'webpack-report/stats.json'), + }) + ); +} + module.exports = config; diff --git a/package.json b/package.json index ad0aaef1897..66aa7e9fe5d 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "underscore": "^1.8.3", "vue": "^2.0.3", "vue-resource": "^0.9.3", - "webpack": "^2.2.1" + "webpack": "^2.2.1", + "webpack-bundle-analyzer": "^2.3.0" }, "devDependencies": { "babel-plugin-istanbul": "^4.0.0", diff --git a/yarn.lock b/yarn.lock index ad4b5223d60..1eaa04e21c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -33,6 +33,10 @@ acorn@^3.0.4: version "3.3.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" +acorn@^4.0.11: + version "4.0.11" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.11.tgz#edcda3bd937e7556410d42ed5860f67399c794c0" + after@0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" @@ -1408,6 +1412,10 @@ dropzone@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/dropzone/-/dropzone-4.2.0.tgz#fbe7acbb9918e0706489072ef663effeef8a79f3" +duplexer@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" + ecc-jsbn@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" @@ -1418,6 +1426,10 @@ ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" +ejs@^2.5.5: + version "2.5.6" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.6.tgz#479636bfa3fe3b1debd52087f0acb204b4f19c88" + elliptic@^6.0.0: version "6.3.3" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.3.3.tgz#5482d9646d54bcb89fd7d994fc9e2e9568876e3f" @@ -1792,7 +1804,7 @@ expand-range@^1.8.1: dependencies: fill-range "^2.1.0" -express@^4.13.3: +express@^4.13.3, express@^4.14.1: version "4.14.1" resolved "https://registry.yarnpkg.com/express/-/express-4.14.1.tgz#646c237f766f148c2120aff073817b9e4d7e0d33" dependencies: @@ -1893,6 +1905,10 @@ fileset@^2.0.2: glob "^7.0.3" minimatch "^3.0.3" +filesize@^3.5.4: + version "3.5.4" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.5.4.tgz#742fc7fb6aef4ee3878682600c22f840731e1fda" + fill-range@^2.1.0: version "2.2.3" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" @@ -2118,6 +2134,12 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: version "1.0.1" resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" +gzip-size@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-3.0.0.tgz#546188e9bdc337f673772f81660464b389dce520" + dependencies: + duplexer "^0.1.1" + handle-thing@^1.2.4: version "1.2.5" resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4" @@ -2627,7 +2649,7 @@ jquery-ujs@^1.2.1: dependencies: jquery ">=1.8.0" -jquery@^2.2.1, jquery@>=1.8.0: +jquery@>=1.8.0, jquery@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.2.1.tgz#3c3e16854ad3d2ac44ac65021b17426d22ad803f" @@ -2824,7 +2846,7 @@ loader-runner@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" -loader-utils@0.2.x, loader-utils@^0.2.11, loader-utils@^0.2.16, loader-utils@^0.2.5: +loader-utils@^0.2.11, loader-utils@^0.2.16, loader-utils@^0.2.5: version "0.2.16" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.16.tgz#f08632066ed8282835dff88dfb52704765adee6d" dependencies: @@ -2903,7 +2925,7 @@ lodash@^3.8.0: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" -lodash@^4.0.0, lodash@^4.0.1, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0: +lodash@^4.0.0, lodash@^4.0.1, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -3216,6 +3238,10 @@ onetime@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" +opener@^1.4.2: + version "1.4.3" + resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8" + opn@4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/opn/-/opn-4.0.2.tgz#7abc22e644dff63b0a96d5ab7f2790c0f01abc95" @@ -3964,7 +3990,7 @@ source-map-support@^0.4.2: dependencies: source-map "^0.5.3" -source-map@0.1.x, source-map@^0.1.41: +source-map@^0.1.41: version "0.1.43" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" dependencies: @@ -4402,6 +4428,21 @@ wbuf@^1.1.0, wbuf@^1.4.0: dependencies: minimalistic-assert "^1.0.0" +webpack-bundle-analyzer@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.3.0.tgz#0d05e96a43033f7cc57f6855b725782ba61e93a4" + dependencies: + acorn "^4.0.11" + chalk "^1.1.3" + commander "^2.9.0" + ejs "^2.5.5" + express "^4.14.1" + filesize "^3.5.4" + gzip-size "^3.0.0" + lodash "^4.17.4" + mkdirp "^0.5.1" + opener "^1.4.2" + webpack-dev-middleware@^1.0.11, webpack-dev-middleware@^1.9.0: version "1.10.0" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.10.0.tgz#7d5be2651e692fddfafd8aaed177c16ff51f0eb8" -- cgit v1.2.3 From 660e1eb5246f70e476bcb437d05ca4d7a3c51740 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Tue, 21 Feb 2017 16:33:58 -0600 Subject: approve opener license (MIT) --- config/dependency_decisions.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml index 7336d7c842a..072ed8a3864 100644 --- a/config/dependency_decisions.yml +++ b/config/dependency_decisions.yml @@ -320,3 +320,9 @@ :why: https://github.com/shinnn/spdx-license-ids/blob/v1.2.2/LICENSE :versions: [] :when: 2017-02-08 22:35:00.225232000 Z +- - :approve + - opener + - :who: Mike Greiling + :why: https://github.com/domenic/opener/blob/1.4.3/LICENSE.txt + :versions: [] + :when: 2017-02-21 22:33:41.729629000 Z -- cgit v1.2.3 From 1c4b8c1a50f6df18811c3af1820a76565da25619 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Tue, 21 Feb 2017 16:38:22 -0600 Subject: add CHANGELOG.md entry for !9396 --- .../28450-test-compiling-frontend-assets-for-production-in-ci.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/28450-test-compiling-frontend-assets-for-production-in-ci.yml diff --git a/changelogs/unreleased/28450-test-compiling-frontend-assets-for-production-in-ci.yml b/changelogs/unreleased/28450-test-compiling-frontend-assets-for-production-in-ci.yml new file mode 100644 index 00000000000..196a9b788ea --- /dev/null +++ b/changelogs/unreleased/28450-test-compiling-frontend-assets-for-production-in-ci.yml @@ -0,0 +1,4 @@ +--- +title: test compiling production assets and generate webpack bundle report in CI +merge_request: 9396 +author: -- cgit v1.2.3 From d16eed3b096bb01efda90dfea47ac549a9d2ffbc Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 22 Feb 2017 08:39:50 +0100 Subject: Fix reprocessing skipped jobs when retrying pipeline --- app/services/ci/retry_pipeline_service.rb | 2 ++ spec/services/ci/retry_pipeline_service_spec.rb | 25 +++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb index 2c5e130e5aa..7fcbac6399a 100644 --- a/app/services/ci/retry_pipeline_service.rb +++ b/app/services/ci/retry_pipeline_service.rb @@ -8,6 +8,8 @@ module Ci pipeline.builds.failed_or_canceled.find_each do |build| next unless build.retryable? + pipeline.mark_as_processable_after_stage(build.stage_idx) + Ci::RetryBuildService.new(project, current_user) .reprocess(build) end diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb index c0af8b8450a..8b1ed6470e4 100644 --- a/spec/services/ci/retry_pipeline_service_spec.rb +++ b/spec/services/ci/retry_pipeline_service_spec.rb @@ -69,6 +69,25 @@ describe Ci::RetryPipelineService, '#execute', :services do end end + context 'when the last stage was skipepd' do + before do + create_build('build 1', :success, 0) + create_build('test 2', :failed, 1) + create_build('report 3', :skipped, 2) + create_build('report 4', :skipped, 2) + end + + it 'retries builds only in the first stage' do + service.execute(pipeline) + + expect(build('build 1')).to be_success + expect(build('test 2')).to be_pending + expect(build('report 3')).to be_created + expect(build('report 4')).to be_created + expect(pipeline.reload).to be_running + end + end + context 'when pipeline contains manual actions' do context 'when there is a canceled manual action in first stage' do before do @@ -90,14 +109,16 @@ describe Ci::RetryPipelineService, '#execute', :services do context 'when there is a skipped manual action in last stage' do before do create_build('rspec 1', :canceled, 0) + create_build('rspec 2', :skipped, 0, :manual) create_build('staging', :skipped, 1, :manual) end - it 'retries canceled job and skips manual action' do + it 'retries canceled job and reprocesses manual actions' do service.execute(pipeline) expect(build('rspec 1')).to be_pending - expect(build('staging')).to be_skipped + expect(build('rspec 2')).to be_skipped + expect(build('staging')).to be_created expect(pipeline.reload).to be_running end end -- cgit v1.2.3 From 1965482f12688f4f26342b91eca888567d0dac04 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 22 Feb 2017 09:06:58 +0100 Subject: Reprocess jobs in stages once when retrying pipeline --- app/services/ci/retry_pipeline_service.rb | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb index 7fcbac6399a..ca19b828e1c 100644 --- a/app/services/ci/retry_pipeline_service.rb +++ b/app/services/ci/retry_pipeline_service.rb @@ -5,13 +5,18 @@ module Ci raise Gitlab::Access::AccessDeniedError end - pipeline.builds.failed_or_canceled.find_each do |build| - next unless build.retryable? + pipeline.builds.failed_or_canceled.tap do |builds| + stage_idx = builds.order('stage_idx ASC') + .pluck('DISTINCT stage_idx').first - pipeline.mark_as_processable_after_stage(build.stage_idx) + pipeline.mark_as_processable_after_stage(stage_idx) - Ci::RetryBuildService.new(project, current_user) - .reprocess(build) + builds.find_each do |build| + next unless build.retryable? + + Ci::RetryBuildService.new(project, current_user) + .reprocess(build) + end end MergeRequests::AddTodoWhenBuildFailsService -- cgit v1.2.3 From 173e27d62a2fa880fa438ef6f07847bbd3e06fea Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Mon, 20 Feb 2017 13:51:47 +0000 Subject: Finished up mattermost team creation --- .../javascripts/behaviors/bind_in_out.js.es6 | 48 ++++++ .../javascripts/behaviors/toggler_behavior.js | 1 - app/assets/javascripts/dispatcher.js.es6 | 1 + app/controllers/groups_controller.rb | 3 +- app/views/groups/_create_chat_team.html.haml | 20 +-- app/views/shared/_group_form.html.haml | 3 +- spec/features/groups_spec.rb | 37 ++++ spec/javascripts/behaviors/bind_in_out_spec.js.es6 | 189 +++++++++++++++++++++ spec/javascripts/helpers/class_spec_helper.js.es6 | 2 + 9 files changed, 289 insertions(+), 15 deletions(-) create mode 100644 app/assets/javascripts/behaviors/bind_in_out.js.es6 create mode 100644 spec/javascripts/behaviors/bind_in_out_spec.js.es6 diff --git a/app/assets/javascripts/behaviors/bind_in_out.js.es6 b/app/assets/javascripts/behaviors/bind_in_out.js.es6 new file mode 100644 index 00000000000..3cb892451e3 --- /dev/null +++ b/app/assets/javascripts/behaviors/bind_in_out.js.es6 @@ -0,0 +1,48 @@ +class BindInOut { + constructor(bindIn, bindOut) { + this.in = bindIn; + this.out = bindOut; + + this.eventWrapper = {}; + this.eventType = /(INPUT|TEXTAREA)/.test(bindIn.tagName) ? 'keyup' : 'change'; + } + + addEvents() { + this.eventWrapper.updateOut = this.updateOut.bind(this); + + this.in.addEventListener(this.eventType, this.eventWrapper.updateOut); + + return this; + } + + updateOut() { + this.out.textContent = this.in.value; + + return this; + } + + removeEvents() { + this.in.removeEventListener(this.eventType, this.eventWrapper.updateOut); + + return this; + } + + static initAll() { + const ins = document.querySelectorAll('*[data-bind-in]'); + + return [].map.call(ins, anIn => BindInOut.init(anIn)); + } + + static init(anIn, anOut) { + const out = anOut || document.querySelector(`*[data-bind-out="${anIn.dataset.bindIn}"]`); + const bindInOut = new BindInOut(anIn, out); + + return bindInOut.addEvents().updateOut(); + } +} + +const global = window.gl || (window.gl = {}); + +global.BindInOut = BindInOut; + +module.exports = BindInOut; diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index a7181904ac9..2d98fc5a545 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -22,7 +22,6 @@ // %div.js-toggle-content // $('body').on('click', '.js-toggle-button', function(e) { - e.preventDefault(); toggleContainer($(this).closest('.js-toggle-container')); }); diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 45aa6050aed..f7ab9eefab1 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -216,6 +216,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); case 'admin:groups:edit': case 'admin:groups:new': new GroupAvatar(); + gl.BindInOut.initAll(); break; case 'projects:tree:show': shortcut_handler = new ShortcutsNavigation(); diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 0e421b915cf..5b1898b0ee1 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -81,7 +81,6 @@ class GroupsController < Groups::ApplicationController end def update - byebug if Groups::UpdateService.new(@group, current_user, group_params).execute redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated." else @@ -143,7 +142,7 @@ class GroupsController < Groups::ApplicationController :request_access_enabled, :share_with_group_lock, :visibility_level, - :parent_id + :parent_id, :create_chat_team, :chat_team_name ] diff --git a/app/views/groups/_create_chat_team.html.haml b/app/views/groups/_create_chat_team.html.haml index 1e702c4f20f..8b908e233dc 100644 --- a/app/views/groups/_create_chat_team.html.haml +++ b/app/views/groups/_create_chat_team.html.haml @@ -4,15 +4,13 @@ = custom_icon('icon_mattermost') Mattermost .col-sm-10 - .checkbox + .checkbox.js-toggle-container = f.label :create_chat_team do - = f.check_box(:create_chat_team, { checked: @group.chat_team }, 'true', 'false') - Link the group to a new Mattermost team - -.form-group - = f.label :chat_team, class: 'control-label' do - Chat Team name - .col-sm-10 - = f.text_field :chat_team, placeholder: @group.chat_team, class: 'form-control mattermost-team-name' - %small - Leave blank to match your group name + .js-toggle-button= f.check_box(:create_chat_team, { checked: true }, 'true', 'false') + Create a Mattermost team for this group + %br + %small.light.js-toggle-content + Team URL: + = Settings.mattermost.host + %span> / + %span{ "data-bind-out" => "create_chat_team"} diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index efb207b9916..5b8c9a66205 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -18,7 +18,8 @@ = f.text_field :path, placeholder: 'open-source', class: 'form-control', autofocus: local_assigns[:autofocus] || false, required: true, pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE, - title: 'Please choose a group name with no special characters.' + title: 'Please choose a group name with no special characters.', + "data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}" - if parent = f.hidden_field :parent_id, value: parent.id diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index 37b7c20239f..b4bd1925822 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -43,6 +43,43 @@ feature 'Group', feature: true do expect(page).to have_namespace_error_message end end + + describe 'Mattermost team creation' do + before do + Settings.mattermost['enabled'] = mattermost_enabled + visit new_group_path + end + + context 'Mattermost enabled' do + let(:mattermost_enabled) { true } + + it 'displays a team creation checkbox' do + expect(page).to have_selector('#group_create_chat_team') + end + + it 'checks the checkbox by default' do + expect(find('#group_create_chat_team')['checked']).to eq(true) + end + + it 'updates the team URL on graph path update', :js do + out_span = find('span[data-bind-out="create_chat_team"]') + + expect(out_span.text).to be_empty + + fill_in('group_path', with: 'test-group') + + expect(out_span.text).to eq('test-group') + end + end + + context 'Mattermost disabled' do + let(:mattermost_enabled) { false } + + it 'doesnt show a team creation checkbox if Mattermost not enabled' do + expect(page).not_to have_selector('#group_create_chat_team') + end + end + end end describe 'create a nested group' do diff --git a/spec/javascripts/behaviors/bind_in_out_spec.js.es6 b/spec/javascripts/behaviors/bind_in_out_spec.js.es6 new file mode 100644 index 00000000000..69d2460ab1c --- /dev/null +++ b/spec/javascripts/behaviors/bind_in_out_spec.js.es6 @@ -0,0 +1,189 @@ +const BindInOut = require('~/behaviors/bind_in_out'); +const ClassSpecHelper = require('../helpers/class_spec_helper'); + +describe('BindInOut', function () { + describe('.constructor', function () { + beforeEach(function () { + this.in = {}; + this.out = {}; + + this.bindInOut = new BindInOut(this.in, this.out); + }); + + it('should set .in', function () { + expect(this.bindInOut.in).toBe(this.in); + }); + + it('should set .out', function () { + expect(this.bindInOut.out).toBe(this.out); + }); + + it('should set .eventWrapper', function () { + expect(this.bindInOut.eventWrapper).toEqual({}); + }); + + describe('if .in is an input', function () { + beforeEach(function () { + this.bindInOut = new BindInOut({ tagName: 'INPUT' }); + }); + + it('should set .eventType to keyup ', function () { + expect(this.bindInOut.eventType).toEqual('keyup'); + }); + }); + + describe('if .in is a textarea', function () { + beforeEach(function () { + this.bindInOut = new BindInOut({ tagName: 'TEXTAREA' }); + }); + + it('should set .eventType to keyup ', function () { + expect(this.bindInOut.eventType).toEqual('keyup'); + }); + }); + + describe('if .in is not an input or textarea', function () { + beforeEach(function () { + this.bindInOut = new BindInOut({ tagName: 'SELECT' }); + }); + + it('should set .eventType to change ', function () { + expect(this.bindInOut.eventType).toEqual('change'); + }); + }); + }); + + describe('.addEvents', function () { + beforeEach(function () { + this.in = jasmine.createSpyObj('in', ['addEventListener']); + + this.bindInOut = new BindInOut(this.in); + + this.addEvents = this.bindInOut.addEvents(); + }); + + it('should set .eventWrapper.updateOut', function () { + expect(this.bindInOut.eventWrapper.updateOut).toEqual(jasmine.any(Function)); + }); + + it('should call .addEventListener', function () { + expect(this.in.addEventListener) + .toHaveBeenCalledWith( + this.bindInOut.eventType, + this.bindInOut.eventWrapper.updateOut, + ); + }); + + it('should return the instance', function () { + expect(this.addEvents).toBe(this.bindInOut); + }); + }); + + describe('.updateOut', function () { + beforeEach(function () { + this.in = { value: 'the-value' }; + this.out = { textContent: 'not-the-value' }; + + this.bindInOut = new BindInOut(this.in, this.out); + + this.updateOut = this.bindInOut.updateOut(); + }); + + it('should set .out.textContent to .in.value', function () { + expect(this.out.textContent).toBe(this.in.value); + }); + + it('should return the instance', function () { + expect(this.updateOut).toBe(this.bindInOut); + }); + }); + + describe('.removeEvents', function () { + beforeEach(function () { + this.in = jasmine.createSpyObj('in', ['removeEventListener']); + this.updateOut = () => {}; + + this.bindInOut = new BindInOut(this.in); + this.bindInOut.eventWrapper.updateOut = this.updateOut; + + this.removeEvents = this.bindInOut.removeEvents(); + }); + + it('should call .removeEventListener', function () { + expect(this.in.removeEventListener) + .toHaveBeenCalledWith( + this.bindInOut.eventType, + this.updateOut, + ); + }); + + it('should return the instance', function () { + expect(this.removeEvents).toBe(this.bindInOut); + }); + }); + + describe('.initAll', function () { + beforeEach(function () { + this.ins = [0, 1, 2]; + this.instances = []; + + spyOn(document, 'querySelectorAll').and.returnValue(this.ins); + spyOn(Array.prototype, 'map').and.callThrough(); + spyOn(BindInOut, 'init'); + + this.initAll = BindInOut.initAll(); + }); + + ClassSpecHelper.itShouldBeAStaticMethod(BindInOut, 'initAll'); + + it('should call .querySelectorAll', function () { + expect(document.querySelectorAll).toHaveBeenCalledWith('*[data-bind-in]'); + }); + + it('should call .map', function () { + expect(Array.prototype.map).toHaveBeenCalledWith(jasmine.any(Function)); + }); + + it('should call .init for each element', function () { + expect(BindInOut.init.calls.count()).toEqual(3); + }); + + it('should return an array of instances', function () { + expect(this.initAll).toEqual(jasmine.any(Array)); + }); + }); + + describe('.init', function () { + beforeEach(function () { + spyOn(BindInOut.prototype, 'addEvents').and.callFake(function () { return this; }); + spyOn(BindInOut.prototype, 'updateOut').and.callFake(function () { return this; }); + + this.init = BindInOut.init({}, {}); + }); + + ClassSpecHelper.itShouldBeAStaticMethod(BindInOut, 'init'); + + it('should call .addEvents', function () { + expect(BindInOut.prototype.addEvents).toHaveBeenCalled(); + }); + + it('should call .updateOut', function () { + expect(BindInOut.prototype.updateOut).toHaveBeenCalled(); + }); + + describe('if no anOut is provided', function () { + beforeEach(function () { + this.anIn = { dataset: { bindIn: 'the-data-bind-in' } }; + + spyOn(document, 'querySelector'); + + BindInOut.init(this.anIn); + }); + + it('should call .querySelector', function () { + expect(document.querySelector) + .toHaveBeenCalledWith(`*[data-bind-out="${this.anIn.dataset.bindIn}"]`); + }); + }); + }); +}); diff --git a/spec/javascripts/helpers/class_spec_helper.js.es6 b/spec/javascripts/helpers/class_spec_helper.js.es6 index d3c37d39431..61db27a8fcc 100644 --- a/spec/javascripts/helpers/class_spec_helper.js.es6 +++ b/spec/javascripts/helpers/class_spec_helper.js.es6 @@ -7,3 +7,5 @@ class ClassSpecHelper { } window.ClassSpecHelper = ClassSpecHelper; + +module.exports = ClassSpecHelper; -- cgit v1.2.3 From f0766fdefaeadcc526d6a87e29b65f5bf7b26318 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Mon, 6 Feb 2017 20:15:25 +0100 Subject: extract pipeline mails layout --- app/assets/images/mailers/gitlab_footer_logo.gif | Bin 0 -> 3654 bytes app/assets/images/mailers/gitlab_header_logo.gif | Bin 0 -> 3040 bytes app/mailers/emails/pipelines.rb | 4 +- app/views/layouts/mailer.html.haml | 72 ++++++ app/views/layouts/mailer.text.haml | 5 + app/views/notify/pipeline_failed_email.html.haml | 280 ++++++++-------------- app/views/notify/pipeline_failed_email.text.erb | 4 - app/views/notify/pipeline_success_email.html.haml | 230 +++++++----------- app/views/notify/pipeline_success_email.text.erb | 4 - 9 files changed, 264 insertions(+), 335 deletions(-) create mode 100644 app/assets/images/mailers/gitlab_footer_logo.gif create mode 100644 app/assets/images/mailers/gitlab_header_logo.gif create mode 100644 app/views/layouts/mailer.html.haml create mode 100644 app/views/layouts/mailer.text.haml diff --git a/app/assets/images/mailers/gitlab_footer_logo.gif b/app/assets/images/mailers/gitlab_footer_logo.gif new file mode 100644 index 00000000000..3f4ef31947b Binary files /dev/null and b/app/assets/images/mailers/gitlab_footer_logo.gif differ diff --git a/app/assets/images/mailers/gitlab_header_logo.gif b/app/assets/images/mailers/gitlab_header_logo.gif new file mode 100644 index 00000000000..387628f831c Binary files /dev/null and b/app/assets/images/mailers/gitlab_header_logo.gif differ diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb index 9460a6cd2be..f9f45ab987b 100644 --- a/app/mailers/emails/pipelines.rb +++ b/app/mailers/emails/pipelines.rb @@ -22,8 +22,8 @@ module Emails mail(bcc: recipients, subject: pipeline_subject(status), skip_premailer: true) do |format| - format.html { render layout: false } - format.text + format.html { render layout: 'mailer' } + format.text { render layout: 'mailer' } end end diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml new file mode 100644 index 00000000000..39133f8cdb3 --- /dev/null +++ b/app/views/layouts/mailer.html.haml @@ -0,0 +1,72 @@ + +%html{ lang: "en" } + %head + %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/ + %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/ + %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/ + %title= message.subject + :css + /* CLIENT-SPECIFIC STYLES */ + body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } + table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; } + img { -ms-interpolation-mode: bicubic; } + + /* iOS BLUE LINKS */ + a[x-apple-data-detectors] { + color: inherit !important; + text-decoration: none !important; + font-size: inherit !important; + font-family: inherit !important; + font-weight: inherit !important; + line-height: inherit !important; + } + + /* ANDROID MARGIN HACK */ + body { margin:0 !important; } + div[style*="margin: 16px 0"] { margin:0 !important; } + + @media only screen and (max-width: 639px) { + body, #body { + min-width: 320px !important; + } + table.wrapper { + width: 100% !important; + min-width: 320px !important; + } + table.wrapper > tbody > tr > td { + border-left: 0 !important; + border-right: 0 !important; + border-radius: 0 !important; + padding-left: 10px !important; + padding-right: 10px !important; + } + } + %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } + %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" } + %tbody + %tr.line + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }   + %tr.header + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } + %img{ alt: "GitLab", height: "50", src: image_url('mailers/gitlab_header_logo.gif'), width: "55" }/ + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } + %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" } + %tbody + = yield + + %tr.footer + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } + %img{ alt: "GitLab", height: "33", src: image_url('mailers/gitlab_footer_logo.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/ + %div + %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications + · + %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help + %div + You're receiving this email because of your account on + = succeed "." do + %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host diff --git a/app/views/layouts/mailer.text.haml b/app/views/layouts/mailer.text.haml new file mode 100644 index 00000000000..6a9c6ced9cc --- /dev/null +++ b/app/views/layouts/mailer.text.haml @@ -0,0 +1,5 @@ += yield + +You're receiving this email because of your account on #{Gitlab.config.gitlab.host}. +Manage all notifications: #{profile_notifications_url} +Help: #{help_url} diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml index d9ebbaa2704..85a1aea3a61 100644 --- a/app/views/notify/pipeline_failed_email.html.haml +++ b/app/views/notify/pipeline_failed_email.html.haml @@ -1,179 +1,109 @@ - -%html{ lang: "en" } - %head - %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/ - %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/ - %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/ - %title= message.subject - :css - /* CLIENT-SPECIFIC STYLES */ - body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } - table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; } - img { -ms-interpolation-mode: bicubic; } - - /* iOS BLUE LINKS */ - a[x-apple-data-detectors] { - color: inherit !important; - text-decoration: none !important; - font-size: inherit !important; - font-family: inherit !important; - font-weight: inherit !important; - line-height: inherit !important; - } - - /* ANDROID MARGIN HACK */ - body { margin:0 !important; } - div[style*="margin: 16px 0"] { margin:0 !important; } - - @media only screen and (max-width: 639px) { - body, #body { - min-width: 320px !important; - } - table.wrapper { - width: 100% !important; - min-width: 320px !important; - } - table.wrapper > tbody > tr > td { - border-left: 0 !important; - border-right: 0 !important; - border-radius: 0 !important; - padding-left: 10px !important; - padding-right: 10px !important; - } - } - %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } - %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" } +%tr.alert + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;background-color:#d22f57;color:#ffffff;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } %tbody - %tr.line - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }   - %tr.header - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } - %img{ alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55" }/ %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } - %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" } + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" } + %img{ alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" } + Your pipeline has failed. +%tr.spacer + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } +   +%tr.section + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" } + - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name + - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner) + %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" } + = namespace_name + \/ + %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" } + = @project.name + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" } + = @pipeline.ref + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } - %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" } - %tbody - %tr.alert - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;background-color:#d22f57;color:#ffffff;" } - %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } - %tbody - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" } - %img{ alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13" }/ - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" } - Your pipeline has failed. - %tr.spacer - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } -   - %tr.section - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } - %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" } - %tbody - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" } - - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name - - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner) - %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" } - = namespace_name - \/ - %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" } - = @project.name - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } - %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } - %tbody - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/ - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" } - = @pipeline.ref - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } - %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } - %tbody - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/ - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } - = @pipeline.short_sha - - if @merge_request - in - %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" } - = @merge_request.to_reference - .commit{ style: "color:#5c5c5c;font-weight:300;" } - = @pipeline.git_commit_message.truncate(50) - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } - %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } - %tbody - %tr - - commit = @pipeline.commit - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - - if commit.author - %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } - = commit.author.name - - else - %span - = commit.author_name - %tr.spacer - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } -   - - failed = @pipeline.statuses.latest.failed - %tr.pre-section - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;" } - Pipeline - %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } - = "\##{@pipeline.id}" - had - = failed.size - failed - #{'build'.pluralize(failed.size)}. - %tr.warning - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" } - Logs may contain sensitive data. Please consider before forwarding this email. - %tr.section - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;" } - %table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;" } - %tbody - - failed.each do |build| - %tr.build-state - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" } - %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } - %tbody - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;" } - %img{ alt: "x", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10" }/ - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;" } - = build.stage - %td{ align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" } - = render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build - %tr.build-log - - if build.has_trace? - %td{ colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;" } - %pre{ style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;" } - = build.trace_html(last_lines: 10).html_safe - - else - %td{ colspan: "2" } - %tr.footer - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } - %img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/ - %div - %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications - · - %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help - %div - You're receiving this email because of your account on - = succeed "." do - %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } + = @pipeline.short_sha + - if @merge_request + in + %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" } + = @merge_request.to_reference + .commit{ style: "color:#5c5c5c;font-weight:300;" } + = @pipeline.git_commit_message.truncate(50) + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } + %tbody + %tr + - commit = @pipeline.commit + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + - if commit.author + %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } + = commit.author.name + - else + %span + = commit.author_name +%tr.spacer + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } +   +- failed = @pipeline.statuses.latest.failed +%tr.pre-section + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;" } + Pipeline + %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } + = "\##{@pipeline.id}" + had + = failed.size + failed + #{'build'.pluralize(failed.size)}. +%tr.warning + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" } + Logs may contain sensitive data. Please consider before forwarding this email. +%tr.section + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;" } + %table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;" } + %tbody + - failed.each do |build| + %tr.build-state + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;" } + %img{ alt: "x", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;" } + = build.stage + %td{ align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" } + = render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build + %tr.build-log + - if build.has_trace? + %td{ colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;" } + %pre{ style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;" } + = build.trace_html(last_lines: 10).html_safe + - else + %td{ colspan: "2" } diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb index ab91c7ef350..520a2fc7d68 100644 --- a/app/views/notify/pipeline_failed_email.text.erb +++ b/app/views/notify/pipeline_failed_email.text.erb @@ -27,7 +27,3 @@ Trace: <%= build.trace_with_state(last_lines: 10)[:text] %> <% end -%> <% end -%> - -You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>. -Manage all notifications: <%= profile_notifications_url %> -Help: <%= help_url %> diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml index 8add2e18206..19d4add06f5 100644 --- a/app/views/notify/pipeline_success_email.html.haml +++ b/app/views/notify/pipeline_success_email.html.haml @@ -1,154 +1,84 @@ - -%html{ lang: "en" } - %head - %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/ - %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/ - %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/ - %title= message.subject - :css - /* CLIENT-SPECIFIC STYLES */ - body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } - table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; } - img { -ms-interpolation-mode: bicubic; } - - /* iOS BLUE LINKS */ - a[x-apple-data-detectors] { - color: inherit !important; - text-decoration: none !important; - font-size: inherit !important; - font-family: inherit !important; - font-weight: inherit !important; - line-height: inherit !important; - } - - /* ANDROID MARGIN HACK */ - body { margin:0 !important; } - div[style*="margin: 16px 0"] { margin:0 !important; } - - @media only screen and (max-width: 639px) { - body, #body { - min-width: 320px !important; - } - table.wrapper { - width: 100% !important; - min-width: 320px !important; - } - table.wrapper > tbody > tr > td { - border-left: 0 !important; - border-right: 0 !important; - border-radius: 0 !important; - padding-left: 10px !important; - padding-right: 10px !important; - } - } - %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } - %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" } +%tr.success + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } %tbody - %tr.line - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }   - %tr.header - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } - %img{ alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55" }/ %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } - %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" } + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" } + %img{ alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" } + Your pipeline has passed. +%tr.spacer + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } +   +%tr.section + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" } + - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name + - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner) + %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" } + = namespace_name + \/ + %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" } + = @project.name + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" } + = @pipeline.ref + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } + = @pipeline.short_sha + - if @merge_request + in + %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" } + = @merge_request.to_reference + .commit{ style: "color:#5c5c5c;font-weight:300;" } + = @pipeline.git_commit_message.truncate(50) + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } - %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" } - %tbody - %tr.success - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" } - %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } - %tbody - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" } - %img{ alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13" }/ - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" } - Your pipeline has passed. - %tr.spacer - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } -   - %tr.section - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } - %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" } - %tbody - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" } - - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name - - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner) - %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" } - = namespace_name - \/ - %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" } - = @project.name - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } - %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } - %tbody - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/ - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" } - = @pipeline.ref - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } - %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } - %tbody - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/ - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } - = @pipeline.short_sha - - if @merge_request - in - %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" } - = @merge_request.to_reference - .commit{ style: "color:#5c5c5c;font-weight:300;" } - = @pipeline.git_commit_message.truncate(50) - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } - %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } - %tbody - %tr - - commit = @pipeline.commit - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - - if commit.author - %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } - = commit.author.name - - else - %span - = commit.author_name - %tr.spacer - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } -   - %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_count - Pipeline - %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } - = "\##{@pipeline.id}" - successfully completed - #{build_count} #{'build'.pluralize(build_count)} - in - #{stage_count} #{'stage'.pluralize(stage_count)}. - %tr.footer - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } - %img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/ - %div - %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications - · - %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help - %div - You're receiving this email because of your account on - = succeed "." do - %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host + - commit = @pipeline.commit + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + - if commit.author + %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } + = commit.author.name + - else + %span + = commit.author_name +%tr.spacer + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } +   +%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_count + Pipeline + %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } + = "\##{@pipeline.id}" + successfully completed + #{build_count} #{'build'.pluralize(build_count)} + in + #{stage_count} #{'stage'.pluralize(stage_count)}. diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb index 40e5e306426..0970a3a4e09 100644 --- a/app/views/notify/pipeline_success_email.text.erb +++ b/app/views/notify/pipeline_success_email.text.erb @@ -18,7 +18,3 @@ Commit Author: <%= commit.author_name %> <% build_count = @pipeline.statuses.latest.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 %>. -Manage all notifications: <%= profile_notifications_url %> -Help: <%= help_url %> -- cgit v1.2.3 From 0df104a389438665df2c7248ae022cdbb767f838 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 7 Feb 2017 16:06:27 +0100 Subject: use custom brand logo in pipeline mails --- app/helpers/emails_helper.rb | 14 ++++++++++++++ app/views/layouts/mailer.html.haml | 2 +- spec/helpers/emails_helper_spec.rb | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index 2843ad96efa..3beddff9206 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -1,4 +1,6 @@ module EmailsHelper + include AppearancesHelper + # Google Actions # https://developers.google.com/gmail/markup/reference/go-to-action def email_action(url) @@ -49,4 +51,16 @@ module EmailsHelper msg = "This link is valid for #{password_reset_token_valid_time}. " msg << "After it expires, you can #{link_tag}." end + + def header_logo + if brand_item && brand_item.header_logo? + brand_header_logo + else + image_tag( + image_url('mailers/gitlab_header_logo.gif'), + size: "55x50", + alt: "GitLab" + ) + end + end end diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml index 39133f8cdb3..53268cc22f8 100644 --- a/app/views/layouts/mailer.html.haml +++ b/app/views/layouts/mailer.html.haml @@ -48,7 +48,7 @@ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }   %tr.header %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } - %img{ alt: "GitLab", height: "50", src: image_url('mailers/gitlab_header_logo.gif'), width: "55" }/ + = header_logo %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" } diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb index 3223556e1d3..b9519e387eb 100644 --- a/spec/helpers/emails_helper_spec.rb +++ b/spec/helpers/emails_helper_spec.rb @@ -43,4 +43,36 @@ describe EmailsHelper do end end end + + describe '#header_logo' do + context 'there is a brand item with a logo' do + it 'returns the brand header logo' do + appearance = create :appearance, header_logo: fixture_file_upload( + Rails.root.join('spec/fixtures/dk.png') + ) + + expect(header_logo).to eq( + %{Dk} + ) + end + end + + context 'there is a brand item without a logo' do + it 'returns the default header logo' do + create :appearance, header_logo: nil + + expect(header_logo).to eq( + %{GitLab} + ) + end + end + + context 'there is no brand item' do + it 'returns the default header logo' do + expect(header_logo).to eq( + %{GitLab} + ) + end + end + end end -- cgit v1.2.3 From 84420c89d5fbf98a19fb831a4489e9d5441e18ad Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 7 Feb 2017 18:23:58 +0100 Subject: add documentation for custom brand logo in email --- doc/README.md | 1 + doc/customization/branded_page_and_email_header.md | 15 +++++++++++++++ .../branded_page_and_email_header/appearance.png | Bin 0 -> 10253 bytes .../custom_brand_header.png | Bin 0 -> 10014 bytes .../custom_email_header.png | Bin 0 -> 37472 bytes 5 files changed, 16 insertions(+) create mode 100644 doc/customization/branded_page_and_email_header.md create mode 100644 doc/customization/branded_page_and_email_header/appearance.png create mode 100644 doc/customization/branded_page_and_email_header/custom_brand_header.png create mode 100644 doc/customization/branded_page_and_email_header/custom_email_header.png diff --git a/doc/README.md b/doc/README.md index 1943d656aa7..c082b91a6b1 100644 --- a/doc/README.md +++ b/doc/README.md @@ -50,6 +50,7 @@ - [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed. - [Update](update/README.md) Update guides to upgrade your installation. - [Welcome message](customization/welcome_message.md) Add a custom welcome message to the sign-in page. +- [Header logo](customization/branded_page_and_email_header.md) Change the logo on the overall page and email header. - [Reply by email](administration/reply_by_email.md) Allow users to comment on issues and merge requests by replying to notification emails. - [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE. - [Git LFS configuration](workflow/lfs/lfs_administration.md) diff --git a/doc/customization/branded_page_and_email_header.md b/doc/customization/branded_page_and_email_header.md new file mode 100644 index 00000000000..9a0f0b382fa --- /dev/null +++ b/doc/customization/branded_page_and_email_header.md @@ -0,0 +1,15 @@ +# Changing the logo on the overall page and email header + +Navigate to the **Admin** area and go to the **Appearance** page. + +Upload the custom logo (**Header logo**) in the section **Navigation bar**. + +![appearance](branded_page_and_email_header/appearance.png) + +After saving the page, your GitLab navigation bar will contain the custom logo: + +![custom_brand_header](branded_page_and_email_header/custom_brand_header.png) + +The GitLab pipeline emails will also have the custom logo: + +![custom_email_header](branded_page_and_email_header/custom_email_header.png) diff --git a/doc/customization/branded_page_and_email_header/appearance.png b/doc/customization/branded_page_and_email_header/appearance.png new file mode 100644 index 00000000000..abbba6f9ac9 Binary files /dev/null and b/doc/customization/branded_page_and_email_header/appearance.png differ diff --git a/doc/customization/branded_page_and_email_header/custom_brand_header.png b/doc/customization/branded_page_and_email_header/custom_brand_header.png new file mode 100644 index 00000000000..7390f8a5e4e Binary files /dev/null and b/doc/customization/branded_page_and_email_header/custom_brand_header.png differ diff --git a/doc/customization/branded_page_and_email_header/custom_email_header.png b/doc/customization/branded_page_and_email_header/custom_email_header.png new file mode 100644 index 00000000000..705698ef4a8 Binary files /dev/null and b/doc/customization/branded_page_and_email_header/custom_email_header.png differ -- cgit v1.2.3 From 26da2c45ac4071f7a33a34c89162709d5a1bc470 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 7 Feb 2017 22:17:59 +0100 Subject: add changelog entry --- changelogs/unreleased/feature-brand-logo-in-emails.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/feature-brand-logo-in-emails.yml diff --git a/changelogs/unreleased/feature-brand-logo-in-emails.yml b/changelogs/unreleased/feature-brand-logo-in-emails.yml new file mode 100644 index 00000000000..a7674b9b25e --- /dev/null +++ b/changelogs/unreleased/feature-brand-logo-in-emails.yml @@ -0,0 +1,4 @@ +--- +title: Brand header logo for pipeline emails +merge_request: 9049 +author: Alexis Reigel -- cgit v1.2.3 From c1e94479bd565a3deebe7452f186815157082cbb Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Fri, 17 Feb 2017 10:05:13 +0100 Subject: restrict height of the custom brand logo in emails --- app/helpers/emails_helper.rb | 5 ++++- spec/helpers/emails_helper_spec.rb | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index 3beddff9206..a6d9e37ac76 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -54,7 +54,10 @@ module EmailsHelper def header_logo if brand_item && brand_item.header_logo? - brand_header_logo + image_tag( + brand_item.header_logo, + style: 'height: 50px' + ) else image_tag( image_url('mailers/gitlab_header_logo.gif'), diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb index b9519e387eb..cd112dbb2fb 100644 --- a/spec/helpers/emails_helper_spec.rb +++ b/spec/helpers/emails_helper_spec.rb @@ -52,7 +52,7 @@ describe EmailsHelper do ) expect(header_logo).to eq( - %{Dk} + %{Dk} ) end end -- cgit v1.2.3 From ad88cf3e1747f1214d6d5de45862cdac98b11504 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Fri, 10 Feb 2017 10:32:59 +0000 Subject: updated ci skip regex to accept underscores and hyphens --- app/services/ci/create_pipeline_service.rb | 3 +- spec/services/ci/create_pipeline_service_spec.rb | 71 ++++++++++-------------- 2 files changed, 31 insertions(+), 43 deletions(-) diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index e3bc9847200..38a85e9fc42 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -59,7 +59,8 @@ module Ci private def skip_ci? - pipeline.git_commit_message =~ /\[(ci skip|skip ci)\]/i if pipeline.git_commit_message + return false unless pipeline.git_commit_message + pipeline.git_commit_message =~ /\[(ci[ _-]skip|skip[ _-]ci)\]/i end def commit diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index ceaca96e25b..8459a3d8cfb 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -79,66 +79,53 @@ describe Ci::CreatePipelineService, services: true do context 'when commit contains a [ci skip] directive' do let(:message) { "some message[ci skip]" } - let(:messageFlip) { "some message[skip ci]" } - let(:capMessage) { "some message[CI SKIP]" } - let(:capMessageFlip) { "some message[SKIP CI]" } + + ci_messages = [ + "some message[ci skip]", + "some message[skip ci]", + "some message[CI SKIP]", + "some message[SKIP CI]", + "some message[ci_skip]", + "some message[skip_ci]", + "some message[ci-skip]", + "some message[skip-ci]" + ] before do allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message } end - it "skips builds creation if there is [ci skip] tag in commit message" do - commits = [{ message: message }] - pipeline = execute(ref: 'refs/heads/master', - before: '00000000', - after: project.commit.id, - commits: commits) + ci_messages.each do |ci_message| + it "skips builds creation if the commit message is #{ci_message}" do + commits = [{ message: ci_message }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) - expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq("skipped") - end - - it "skips builds creation if there is [skip ci] tag in commit message" do - commits = [{ message: messageFlip }] - pipeline = execute(ref: 'refs/heads/master', - before: '00000000', - after: project.commit.id, - commits: commits) - - expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq("skipped") + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq("skipped") + end end - it "skips builds creation if there is [CI SKIP] tag in commit message" do - commits = [{ message: capMessage }] - pipeline = execute(ref: 'refs/heads/master', - before: '00000000', - after: project.commit.id, - commits: commits) - - expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq("skipped") - end + it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do + allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" } - it "skips builds creation if there is [SKIP CI] tag in commit message" do - commits = [{ message: capMessageFlip }] + commits = [{ message: "some message" }] pipeline = execute(ref: 'refs/heads/master', before: '00000000', after: project.commit.id, commits: commits) expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq("skipped") + expect(pipeline.builds.first.name).to eq("rspec") end - it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do - allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" } + it "does not skip builds creation if the commit message is nil" do + allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { nil } - commits = [{ message: "some message" }] + commits = [{ message: nil }] pipeline = execute(ref: 'refs/heads/master', before: '00000000', after: project.commit.id, -- cgit v1.2.3 From 3bf60f8a64d7f6fd0d642378e6ed70785b65405c Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 22 Feb 2017 12:34:16 +0100 Subject: Change images location (s/images/img) --- doc/pages/images/add_certificate_to_pages.png | Bin 58581 -> 0 bytes doc/pages/images/choose_ci_template.png | Bin 75580 -> 0 bytes doc/pages/images/dns_a_record_example.png | Bin 10681 -> 0 bytes doc/pages/images/dns_cname_record_example.png | Bin 11972 -> 0 bytes doc/pages/images/remove_fork_relashionship.png | Bin 39941 -> 0 bytes doc/pages/images/setup_ci.png | Bin 27380 -> 0 bytes doc/pages/img/add_certificate_to_pages.png | Bin 0 -> 14608 bytes doc/pages/img/choose_ci_template.png | Bin 0 -> 23532 bytes doc/pages/img/dns_a_record_example.png | Bin 0 -> 4709 bytes doc/pages/img/dns_cname_record_example.png | Bin 0 -> 5004 bytes doc/pages/img/remove_fork_relashionship.png | Bin 0 -> 13646 bytes doc/pages/img/setup_ci.png | Bin 0 -> 10033 bytes doc/pages/pages_quick_start_guide.md | 12 ++++++------ ...ic_sites_domains_dns_records_ssl_tls_certificates.md | 6 +++--- 14 files changed, 9 insertions(+), 9 deletions(-) delete mode 100644 doc/pages/images/add_certificate_to_pages.png delete mode 100644 doc/pages/images/choose_ci_template.png delete mode 100644 doc/pages/images/dns_a_record_example.png delete mode 100644 doc/pages/images/dns_cname_record_example.png delete mode 100644 doc/pages/images/remove_fork_relashionship.png delete mode 100644 doc/pages/images/setup_ci.png create mode 100644 doc/pages/img/add_certificate_to_pages.png create mode 100644 doc/pages/img/choose_ci_template.png create mode 100644 doc/pages/img/dns_a_record_example.png create mode 100644 doc/pages/img/dns_cname_record_example.png create mode 100644 doc/pages/img/remove_fork_relashionship.png create mode 100644 doc/pages/img/setup_ci.png diff --git a/doc/pages/images/add_certificate_to_pages.png b/doc/pages/images/add_certificate_to_pages.png deleted file mode 100644 index 94d48fafa6e..00000000000 Binary files a/doc/pages/images/add_certificate_to_pages.png and /dev/null differ diff --git a/doc/pages/images/choose_ci_template.png b/doc/pages/images/choose_ci_template.png deleted file mode 100644 index dea4ad2ed28..00000000000 Binary files a/doc/pages/images/choose_ci_template.png and /dev/null differ diff --git a/doc/pages/images/dns_a_record_example.png b/doc/pages/images/dns_a_record_example.png deleted file mode 100644 index f035d30984c..00000000000 Binary files a/doc/pages/images/dns_a_record_example.png and /dev/null differ diff --git a/doc/pages/images/dns_cname_record_example.png b/doc/pages/images/dns_cname_record_example.png deleted file mode 100644 index 93e011d743d..00000000000 Binary files a/doc/pages/images/dns_cname_record_example.png and /dev/null differ diff --git a/doc/pages/images/remove_fork_relashionship.png b/doc/pages/images/remove_fork_relashionship.png deleted file mode 100644 index 8c4fab92990..00000000000 Binary files a/doc/pages/images/remove_fork_relashionship.png and /dev/null differ diff --git a/doc/pages/images/setup_ci.png b/doc/pages/images/setup_ci.png deleted file mode 100644 index 5569f6aacf5..00000000000 Binary files a/doc/pages/images/setup_ci.png and /dev/null differ diff --git a/doc/pages/img/add_certificate_to_pages.png b/doc/pages/img/add_certificate_to_pages.png new file mode 100644 index 00000000000..d92a981dc60 Binary files /dev/null and b/doc/pages/img/add_certificate_to_pages.png differ diff --git a/doc/pages/img/choose_ci_template.png b/doc/pages/img/choose_ci_template.png new file mode 100644 index 00000000000..0697542abc8 Binary files /dev/null and b/doc/pages/img/choose_ci_template.png differ diff --git a/doc/pages/img/dns_a_record_example.png b/doc/pages/img/dns_a_record_example.png new file mode 100644 index 00000000000..b923730388a Binary files /dev/null and b/doc/pages/img/dns_a_record_example.png differ diff --git a/doc/pages/img/dns_cname_record_example.png b/doc/pages/img/dns_cname_record_example.png new file mode 100644 index 00000000000..d64a843a283 Binary files /dev/null and b/doc/pages/img/dns_cname_record_example.png differ diff --git a/doc/pages/img/remove_fork_relashionship.png b/doc/pages/img/remove_fork_relashionship.png new file mode 100644 index 00000000000..f5b5e543f21 Binary files /dev/null and b/doc/pages/img/remove_fork_relashionship.png differ diff --git a/doc/pages/img/setup_ci.png b/doc/pages/img/setup_ci.png new file mode 100644 index 00000000000..7ce0431f4d4 Binary files /dev/null and b/doc/pages/img/setup_ci.png differ diff --git a/doc/pages/pages_quick_start_guide.md b/doc/pages/pages_quick_start_guide.md index b86b451ba5e..b0a33136d06 100644 --- a/doc/pages/pages_quick_start_guide.md +++ b/doc/pages/pages_quick_start_guide.md @@ -46,8 +46,8 @@ Watch the [video tutorial](https://youtu.be/TWqh9MtT4Bg) we've created for the s 1. Choose your SSG template 1. Fork a project from the [Pages group](https://gitlab.com/pages) 1. Remove the fork relationship by navigating to your **Project**'s **Settings** > **Edit Project** - - ![remove fork relashionship](images/remove_fork_relashionship.png) + + ![remove fork relashionship](img/remove_fork_relashionship.png) 1. Enable Shared Runners for your fork: navigate to your **Project**'s **Settings** > **CI/CD Pipelines** 1. Trigger a build (push a change to any file) @@ -73,17 +73,17 @@ To turn a **project website** forked from the Pages group into a **user/group** 1. From your **Project**'s **[Dashboard](https://gitlab.com/dashboard/projects)**, click **New project**, and name it considering the [examples above](#practical-examples). 1. Clone it to your local computer, add your website files to your project, add, commit and push to GitLab. 1. From the your **Project**'s page, click **Set up CI**: - - ![setup GitLab CI](images/setup_ci.png) + + ![setup GitLab CI](img/setup_ci.png) 1. Choose one of the templates from the dropbox menu. Pick up the template corresponding to the SSG you're using (or plain HTML). - ![gitlab-ci templates](images/choose_ci_template.png) + ![gitlab-ci templates](img/choose_ci_template.png) Once you have both site files and `.gitlab-ci.yml` in your project's root, GitLab CI will build your site and deploy it with Pages. Once the first build passes, you see your site is live by navigating to your **Project**'s **Settings** > **Pages**, where you'll find its default URL. > **Notes:** -> +> > - GitLab Pages [supports any SSG](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/), but, if you don't find yours among the templates, you'll need to configure your own `.gitlab-ci.yml`. Do do that, please read through the article [Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](pages_creating_and_tweaking_gitlab-ci_.html). New SSGs are very welcome among the [example projects](https://gitlab.com/pages). If you set up a new one, please [contribute](https://gitlab.com/pages/pages.gitlab.io/blob/master/CONTRIBUTING.md) to our examples. > > - The second step _"Clone it to your local computer"_, can be done differently, achieving the same results: instead of cloning the bare repository to you local computer and moving your site files into it, you can run `git init` in your local website directory, add the remote URL: `git remote add origin git@gitlab.com:namespace/project-name.git`, then add, commit, and push. diff --git a/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md b/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md index c6e2cab0bc3..2564d87ac3a 100644 --- a/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md +++ b/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md @@ -86,7 +86,7 @@ In case you want to point a root domain (`example.com`) to your GitLab Pages sit **Practical Example:** -![DNS A record pointing to GitLab.com Pages server](images/dns_a_record_example.png) +![DNS A record pointing to GitLab.com Pages server](img/dns_a_record_example.png) #### DNS CNAME record @@ -96,7 +96,7 @@ Notice that, despite it's a user or project website, the `CNAME` should point to **Practical Example:** -![DNS CNAME record pointing to GitLab.com project](images/dns_cname_record_example.png) +![DNS CNAME record pointing to GitLab.com project](img/dns_cname_record_example.png) #### TL;DR @@ -138,7 +138,7 @@ Regardless the CA you choose, the steps to add your certificate to your Pages pr 1. An intermediary certificate 1. A public key -![Pages project - adding certificates](images/add_certificate_to_pages.png) +![Pages project - adding certificates](img/add_certificate_to_pages.png) These fields are found under your **Project**'s **Settings** > **Pages** > **New Domain**. -- cgit v1.2.3 From bb483230e60692e203775346ac3ad4407e3864a5 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Thu, 12 Jan 2017 14:00:06 +0000 Subject: added specs for version check image --- app/assets/javascripts/version_check_image.js.es6 | 16 +++++------ spec/features/help_pages_spec.rb | 26 +++++++++++++++++ spec/helpers/version_check_helper_spec.rb | 34 +++++++++++++++++++++++ spec/javascripts/.eslintrc | 3 +- spec/javascripts/helpers/class_spec_helper.js.es6 | 2 ++ spec/javascripts/version_check_image_spec.js.es6 | 33 ++++++++++++++++++++++ 6 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 spec/helpers/version_check_helper_spec.rb create mode 100644 spec/javascripts/version_check_image_spec.js.es6 diff --git a/app/assets/javascripts/version_check_image.js.es6 b/app/assets/javascripts/version_check_image.js.es6 index 1fa2b5ac399..d4f716acb72 100644 --- a/app/assets/javascripts/version_check_image.js.es6 +++ b/app/assets/javascripts/version_check_image.js.es6 @@ -1,10 +1,10 @@ -(() => { - class VersionCheckImage { - static bindErrorEvent(imageElement) { - imageElement.off('error').on('error', () => imageElement.hide()); - } +class VersionCheckImage { + static bindErrorEvent(imageElement) { + imageElement.off('error').on('error', () => imageElement.hide()); } +} - window.gl = window.gl || {}; - gl.VersionCheckImage = VersionCheckImage; -})(); +window.gl = window.gl || {}; +gl.VersionCheckImage = VersionCheckImage; + +module.exports = VersionCheckImage; diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb index 40a1fced8d8..e0b2404e60a 100644 --- a/spec/features/help_pages_spec.rb +++ b/spec/features/help_pages_spec.rb @@ -33,4 +33,30 @@ describe 'Help Pages', feature: true do it_behaves_like 'help page', prefix: '/gitlab' end end + + context 'in a production environment with version check enabled', js: true do + before do + allow(Rails.env).to receive(:production?) { true } + allow(current_application_settings).to receive(:version_check_enabled) { true } + allow_any_instance_of(VersionCheck).to receive(:url) { '/version-check-url' } + + login_as :user + visit help_path + end + + it 'should display a version check image' do + expect(find('.js-version-status-badge')).to be_visible + end + + it 'should have a src url' do + expect(find('.js-version-status-badge')['src']).to match(/\/version-check-url/) + end + + it 'should hide the version check image if the image request fails' do + # We use '--load-images=no' with poltergeist so we must trigger manually + execute_script("$('.js-version-status-badge').trigger('error');") + + expect(find('.js-version-status-badge', visible: false)).not_to be_visible + end + end end diff --git a/spec/helpers/version_check_helper_spec.rb b/spec/helpers/version_check_helper_spec.rb new file mode 100644 index 00000000000..889fe441171 --- /dev/null +++ b/spec/helpers/version_check_helper_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe VersionCheckHelper do + describe '#version_status_badge' do + it 'should return nil if not dev environment and not enabled' do + allow(Rails.env).to receive(:production?) { false } + allow(current_application_settings).to receive(:version_check_enabled) { false } + + expect(helper.version_status_badge).to be(nil) + end + + context 'when production and enabled' do + before do + allow(Rails.env).to receive(:production?) { true } + allow(current_application_settings).to receive(:version_check_enabled) { true } + allow_any_instance_of(VersionCheck).to receive(:url) { 'https://version.host.com/check.svg?gitlab_info=xxx' } + + @image_tag = helper.version_status_badge + end + + it 'should return an image tag' do + expect(@image_tag).to match(/^
'); + }); + + it('registers an error event', function () { + spyOn($.prototype, 'on'); + spyOn($.prototype, 'off').and.callFake(function () { return this; }); + + VersionCheckImage.bindErrorEvent(this.imageElement); + + expect($.prototype.off).toHaveBeenCalledWith('error'); + expect($.prototype.on).toHaveBeenCalledWith('error', jasmine.any(Function)); + }); + + it('hides the imageElement on error', function () { + spyOn($.prototype, 'hide'); + + VersionCheckImage.bindErrorEvent(this.imageElement); + + this.imageElement.trigger('error'); + + expect($.prototype.hide).toHaveBeenCalled(); + }); + }); +}); -- cgit v1.2.3 From 87fe6a27dd4c17cf6b202de809faa821712a3e17 Mon Sep 17 00:00:00 2001 From: Mark Fletcher Date: Wed, 22 Feb 2017 17:07:17 +0530 Subject: Document when current coverage configuration option was introduced * Introduced in v8.17 [skip ci] --- changelogs/unreleased/28524-gitlab-ci-yml-coverage-key-is-unknown.yml | 4 ++++ doc/ci/yaml/README.md | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 changelogs/unreleased/28524-gitlab-ci-yml-coverage-key-is-unknown.yml diff --git a/changelogs/unreleased/28524-gitlab-ci-yml-coverage-key-is-unknown.yml b/changelogs/unreleased/28524-gitlab-ci-yml-coverage-key-is-unknown.yml new file mode 100644 index 00000000000..eda5764c13e --- /dev/null +++ b/changelogs/unreleased/28524-gitlab-ci-yml-coverage-key-is-unknown.yml @@ -0,0 +1,4 @@ +--- +title: Document when current coverage configuration option was introduced +merge_request: 9443 +author: diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index a73598df812..dd3ba1283f8 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1003,6 +1003,9 @@ job: ### coverage +**Notes:** +- [Introduced][ce-7447] in GitLab 8.17. + `coverage` allows you to configure how code coverage will be extracted from the job output. @@ -1361,3 +1364,4 @@ CI with various languages. [ce-6669]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6669 [variables]: ../variables/README.md [ce-7983]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7983 +[ce-7447]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7447 -- cgit v1.2.3 From 699aab02d09cee93570c41ba19251f7a772f8171 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 22 Feb 2017 13:03:05 +0100 Subject: Generalize the naming of the getting started docs --- doc/pages/getting_started_part_one.md | 164 ++++++++++++ doc/pages/getting_started_part_three.md | 279 +++++++++++++++++++++ doc/pages/getting_started_part_two.md | 112 +++++++++ doc/pages/index.md | 46 ++-- doc/pages/pages_creating_and_tweaking_gitlab-ci.md | 279 --------------------- doc/pages/pages_quick_start_guide.md | 112 --------- ...tes_domains_dns_records_ssl_tls_certificates.md | 164 ------------ doc/user/project/pages/index.md | 2 +- 8 files changed, 579 insertions(+), 579 deletions(-) create mode 100644 doc/pages/getting_started_part_one.md create mode 100644 doc/pages/getting_started_part_three.md create mode 100644 doc/pages/getting_started_part_two.md delete mode 100644 doc/pages/pages_creating_and_tweaking_gitlab-ci.md delete mode 100644 doc/pages/pages_quick_start_guide.md delete mode 100644 doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md diff --git a/doc/pages/getting_started_part_one.md b/doc/pages/getting_started_part_one.md new file mode 100644 index 00000000000..8a98afeeb86 --- /dev/null +++ b/doc/pages/getting_started_part_one.md @@ -0,0 +1,164 @@ +# GitLab Pages from A to Z: Part 1 + +> Type: user guide +> +> Level: beginner + +- **Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates** +- _[Part 2: Quick Start Guide - Setting Up GitLab Pages](getting_started_part_two.md)_ +- _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md)_ + +---- + +This is a comprehensive guide, made for those who want to publish a website with GitLab Pages but aren't familiar with the entire process involved. + +To **enable** GitLab Pages for GitLab CE (Community Edition) and GitLab EE (Enterprise Edition), please read the [admin documentation](https://docs.gitlab.com/ce/administration/pages/index.html), and/or watch this [video tutorial](https://youtu.be/dD8c7WNcc6s). + +> For this guide, we assume you already have GitLab Pages server up and running for your GitLab instance. + +## What you need to know before getting started + +Before we begin, let's understand a few concepts first. + +### Static Sites + +GitLab Pages only supports static websites, meaning, your output files must be HTML, CSS, and JavaScript only. + +To create your static site, you can either hardcode in HTML, CSS, and JS, or use a [Static Site Generator (SSG)](https://www.staticgen.com/) to simplify your code and build the static site for you, which is highly recommendable and much faster than hardcoding. + +#### Further Reading + +- Read through this technical overview on [Static versus Dynamic Websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) +- Understand [how modern Static Site Generators work](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) and what you can add to your static site +- You can use [any SSG with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) +- Fork an [example project](https://gitlab.com/pages) to build your website based upon + +### GitLab Pages Domain + +If you set up a GitLab Pages project on GitLab.com, it will automatically be accessible under a [subdomain of `.pages.io`](https://docs.gitlab.com/ce/user/project/pages/). The `` is defined by your username on GitLab.com, or the group name you created this project under. + +> Note: If you use your own GitLab instance to deploy your site with GitLab Pages, check with your sysadmin what's your Pages wildcard domain. This guide is valid for any GitLab instance, you just need to replace Pages wildcard domain on GitLab.com (`*.gitlab.io`) with your own. + +#### Practical examples + +**Project Websites:** + +- You created a project called `blog` under your username `john`, therefore your project URL is `https://gitlab.com/john/blog/`. Once you enable GitLab Pages for this project, and build your site, it will be available under `https://john.gitlab.io/blog/`. +- You created a group for all your websites called `websites`, and a project within this group is called `blog`. Your project URL is `https://gitlab.com/websites/blog/`. Once you enable GitLab Pages for this project, the site will live under `https://websites.gitlab.io/blog/`. + +**User and Group Websites:** + +- Under your username, `john`, you created a project called `john.gitlab.io`. Your project URL will be `https://gitlab.com/john/john.gitlab.io`. Once you enable GitLab Pages for your project, your website will be published under `https://john.gitlab.io`. +- Under your group `websites`, you created a project called `websites.gitlab.io`. your project's URL will be `https://gitlab.com/websites/websites.gitlab.io`. Once you enable GitLab Pages for your project, your website will be published under `https://websites.gitlab.io`. + +**General example:** + +- On GitLab.com, a project site will always be available under `https://namespace.gitlab.io/project-name` +- On GitLab.com, a user or group website will be available under `https://namespace.gitlab.io/` +- On your GitLab instance, replace `gitlab.io` above with your Pages server domain. Ask your sysadmin for this information. + +### DNS Records + +A Domain Name System (DNS) web service routes visitors to websites by translating domain names (such as `www.example.com`) into the numeric IP addresses (such as `192.0.2.1`) that computers use to connect to each other. + +A DNS record is created to point a (sub)domain to a certain location, which can be an IP address or another domain. In case you want to use GitLab Pages with your own (sub)domain, you need to access your domain's registrar control panel to add a DNS record pointing it back to your GitLab Pages site. + +Note that **how to** add DNS records depends on which server your domain is hosted on. Every control panel has its own place to do it. If you are not an admin of your domain, and don't have access to your registrar, you'll need to ask for the technical support of your hosting service to do it for you. + +To help you out, we've gathered some instructions on how to do that for the most popular hosting services: + +- [Amazon](http://docs.aws.amazon.com/gettingstarted/latest/swh/getting-started-configure-route53.html) +- [Bluehost](https://my.bluehost.com/cgi/help/559) +- [CloudFlare](https://support.cloudflare.com/hc/en-us/articles/200169096-How-do-I-add-A-records-) +- [cPanel](https://documentation.cpanel.net/display/ALD/Edit+DNS+Zone) +- [DreamHost](https://help.dreamhost.com/hc/en-us/articles/215414867-How-do-I-add-custom-DNS-records-) +- [Go Daddy](https://www.godaddy.com/help/add-an-a-record-19238) +- [Hostgator](http://support.hostgator.com/articles/changing-dns-records) +- [Inmotion hosting](https://my.bluehost.com/cgi/help/559) +- [Media Temple](https://mediatemple.net/community/products/dv/204403794/how-can-i-change-the-dns-records-for-my-domain) +- [Microsoft](https://msdn.microsoft.com/en-us/library/bb727018.aspx) + +If your hosting service is not listed above, you can just try to search the web for "how to add dns record on ". + +#### DNS A record + +In case you want to point a root domain (`example.com`) to your GitLab Pages site, deployed to `namespace.gitlab.io`, you need to log into your domain's admin control panel and add a DNS `A` record pointing your domain to Pages' server IP address. For projects on GitLab.com, this IP is `104.208.235.32`. For projects leaving in other GitLab instances (CE or EE), please contact your sysadmin asking for this information (which IP address is Pages server running on your instance). + +**Practical Example:** + +![DNS A record pointing to GitLab.com Pages server](img/dns_a_record_example.png) + +#### DNS CNAME record + +In case you want to point a subdomain (`hello-world.example.com`) to your GitLab Pages site initially deployed to `namespace.gitlab.io`, you need to log into your domain's admin control panel and add a DNS `CNAME` record pointing your subdomain to your website URL (`namespace.gitlab.io`) address. + +Notice that, despite it's a user or project website, the `CNAME` should point to your Pages domain (`namespace.gitlab.io`), without any `/project-name`. + +**Practical Example:** + +![DNS CNAME record pointing to GitLab.com project](img/dns_cname_record_example.png) + +#### TL;DR + +| From | DNS Record | To | +| ---- | ---------- | -- | +| domain.com | A | 104.208.235.32 | +| subdomain.domain.com | CNAME | namespace.gitlab.io | + +> **Notes**: +> +> - **Do not** use a CNAME record if you want to point your `domain.com` to your GitLab Pages site. Use an `A` record instead. +> - **Do not** add any special chars after the default Pages domain. E.g., **do not** point your `subdomain.domain.com` to `namespace.gitlab.io.` or `namespace.gitlab.io/`. + +### SSL/TLS Certificates + +Every GitLab Pages project on GitLab.com will be available under HTTPS for the default Pages domain (`*.gitlab.io`). Once you set up your Pages project with your custom (sub)domain, if you want it secured by HTTPS, you will have to issue a certificate for that (sub)domain and install it on your project. + +> Note: certificates are NOT required to add to your custom (sub)domain on your GitLab Pages project, though they are highly recommendable. + +The importance of having any website securely served under HTTPS is explained on the introductory section of the blog post [Secure GitLab Pages with StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/#https-a-quick-overview). + +The reason why certificates are so important is that they encrypt the connection between the **client** (you, me, your visitors) and the **server** (where you site lives), through a keychain of authentications and validations. + +### Issuing Certificates + +GitLab Pages accepts [PEM](https://support.quovadisglobal.com/kb/a37/what-is-pem-format.aspx) certificates issued by [Certificate Authorities (CA)](https://en.wikipedia.org/wiki/Certificate_authority) and self-signed certificates. Of course, [you'd rather issue a certificate than generate a self-signed](https://en.wikipedia.org/wiki/Self-signed_certificate), for security reasons and for having browsers trusting your site's certificate. + +There are several different kinds of certificates, each one with certain security level. A static personal website will not require the same security level as an online banking web app, for instance. There are a couple Certificate Authorities that offer free certificates, aiming to make the internet more secure to everyone. The most popular is [Let's Encrypt](https://letsencrypt.org/), which issues certificates trusted by most of browsers, it's open source, and free to use. Please read through this tutorial to understand [how to secure your GitLab Pages website with Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/). + +With the same popularity, there are [certificates issued by CloudFlare](https://www.cloudflare.com/ssl/), which also offers a [free CDN service](https://blog.cloudflare.com/cloudflares-free-cdn-and-you/). Their certs are valid up to 15 years. Read through the tutorial on [how to add a CloudFlare Certificate to your GitLab Pages website](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/). + +### Adding certificates to your project + +Regardless the CA you choose, the steps to add your certificate to your Pages project are the same. + +#### What do you need + +1. A PEM certificate +1. An intermediary certificate +1. A public key + +![Pages project - adding certificates](img/add_certificate_to_pages.png) + +These fields are found under your **Project**'s **Settings** > **Pages** > **New Domain**. + +#### What's what? + +- A PEM certificate is the certificate generated by the CA, which needs to be added to the field **Certificate (PEM)**. +- An [intermediary certificate][] \(aka "root certificate"\) is the part of the encryption keychain that identifies the CA. Usually it's combined with the PEM certificate, but there are some cases in which you need to add them manually. [CloudFlare certs](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) are one of these cases. +- A public key is an encrypted key which validates your PEM against your domain. + +#### Now what? + +Now that you hopefully understand why you need all of this, it's simple: + +- Your PEM certificate needs to be added to the first field +- If your certificate is missing its intermediary, copy and paste the root certificate (usually available from your CA website) and paste it in the [same field as your PEM certificate](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/), just jumping a line between them. +- Copy your public key and paste it in the last field + +> Note: **do not** open certificates or encryption keys in regular text editors. Always use code editors (such as Sublime Text, Atom, Dreamweaver, Brackets, etc). + +## Further Reading + +- Read through GitLab Pages from A to Z _[Part 2: Quick Start Guide - Setting Up GitLab Pages](getting_started_part_two.md)_ +- Read through GitLab Pages from A to Z _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md)_ diff --git a/doc/pages/getting_started_part_three.md b/doc/pages/getting_started_part_three.md new file mode 100644 index 00000000000..0bc6a601a09 --- /dev/null +++ b/doc/pages/getting_started_part_three.md @@ -0,0 +1,279 @@ +# GitLab Pages from A to Z: Part 3 + +> Type: user guide +> +> Level: intermediate + +- _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](getting_started_part_one.md)_ +- _[Part 2: Quick Start Guide - Setting Up GitLab Pages](getting_started_part_two.md)_ +- **Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages** + +--- + +### Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages + +[GitLab CI](https://about.gitlab.com/gitlab-ci/) serves numerous purposes, to build, test, and deploy your app from GitLab through [Continuous Integration, Continuous Delivery, and Continuous Deployment](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) methods. You will need it to build your website with GitLab Pages, and deploy it to the Pages server. + +What this file actually does is telling the [GitLab Runner](https://docs.gitlab.com/runner/) to run scripts as you would do from the command line. The Runner acts as your terminal. GitLab CI tells the Runner which commands to run. Both are built-in in GitLab, and you don't need to set up anything for them to work. + +Explaining [every detail of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html) and GitLab Runner is out of the scope of this guide, but we'll need to understand just a few things to be able to write our own `.gitlab-ci.yml` or tweak an existing one. It's an [Yaml](http://docs.ansible.com/ansible/YAMLSyntax.html) file, with its own syntax. You can always check your CI syntax with the [GitLab CI Lint Tool](https://gitlab.com/ci/lint). + +**Practical Example:** + +Let's consider you have a [Jekyll](https://jekyllrb.com/) site. To build it locally, you would open your terminal, and run `jekyll build`. Of course, before building it, you had to install Jekyll in your computer. For that, you had to open your terminal and run `gem install jekyll`. Right? GitLab CI + GitLab Runner do the same thing. But you need to write in the `.gitlab-ci.yml` the script you want to run so the GitLab Runner will do it for you. It looks more complicated then it is. What you need to tell the Runner: + +``` +$ gem install jekyll +$ jekyll build +``` + +#### Script + +To transpose this script to Yaml, it would be like this: + +```yaml +script: + - gem install jekyll + - jekyll build +``` + +#### Job + +So far so good. Now, each `script`, in GitLab is organized by a `job`, which is a bunch of scripts and settings you want to apply to that specific task. + +```yaml +job: + script: + - gem install jekyll + - jekyll build +``` + +For GitLab Pages, this `job` has a specific name, called `pages`, which tells the Runner you want that task to deploy your website with GitLab Pages: + +```yaml +pages: + script: + - gem install jekyll + - jekyll build +``` + +#### `public` Dir + +We also need to tell Jekyll where do you want the website to build, and GitLab Pages will only consider files in a directory called `public`. To do that with Jekyll, we need to add a flag specifying the [destination (`-d`)](https://jekyllrb.com/docs/usage/) of the built website: `jekyll build -d public`. Of course, we need to tell this to our Runner: + +```yaml +pages: + script: + - gem install jekyll + - jekyll build -d public +``` + +#### Artifacts + +We also need to tell the Runner that this _job_ generates _artifacts_, which is the site built by Jekyll. Where are these artifacts stored? In the `public` directory: + +```yaml +pages: + script: + - gem install jekyll + - jekyll build -d public + artifacts: + paths: + - public +``` + +The script above would be enough to build your Jekyll site with GitLab Pages. But, from Jekyll 3.4.0 on, its default template originated by `jekyll new project` requires [Bundler](http://bundler.io/) to install Jekyll dependencies and the default theme. To adjust our script to meet these new requirements, we only need to install and build Jekyll with Bundler: + +```yaml +pages: + script: + - bundle install + - bundle exec jekyll build -d public + artifacts: + paths: + - public +``` + +That's it! A `.gitlab-ci.yml` with the content above would deploy your Jekyll 3.4.0 site with GitLab Pages. This is the minimum configuration for our example. On the steps below, we'll refine the script by adding extra options to our GitLab CI. + +#### Image + +At this point, you probably ask yourself: "okay, but to install Jekyll I need Ruby. Where is Ruby on that script?". The answer is simple: the first thing GitLab Runner will look for in your `.gitlab-ci.yml` is a [Docker](https://www.docker.com/) image specifying what do you need in your container to run that script: + +```yaml +image: ruby:2.3 + +pages: + script: + - bundle install + - bundle exec jekyll build -d public + artifacts: + paths: + - public +``` + +In this case, you're telling the Runner to pull this image, which contains Ruby 2.3 as part of its file system. When you don't specify this image in your configuration, the Runner will use a default image, which is Ruby 2.1. + +If your SSG needs [NodeJS](https://nodejs.org/) to build, you'll need to specify which image you want to use, and this image should contain NodeJS as part of its file system. E.g., for a [Hexo](https://gitlab.com/pages/hexo) site, you can use `image: node:4.2.2`. + +> Note: we're not trying to explain what a Docker image is, we just need to introduce the concept with a minimum viable explanation. To know more about Docker images, please visit their website or take a look at a [summarized explanation](http://paislee.io/how-to-automate-docker-deployments/) here. + +Let's go a little further. + +#### Branching + +If you use GitLab as a version control platform, you will have your branching strategy to work on your project. Meaning, you will have other branches in your project, but you'll want only pushes to the default branch (usually `master`) to be deployed to your website. To do that, we need to add another line to our CI, telling the Runner to only perform that _job_ called `pages` on the `master` branch `only`: + +```yaml +image: ruby:2.3 + +pages: + script: + - bundle install + - bundle exec jekyll build -d public + artifacts: + paths: + - public + only: + - master +``` + +#### Stages + +Another interesting concept to keep in mind are build stages. Your web app can pass through a lot of tests and other tasks until it's deployed to staging or production environments. There are three default stages on GitLab CI: build, test, and deploy. To specify which stage your _job_ is running, simply add another line to your CI: + +```yaml +image: ruby:2.3 + +pages: + stage: deploy + script: + - bundle install + - bundle exec jekyll build -d public + artifacts: + paths: + - public + only: + - master +``` + +You might ask yourself: "why should I bother with stages at all?" Well, let's say you want to be able to test your script and check the built site before deploying your site to production. You want to run the test exactly as your script will do when you push to `master`. It's simple, let's add another task (_job_) to our CI, telling it to test every push to other branches, `except` the `master` branch: + +```yaml +image: ruby:2.3 + +pages: + stage: deploy + script: + - bundle install + - bundle exec jekyll build -d public + artifacts: + paths: + - public + only: + - master + +test: + stage: test + script: + - bundle install + - bundle exec jekyll build -d test + artifacts: + paths: + - test + except: + - master +``` + +The `test` job is running on the stage `test`, Jekyll will build the site in a directory called `test`, and this job will affect all the branches except `master`. + +The best benefit of applying _stages_ to different _jobs_ is that every job in the same stage builds in parallel. So, if your web app needs more than one test before being deployed, you can run all your test at the same time, it's not necessary to wait on test to finish to run the other. Of course, this is just a brief introduction of GitLab CI and GitLab Runner, which are tools much more powerful than that. This is what you need to be able to create and tweak your builds for your GitLab Pages site. + +#### Before Script + +To avoid running the same script multiple times across your _jobs_, you can add the parameter `before_script`, in which you specify which commands you want to run for every single _job_. In our example, notice that we run `bundle install` for both jobs, `pages` and `test`. We don't need to repeat it: + +```yaml +image: ruby:2.3 + +before_script: + - bundle install + +pages: + stage: deploy + script: + - bundle exec jekyll build -d public + artifacts: + paths: + - public + only: + - master + +test: + stage: test + script: + - bundle exec jekyll build -d test + artifacts: + paths: + - test + except: + - master +``` + +#### Caching Dependencies + +If you want to cache the installation files for your projects dependencies, for building faster, you can use the parameter `cache`. For this example, we'll cache Jekyll dependencies in a `vendor` directory when we run `bundle install`: + +```yaml +image: ruby:2.3 + +cache: + paths: + - vendor/ + +before_script: + - bundle install --path vendor + +pages: + stage: deploy + script: + - bundle exec jekyll build -d public + artifacts: + paths: + - public + only: + - master + +test: + stage: test + script: + - bundle exec jekyll build -d test + artifacts: + paths: + - test + except: + - master +``` + +For this specific case, we need to exclude `/vendor` from Jekyll `_config.yml` file, otherwise Jekyll will understand it as a regular directory to build together with the site: + +```yml +exclude: + - vendor +``` + +There we go! Now our GitLab CI not only builds our website, but also **continuously test** pushes to feature-branches, **caches** dependencies installed with Bundler, and **continuously deploy** every push to the `master` branch. + +### Advanced GitLab CI for GitLab Pages + +What you can do with GitLab CI is pretty much up to your creativity. Once you get used to it, you start creating awesome scripts that automate most of tasks you'd do manually in the past. Read through the [documentation of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html) to understand how to go even further on your scripts. + +- On this blog post, understand the concept of [using GitLab CI `environments` to deploy your web app to staging and production](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/). +- On this post, learn [how to run jobs sequentially, in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/) +- On this blog post, we go through the process of [pulling specific directories from different projects](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/) to deploy this website you're looking at, docs.gitlab.com. +- On this blog post, we teach you [how to use GitLab Pages to produce a code coverage report](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/). + +## Further Reading + +- Read through _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](getting_started_part_one.md)_ +- Read through _[Part 2: Quick Start Guide - Setting Up GitLab Pages](getting_started_part_two.md)_ diff --git a/doc/pages/getting_started_part_two.md b/doc/pages/getting_started_part_two.md new file mode 100644 index 00000000000..b1d58b5b024 --- /dev/null +++ b/doc/pages/getting_started_part_two.md @@ -0,0 +1,112 @@ +# GitLab Pages from A to Z: Part 2 + +> Type: user guide +> +> Level: beginner + +- _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](getting_started_part_one.md)_ +- **Part 2: Quick Start Guide - Setting Up GitLab Pages** +- _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md)_ + +---- + +## Setting Up GitLab Pages + +For a complete step-by-step tutorial, please read the blog post [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/). The following sections will explain what do you need and why do you need them. + + + +### What You Need to Get Started + +1. A project +1. A configuration file (`.gitlab-ci.yml`) to deploy your site +1. A specific `job` called `pages` in the configuration file that will make GitLab aware that you are deploying a GitLab Pages website + +#### Optional Features + +1. A custom domain or subdomain +1. A DNS pointing your (sub)domain to your Pages site + 1. **Optional**: an SSL/TLS certificate so your custom domain is accessible under HTTPS. + +### Project + +Your GitLab Pages project is a regular project created the same way you do for the other ones. To get started with GitLab Pages, you have two ways: + +- Fork one of the templates from Page Examples, or +- Create a new project from scratch + +Let's go over both options. + +#### Fork a Project to Get Started From + +To make things easy for you, we've created this [group](https://gitlab.com/pages) of default projects containing the most popular SSGs templates. + +Watch the [video tutorial](https://youtu.be/TWqh9MtT4Bg) we've created for the steps below. + +1. Choose your SSG template +1. Fork a project from the [Pages group](https://gitlab.com/pages) +1. Remove the fork relationship by navigating to your **Project**'s **Settings** > **Edit Project** + + ![remove fork relashionship](img/remove_fork_relashionship.png) + +1. Enable Shared Runners for your fork: navigate to your **Project**'s **Settings** > **CI/CD Pipelines** +1. Trigger a build (push a change to any file) +1. As soon as the build passes, your website will have been deployed with GitLab Pages. Your website URL will be available under your **Project**'s **Settings** > **Pages** + +To turn a **project website** forked from the Pages group into a **user/group** website, you'll need to: + +- Rename it to `namespace.gitlab.io`: navigate to **Project**'s **Settings** > **Edit Project** > **Rename repository** +- Adjust your SSG's [base URL](#urls-and-baseurls) to from `"project-name"` to `""`. This setting will be at a different place for each SSG, as each of them have their own structure and file tree. Most likelly, it will be in the SSG's config file. + +> **Notes:** +> +>1. Why do I need to remove the fork relationship? +> +> Unless you want to contribute to the original project, you won't need it connected to the upstream. A [fork](https://about.gitlab.com/2016/12/01/how-to-keep-your-fork-up-to-date-with-its-origin/#fork) is useful for submitting merge requests to the upstream. +> +> 2. Why do I need to enable Shared Runners? +> +> Shared Runners will run the script set by your GitLab CI configuration file. They're enabled by default to new projects, but not to forks. + +#### Create a Project from Scratch + +1. From your **Project**'s **[Dashboard](https://gitlab.com/dashboard/projects)**, click **New project**, and name it considering the [examples above](#practical-examples). +1. Clone it to your local computer, add your website files to your project, add, commit and push to GitLab. +1. From the your **Project**'s page, click **Set up CI**: + + ![setup GitLab CI](img/setup_ci.png) + +1. Choose one of the templates from the dropbox menu. Pick up the template corresponding to the SSG you're using (or plain HTML). + + ![gitlab-ci templates](img/choose_ci_template.png) + +Once you have both site files and `.gitlab-ci.yml` in your project's root, GitLab CI will build your site and deploy it with Pages. Once the first build passes, you see your site is live by navigating to your **Project**'s **Settings** > **Pages**, where you'll find its default URL. + +> **Notes:** +> +> - GitLab Pages [supports any SSG](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/), but, if you don't find yours among the templates, you'll need to configure your own `.gitlab-ci.yml`. Do do that, please read through the article [Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md). New SSGs are very welcome among the [example projects](https://gitlab.com/pages). If you set up a new one, please [contribute](https://gitlab.com/pages/pages.gitlab.io/blob/master/CONTRIBUTING.md) to our examples. +> +> - The second step _"Clone it to your local computer"_, can be done differently, achieving the same results: instead of cloning the bare repository to you local computer and moving your site files into it, you can run `git init` in your local website directory, add the remote URL: `git remote add origin git@gitlab.com:namespace/project-name.git`, then add, commit, and push. + +### URLs and Baseurls + + + +Every Static Site Generator (SSG) default configuration expects to find your website under a (sub)domain (`example.com`), not in a subdirectory of that domain (`example.com/subdir`). Therefore, whenever you publish a project website (`namespace.gitlab.io/project-name`), you'll have to look for this configuration (base URL) on your SSG's documentation and set it up to reflect this pattern. + +For example, for a Jekyll site, the `baseurl` is defined in the Jekyll configuration file, `_config.yml`. If your website URL is `https://john.gitlab.io/blog/`, you need to add this line to `_config.yml`: + +```yaml +baseurl: "/blog" +``` + +On the contrary, if you deploy your website after forking one of our [default examples](https://gitlab.com/pages), the baseurl will already be configured this way, as all examples there are project websites. If you decide to make yours a user or group website, you'll have to remove this configuration from your project. For the Jekyll example we've just mentioned, you'd have to change Jekyll's `_config.yml` to: + +```yaml +baseurl: "" +``` + +## Further Reading + +- Read through _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](getting_started_part_one.md)_ +- Read through _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md)_ diff --git a/doc/pages/index.md b/doc/pages/index.md index 54a951f0a2a..fe328748668 100644 --- a/doc/pages/index.md +++ b/doc/pages/index.md @@ -2,39 +2,39 @@ ## Product -- [Product Webpage](https://pages.gitlab.io) -- [We're Bringing GitLab Pages to CE](https://about.gitlab.com/2016/12/24/were-bringing-gitlab-pages-to-community-edition/) -- [Pages Group - Templates](https://gitlab.com/pages) +- [Product webpage](https://pages.gitlab.io) +- [We're bringing GitLab Pages to CE](https://about.gitlab.com/2016/12/24/were-bringing-gitlab-pages-to-community-edition/) +- [Pages group - templates](https://gitlab.com/pages) -## Getting Started +## Getting started -- Comprehensive Step-by-Step Guide: [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) +- [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) a comprehensive step-by-step guide - GitLab Pages from A to Z - - [Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](pages_static_sites_domains_dns_records_ssl_tls_certificates.html) - - [Part 2: Quick Start Guide - Setting Up GitLab Pages](pages_quick_start_guide.html) - - Video tutorial: [How to Publish a Website with GitLab Pages on GitLab.com: from a forked project](https://youtu.be/TWqh9MtT4Bg) - - Video tutorial: [How to Publish a Website with GitLab Pages on GitLab.com: from scratch](#LINK) - - [Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](pages_creating_and_tweaking_gitlab-ci.html) -- Secure GitLab Pages Custom Domain with SSL/TLS Certificates + - [Part 1: Static sites, domains, DNS records, and SSL/TLS certificates](getting_started_part_one.md) + - [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md) + - Video tutorial: [How to publish a website with GitLab Pages on GitLab.com: from a forked project](https://youtu.be/TWqh9MtT4Bg) + - Video tutorial: [How to publish a website with GitLab Pages on GitLab.com: from scratch](#LINK) + - [Part 3: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md) +- Secure GitLab Pages custom domain with SSL/TLS certificates - [Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/) - [CloudFlare](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) - [StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/) -- Static Site Generators - Blog Posts Series - - [SSGs Part 1: Static vs Dynamic Websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) - - [SSGs Part 2: Modern Static Site Generators](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) - - [SSGs Part 3: Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) +- Static Site Generators - Blog posts series + - [SSGs part 1: Static vs dynamic websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) + - [SSGs part 2: Modern static site generators](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) + - [SSGs part 3: Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) - [Posting to your GitLab Pages blog from iOS](https://about.gitlab.com/2016/08/19/posting-to-your-gitlab-pages-blog-from-ios/) -## Advanced Use +## Advanced use - Blog Posts: - - [GitLab CI: Run jobs Sequentially, in Parallel, or Build a Custom Pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/) - - [GitLab CI: Deployment & Environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/) - - [Building a new GitLab Docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/) - - [Publish Code Coverage Report with GitLab Pages](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/) + - [GitLab CI: Run jobs sequentially, in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/) + - [GitLab CI: Deployment & environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/) + - [Building a new GitLab docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/) + - [Publish code coverage reports with GitLab Pages](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/) -## General Documentation +## General documentation -- [User docs](../user/project/pages/) -- [Admin docs](administration.html) +- [User docs](../user/project/pages/index.md) +- [Admin docs](../administration/pages/index.md) - Video tutorial - [How to Enable GitLab Pages for GitLab CE and EE](https://youtu.be/dD8c7WNcc6s) diff --git a/doc/pages/pages_creating_and_tweaking_gitlab-ci.md b/doc/pages/pages_creating_and_tweaking_gitlab-ci.md deleted file mode 100644 index 8e1877c4981..00000000000 --- a/doc/pages/pages_creating_and_tweaking_gitlab-ci.md +++ /dev/null @@ -1,279 +0,0 @@ -# GitLab Pages from A to Z: Part 3 - -> Type: user guide -> -> Level: intermediate - -- **Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages** -- _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](pages_static_sites_domains_dns_records_ssl_tls_certificates.html)_ -- _[Part 2: Quick Start Guide - Setting Up GitLab Pages](pages_quick_start_guide.html)_ - ----- - -### Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages - -[GitLab CI](https://about.gitlab.com/gitlab-ci/) serves inumerous purposes, to build, test, and deploy your app from GitLab through [Continuous Integration, Continuous Delivery, and Continuous Deployment](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) methods. You will need it to build your website with GitLab Pages, and deploy it to the Pages server. - -What this file actually does is telling the [GitLab Runner](https://docs.gitlab.com/runner/) to run scripts as you would do from the command line. The Runner acts as your terminal. GitLab CI tells the Runner which commands to run. Both are built-in in GitLab, and you don't need to set up anything for them to work. - -Explaining [every detail of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html) and GitLab Runner is out of the scope of this guide, but we'll need to understand just a few things to be able to write our own `.gitlab-ci.yml` or tweak an existing one. It's an [Yaml](http://docs.ansible.com/ansible/YAMLSyntax.html) file, with its own syntax. You can always check your CI syntax with the [GitLab CI Lint Tool](https://gitlab.com/ci/lint). - -**Practical Example:** - -Let's consider you have a [Jekyll](https://jekyllrb.com/) site. To build it locally, you would open your terminal, and run `jekyll build`. Of course, before building it, you had to install Jekyll in your computer. For that, you had to open your terminal and run `gem install jekyll`. Right? GitLab CI + GitLab Runner do the same thing. But you need to write in the `.gitlab-ci.yml` the script you want to run so the GitLab Runner will do it for you. It looks more complicated then it is. What you need to tell the Runner: - -``` -$ gem install jekyll -$ jekyll build -``` - -#### Script - -To transpose this script to Yaml, it would be like this: - -```yaml -script: - - gem install jekyll - - jekyll build -``` - -#### Job - -So far so good. Now, each `script`, in GitLab is organized by a `job`, which is a bunch of scripts and settings you want to apply to that specific task. - -```yaml -job: - script: - - gem install jekyll - - jekyll build -``` - -For GitLab Pages, this `job` has a specific name, called `pages`, which tells the Runner you want that task to deploy your website with GitLab Pages: - -```yaml -pages: - script: - - gem install jekyll - - jekyll build -``` - -#### `public` Dir - -We also need to tell Jekyll where do you want the website to build, and GitLab Pages will only consider files in a directory called `public`. To do that with Jekyll, we need to add a flag specifying the [destination (`-d`)](https://jekyllrb.com/docs/usage/) of the built website: `jekyll build -d public`. Of course, we need to tell this to our Runner: - -```yaml -pages: - script: - - gem install jekyll - - jekyll build -d public -``` - -#### Artifacts - -We also need to tell the Runner that this _job_ generates _artifacts_, which is the site built by Jekyll. Where are these artifacts stored? In the `public` directory: - -```yaml -pages: - script: - - gem install jekyll - - jekyll build -d public - artifacts: - paths: - - public -``` - -The script above would be enough to build your Jekyll site with GitLab Pages. But, from Jekyll 3.4.0 on, its default template originated by `jekyll new project` requires [Bundler](http://bundler.io/) to install Jekyll dependencies and the default theme. To adjust our script to meet these new requirements, we only need to install and build Jekyll with Bundler: - -```yaml -pages: - script: - - bundle install - - bundle exec jekyll build -d public - artifacts: - paths: - - public -``` - -That's it! A `.gitlab-ci.yml` with the content above would deploy your Jekyll 3.4.0 site with GitLab Pages. This is the minimum configuration for our example. On the steps below, we'll refine the script by adding extra options to our GitLab CI. - -#### Image - -At this point, you probably ask yourself: "okay, but to install Jekyll I need Ruby. Where is Ruby on that script?". The answer is simple: the first thing GitLab Runner will look for in your `.gitlab-ci.yml` is a [Docker](https://www.docker.com/) image specifying what do you need in your container to run that script: - -```yaml -image: ruby:2.3 - -pages: - script: - - bundle install - - bundle exec jekyll build -d public - artifacts: - paths: - - public -``` - -In this case, you're telling the Runner to pull this image, which contains Ruby 2.3 as part of its file system. When you don't specify this image in your configuration, the Runner will use a default image, which is Ruby 2.1. - -If your SSG needs [NodeJS](https://nodejs.org/) to build, you'll need to specify which image you want to use, and this image should contain NodeJS as part of its file system. E.g., for a [Hexo](https://gitlab.com/pages/hexo) site, you can use `image: node:4.2.2`. - -> Note: we're not trying to explain what a Docker image is, we just need to introduce the concept with a minimum viable explanation. To know more about Docker images, please visit their website or take a look at a [summarized explanation](http://paislee.io/how-to-automate-docker-deployments/) here. - -Let's go a little further. - -#### Branching - -If you use GitLab as a version control platform, you will have your branching strategy to work on your project. Meaning, you will have other branches in your project, but you'll want only pushes to the default branch (usually `master`) to be deployed to your website. To do that, we need to add another line to our CI, telling the Runner to only perform that _job_ called `pages` on the `master` branch `only`: - -```yaml -image: ruby:2.3 - -pages: - script: - - bundle install - - bundle exec jekyll build -d public - artifacts: - paths: - - public - only: - - master -``` - -#### Stages - -Another interesting concept to keep in mind are build stages. Your web app can pass through a lot of tests and other tasks until it's deployed to staging or production environments. There are three default stages on GitLab CI: build, test, and deploy. To specify which stage your _job_ is running, simply add another line to your CI: - -```yaml -image: ruby:2.3 - -pages: - stage: deploy - script: - - bundle install - - bundle exec jekyll build -d public - artifacts: - paths: - - public - only: - - master -``` - -You might ask yourself: "why should I bother with stages at all?" Well, let's say you want to be able to test your script and check the built site before deploying your site to production. You want to run the test exactly as your script will do when you push to `master`. It's simple, let's add another task (_job_) to our CI, telling it to test every push to other branches, `except` the `master` branch: - -```yaml -image: ruby:2.3 - -pages: - stage: deploy - script: - - bundle install - - bundle exec jekyll build -d public - artifacts: - paths: - - public - only: - - master - -test: - stage: test - script: - - bundle install - - bundle exec jekyll build -d test - artifacts: - paths: - - test - except: - - master -``` - -The `test` job is running on the stage `test`, Jekyll will build the site in a directory called `test`, and this job will affect all the branches except `master`. - -The best benefit of applying _stages_ to different _jobs_ is that every job in the same stage builds in parallel. So, if your web app needs more than one test before being deployed, you can run all your test at the same time, it's not necessary to wait on test to finish to run the other. Of course, this is just a brief introduction of GitLab CI and GitLab Runner, which are tools much more powerful than that. This is what you need to be able to create and tweak your builds for your GitLab Pages site. - -#### Before Script - -To avoid running the same script multiple times across your _jobs_, you can add the parameter `before_script`, in which you specify which commands you want to run for every single _job_. In our example, notice that we run `bundle install` for both jobs, `pages` and `test`. We don't need to repeat it: - -```yaml -image: ruby:2.3 - -before_script: - - bundle install - -pages: - stage: deploy - script: - - bundle exec jekyll build -d public - artifacts: - paths: - - public - only: - - master - -test: - stage: test - script: - - bundle exec jekyll build -d test - artifacts: - paths: - - test - except: - - master -``` - -#### Caching Dependencies - -If you want to cache the installation files for your projects dependencies, for building faster, you can use the parameter `cache`. For this example, we'll cache Jekyll dependencies in a `vendor` directory when we run `bundle install`: - -```yaml -image: ruby:2.3 - -cache: - paths: - - vendor/ - -before_script: - - bundle install --path vendor - -pages: - stage: deploy - script: - - bundle exec jekyll build -d public - artifacts: - paths: - - public - only: - - master - -test: - stage: test - script: - - bundle exec jekyll build -d test - artifacts: - paths: - - test - except: - - master -``` - -For this specific case, we need to exclude `/vendor` from Jekyll `_config.yml` file, otherwise Jekyll will understand it as a regular directory to build together with the site: - -```yml -exclude: - - vendor -``` - -There we go! Now our GitLab CI not only builds our website, but also **continuously test** pushes to feature-branches, **caches** dependencies installed with Bundler, and **continuously deploy** every push to the `master` branch. - -### Advanced GitLab CI for GitLab Pages - -What you can do with GitLab CI is pretty much up to your creativity. Once you get used to it, you start creating awesome scripts that automate most of tasks you'd do manually in the past. Read through the [documentation of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html) to understand how to go even further on your scripts. - -- On this blog post, understand the concept of [using GitLab CI `environments` to deploy your web app to staging and production](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/). -- On this post, learn [how to run jobs sequentially, in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/) -- On this blog post, we go through the process of [pulling specific directories from different projects](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/) to deploy this website you're looking at, docs.gitlab.com. -- On this blog post, we teach you [how to use GitLab Pages to produce a code coverage report](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/). - -## Further Reading - -- Read through _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](pages_static_sites_domains_dns_records_ssl_tls_certificates.html)_ -- Read through GitLab Pages from A to Z _[Part 2: Quick Start Guide - Setting Up GitLab Pages](pages_quick_start_guide.html)_ diff --git a/doc/pages/pages_quick_start_guide.md b/doc/pages/pages_quick_start_guide.md deleted file mode 100644 index b0a33136d06..00000000000 --- a/doc/pages/pages_quick_start_guide.md +++ /dev/null @@ -1,112 +0,0 @@ -# GitLab Pages from A to Z: Part 2 - -> Type: user guide -> -> Level: beginner - -- **Part 2: Quick Start Guide - Setting Up GitLab Pages** -- _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](pages_static_sites_domains_dns_records_ssl_tls_certificates.html)_ -- _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](pages_creating_and_tweaking_gitlab-ci.html)_ - ----- - -## Setting Up GitLab Pages - -For a complete step-by-step tutorial, please read the blog post [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/). The following sections will explain what do you need and why do you need them. - - - -### What You Need to Get Started - -1. A project -1. A configuration file (`.gitlab-ci.yml`) to deploy your site -1. A specific `job` called `pages` in the configuration file that will make GitLab aware that you are deploying a GitLab Pages website - -#### Optional Features - -1. A custom domain or subdomain -1. A DNS pointing your (sub)domain to your Pages site - 1. **Optional**: an SSL/TLS certificate so your custom domain is accessible under HTTPS. - -### Project - -Your GitLab Pages project is a regular project created the same way you do for the other ones. To get started with GitLab Pages, you have two ways: - -- Fork one of the templates from Page Examples, or -- Create a new project from scratch - -Let's go over both options. - -#### Fork a Project to Get Started From - -To make things easy for you, we've created this [group](https://gitlab.com/pages) of default projects containing the most popular SSGs templates. - -Watch the [video tutorial](https://youtu.be/TWqh9MtT4Bg) we've created for the steps below. - -1. Choose your SSG template -1. Fork a project from the [Pages group](https://gitlab.com/pages) -1. Remove the fork relationship by navigating to your **Project**'s **Settings** > **Edit Project** - - ![remove fork relashionship](img/remove_fork_relashionship.png) - -1. Enable Shared Runners for your fork: navigate to your **Project**'s **Settings** > **CI/CD Pipelines** -1. Trigger a build (push a change to any file) -1. As soon as the build passes, your website will have been deployed with GitLab Pages. Your website URL will be available under your **Project**'s **Settings** > **Pages** - -To turn a **project website** forked from the Pages group into a **user/group** website, you'll need to: - -- Rename it to `namespace.gitlab.io`: navigate to **Project**'s **Settings** > **Edit Project** > **Rename repository** -- Adjust your SSG's [base URL](#urls-and-baseurls) to from `"project-name"` to `""`. This setting will be at a different place for each SSG, as each of them have their own structure and file tree. Most likelly, it will be in the SSG's config file. - -> **Notes:** -> ->1. Why do I need to remove the fork relationship? -> -> Unless you want to contribute to the original project, you won't need it connected to the upstream. A [fork](https://about.gitlab.com/2016/12/01/how-to-keep-your-fork-up-to-date-with-its-origin/#fork) is useful for submitting merge requests to the upstream. -> -> 2. Why do I need to enable Shared Runners? -> -> Shared Runners will run the script set by your GitLab CI configuration file. They're enabled by default to new projects, but not to forks. - -#### Create a Project from Scratch - -1. From your **Project**'s **[Dashboard](https://gitlab.com/dashboard/projects)**, click **New project**, and name it considering the [examples above](#practical-examples). -1. Clone it to your local computer, add your website files to your project, add, commit and push to GitLab. -1. From the your **Project**'s page, click **Set up CI**: - - ![setup GitLab CI](img/setup_ci.png) - -1. Choose one of the templates from the dropbox menu. Pick up the template corresponding to the SSG you're using (or plain HTML). - - ![gitlab-ci templates](img/choose_ci_template.png) - -Once you have both site files and `.gitlab-ci.yml` in your project's root, GitLab CI will build your site and deploy it with Pages. Once the first build passes, you see your site is live by navigating to your **Project**'s **Settings** > **Pages**, where you'll find its default URL. - -> **Notes:** -> -> - GitLab Pages [supports any SSG](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/), but, if you don't find yours among the templates, you'll need to configure your own `.gitlab-ci.yml`. Do do that, please read through the article [Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](pages_creating_and_tweaking_gitlab-ci_.html). New SSGs are very welcome among the [example projects](https://gitlab.com/pages). If you set up a new one, please [contribute](https://gitlab.com/pages/pages.gitlab.io/blob/master/CONTRIBUTING.md) to our examples. -> -> - The second step _"Clone it to your local computer"_, can be done differently, achieving the same results: instead of cloning the bare repository to you local computer and moving your site files into it, you can run `git init` in your local website directory, add the remote URL: `git remote add origin git@gitlab.com:namespace/project-name.git`, then add, commit, and push. - -### URLs and Baseurls - - - -Every Static Site Generator (SSG) default configuration expects to find your website under a (sub)domain (`example.com`), not in a subdirectory of that domain (`example.com/subdir`). Therefore, whenever you publish a project website (`namespace.gitlab.io/project-name`), you'll have to look for this configuration (base URL) on your SSG's documentation and set it up to reflect this pattern. - -For example, for a Jekyll site, the `baseurl` is defined in the Jekyll configuration file, `_config.yml`. If your website URL is `https://john.gitlab.io/blog/`, you need to add this line to `_config.yml`: - -```yaml -baseurl: "/blog" -``` - -On the contrary, if you deploy your website after forking one of our [default examples](https://gitlab.com/pages), the baseurl will already be configured this way, as all examples there are project websites. If you decide to make yours a user or group website, you'll have to remove this configuration from your project. For the Jekyll example we've just mentioned, you'd have to change Jekyll's `_config.yml` to: - -```yaml -baseurl: "" -``` - -## Further Reading - -- Read through _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](pages_static_sites_domains_dns_records_ssl_tls_certificates.html)_ -- Read through _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](pages_creating_and_tweaking_gitlab-ci.html)_ diff --git a/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md b/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md deleted file mode 100644 index 2564d87ac3a..00000000000 --- a/doc/pages/pages_static_sites_domains_dns_records_ssl_tls_certificates.md +++ /dev/null @@ -1,164 +0,0 @@ -# GitLab Pages from A to Z: Part 1 - -> Type: user guide -> -> Level: beginner - -- **Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates** -- _[Part 2: Quick Start Guide - Setting Up GitLab Pages](pages_quick_start_guide.html)_ -- _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](pages_creating_and_tweaking_gitlab-ci.html)_ - ----- - -This is a comprehensive guide, made for those who want to publish a website with GitLab Pages but aren't familiar with the entire process involved. - -To **enable** GitLab Pages for GitLab CE (Community Edition) and GitLab EE (Enterprise Edition), please read the [admin documentation](https://docs.gitlab.com/ce/administration/pages/index.html), and/or watch this [video tutorial](https://youtu.be/dD8c7WNcc6s). - -> For this guide, we assume you already have GitLab Pages server up and running for your GitLab instance. - -## What you need to know before getting started - -Before we begin, let's understand a few concepts first. - -### Static Sites - -GitLab Pages only supports static websites, meaning, your output files must be HTML, CSS, and JavaScript only. - -To create your static site, you can either hardcode in HTML, CSS, and JS, or use a [Static Site Generator (SSG)](https://www.staticgen.com/) to simplify your code and build the static site for you, which is highly recommendable and much faster than hardcoding. - -#### Further Reading - -- Read through this technical overview on [Static versus Dynamic Websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) -- Understand [how modern Static Site Generators work](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) and what you can add to your static site -- You can use [any SSG with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) -- Fork an [example project](https://gitlab.com/pages) to build your website based upon - -### GitLab Pages Domain - -If you set up a GitLab Pages project on GitLab.com, it will automatically be accessible under a [subdomain of `.pages.io`](https://docs.gitlab.com/ce/user/project/pages/). The `` is defined by your username on GitLab.com, or the group name you created this project under. - -> Note: If you use your own GitLab instance to deploy your site with GitLab Pages, check with your sysadmin what's your Pages wildcard domain. This guide is valid for any GitLab instance, you just need to replace Pages wildcard domain on GitLab.com (`*.gitlab.io`) with your own. - -#### Practical examples - -**Project Websites:** - -- You created a project called `blog` under your username `john`, therefore your project URL is `https://gitlab.com/john/blog/`. Once you enable GitLab Pages for this project, and build your site, it will be available under `https://john.gitlab.io/blog/`. -- You created a group for all your websites called `websites`, and a project within this group is called `blog`. Your project URL is `https://gitlab.com/websites/blog/`. Once you enable GitLab Pages for this project, the site will live under `https://websites.gitlab.io/blog/`. - -**User and Group Websites:** - -- Under your username, `john`, you created a project called `john.gitlab.io`. Your project URL will be `https://gitlab.com/john/john.gitlab.io`. Once you enable GitLab Pages for your project, your website will be published under `https://john.gitlab.io`. -- Under your group `websites`, you created a project called `websites.gitlab.io`. your project's URL will be `https://gitlab.com/websites/websites.gitlab.io`. Once you enable GitLab Pages for your project, your website will be published under `https://websites.gitlab.io`. - -**General example:** - -- On GitLab.com, a project site will always be available under `https://namespace.gitlab.io/project-name` -- On GitLab.com, a user or group website will be available under `https://namespace.gitlab.io/` -- On your GitLab instance, replace `gitlab.io` above with your Pages server domain. Ask your sysadmin for this information. - -### DNS Records - -A Domain Name System (DNS) web service routes visitors to websites by translating domain names (such as `www.example.com`) into the numeric IP addresses (such as `192.0.2.1`) that computers use to connect to each other. - -A DNS record is created to point a (sub)domain to a certain location, which can be an IP address or another domain. In case you want to use GitLab Pages with your own (sub)domain, you need to access your domain's registrar control panel to add a DNS record pointing it back to your GitLab Pages site. - -Note that **how to** add DNS records depends on which server your domain is hosted on. Every control panel has its own place to do it. If you are not an admin of your domain, and don't have access to your registrar, you'll need to ask for the technical support of your hosting service to do it for you. - -To help you out, we've gathered some instructions on how to do that for the most popular hosting services: - -- [Amazon](http://docs.aws.amazon.com/gettingstarted/latest/swh/getting-started-configure-route53.html) -- [Bluehost](https://my.bluehost.com/cgi/help/559) -- [CloudFlare](https://support.cloudflare.com/hc/en-us/articles/200169096-How-do-I-add-A-records-) -- [cPanel](https://documentation.cpanel.net/display/ALD/Edit+DNS+Zone) -- [DreamHost](https://help.dreamhost.com/hc/en-us/articles/215414867-How-do-I-add-custom-DNS-records-) -- [Go Daddy](https://www.godaddy.com/help/add-an-a-record-19238) -- [Hostgator](http://support.hostgator.com/articles/changing-dns-records) -- [Inmotion hosting](https://my.bluehost.com/cgi/help/559) -- [Media Temple](https://mediatemple.net/community/products/dv/204403794/how-can-i-change-the-dns-records-for-my-domain) -- [Microsoft](https://msdn.microsoft.com/en-us/library/bb727018.aspx) - -If your hosting service is not listed above, you can just try to search the web for "how to add dns record on ". - -#### DNS A record - -In case you want to point a root domain (`example.com`) to your GitLab Pages site, deployed to `namespace.gitlab.io`, you need to log into your domain's admin control panel and add a DNS `A` record pointing your domain to Pages' server IP address. For projects on GitLab.com, this IP is `104.208.235.32`. For projects leaving in other GitLab instances (CE or EE), please contact your sysadmin asking for this information (which IP address is Pages server running on your instance). - -**Practical Example:** - -![DNS A record pointing to GitLab.com Pages server](img/dns_a_record_example.png) - -#### DNS CNAME record - -In case you want to point a subdomain (`hello-world.example.com`) to your GitLab Pages site initially deployed to `namespace.gitlab.io`, you need to log into your domain's admin control panel and add a DNS `CNAME` record pointing your subdomain to your website URL (`namespace.gitlab.io`) address. - -Notice that, despite it's a user or project website, the `CNAME` should point to your Pages domain (`namespace.gitlab.io`), without any `/project-name`. - -**Practical Example:** - -![DNS CNAME record pointing to GitLab.com project](img/dns_cname_record_example.png) - -#### TL;DR - -| From | DNS Record | To | -| ---- | ---------- | -- | -| domain.com | A | 104.208.235.32 | -| subdomain.domain.com | CNAME | namespace.gitlab.io | - -> **Notes**: -> -> - **Do not** use a CNAME record if you want to point your `domain.com` to your GitLab Pages site. Use an `A` record instead. -> - **Do not** add any special chars after the default Pages domain. E.g., **do not** point your `subdomain.domain.com` to `namespace.gitlab.io.` or `namespace.gitlab.io/`. - -### SSL/TLS Certificates - -Every GitLab Pages project on GitLab.com will be available under HTTPS for the default Pages domain (`*.gitlab.io`). Once you set up your Pages project with your custom (sub)domain, if you want it secured by HTTPS, you will have to issue a certificate for that (sub)domain and install it on your project. - -> Note: certificates are NOT required to add to your custom (sub)domain on your GitLab Pages project, though they are highly recommendable. - -The importance of having any website securely served under HTTPS is explained on the introductory section of the blog post [Secure GitLab Pages with StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/#https-a-quick-overview). - -The reason why certificates are so important is that they encrypt the connection between the **client** (you, me, your visitors) and the **server** (where you site lives), through a keychain of authentications and validations. - -### Issuing Certificates - -GitLab Pages accepts [PEM](https://support.quovadisglobal.com/kb/a37/what-is-pem-format.aspx) certificates issued by [Certificate Authorities (CA)](https://en.wikipedia.org/wiki/Certificate_authority) and self-signed certificates. Of course, [you'd rather issue a certificate than generate a self-signed](https://en.wikipedia.org/wiki/Self-signed_certificate), for security reasons and for having browsers trusting your site's certificate. - -There are several different kinds of certificates, each one with certain security level. A static personal website will not require the same security level as an online banking web app, for instance. There are a couple Certificate Authorities that offer free certificates, aiming to make the internet more secure to everyone. The most popular is [Let's Encrypt](https://letsencrypt.org/), which issues certificates trusted by most of browsers, it's open source, and free to use. Please read through this tutorial to understand [how to secure your GitLab Pages website with Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/). - -With the same popularity, there are [certificates issued by CloudFlare](https://www.cloudflare.com/ssl/), which also offers a [free CDN service](https://blog.cloudflare.com/cloudflares-free-cdn-and-you/). Their certs are valid up to 15 years. Read through the tutorial on [how to add a CloudFlare Certificate to your GitLab Pages website](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/). - -### Adding certificates to your project - -Regardless the CA you choose, the steps to add your certificate to your Pages project are the same. - -#### What do you need - -1. A PEM certificate -1. An intermediary certificate -1. A public key - -![Pages project - adding certificates](img/add_certificate_to_pages.png) - -These fields are found under your **Project**'s **Settings** > **Pages** > **New Domain**. - -#### What's what? - -- A PEM certificate is the certificate generated by the CA, which needs to be added to the field **Certificate (PEM)**. -- An [intermediary certificate][] (aka "root certificate") is the part of the encryption keychain that identifies the CA. Usually it's combined with the PEM certificate, but there are some cases in which you need to add them manually. [CloudFlare certs](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) are one of these cases. -- A public key is an encrypted key which validates your PEM against your domain. - -#### Now what? - -Now that you hopefully understand why you need all of this, it's simple: - -- Your PEM certificate needs to be added to the first field -- If your certificate is missing its intermediary, copy and paste the root certificate (usually available from your CA website) and paste it in the [same field as your PEM certificate](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/), just jumping a line between them. -- Copy your public key and paste it in the last field - -> Note: **do not** open certificates or encryption keys in regular text editors. Always use code editors (such as Sublime Text, Atom, Dreamweaver, Brackets, etc). - -## Further Reading - -- Read through GitLab Pages from A to Z _[Part 2: Quick Start Guide - Setting Up GitLab Pages](pages_quick_start_guide.html)_ -- Read through GitLab Pages from A to Z _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](pages_creating_and_tweaking_gitlab-ci.html)_ diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md index e07b7e83f81..816600964de 100644 --- a/doc/user/project/pages/index.md +++ b/doc/user/project/pages/index.md @@ -445,5 +445,5 @@ For a list of known issues, visit GitLab's [public issue tracker]. [ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605 [quick start guide]: ../../../ci/quick_start/README.md [pages-index-guide]: ../../../pages/ -[pages-quick]: ../../../pages/pages_quick_start_guide.html +[pages-quick]: ../../../pages/getting_started_part_one.md [video-pages-fork]: https://youtu.be/TWqh9MtT4Bg -- cgit v1.2.3 From 0b402e11e355dc8d834fbc139f4bca810a9f766e Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Mon, 20 Feb 2017 15:35:05 +0100 Subject: Remove deprecated `upvotes` and `downvotes` from the notes API --- changelogs/unreleased/api-notes-entity-fields.yml | 4 + doc/api/notes.md | 18 +- doc/api/projects.md | 2 - doc/api/users.md | 2 - doc/api/v3_to_v4.md | 2 +- lib/api/api.rb | 1 + lib/api/entities.rb | 3 - lib/api/v3/entities.rb | 34 ++ lib/api/v3/notes.rb | 148 ++++++++ lib/api/v3/projects.rb | 4 +- lib/api/v3/users.rb | 21 ++ spec/requests/api/v3/notes_spec.rb | 432 ++++++++++++++++++++++ spec/requests/api/v3/users_spec.rb | 77 ++++ 13 files changed, 723 insertions(+), 25 deletions(-) create mode 100644 changelogs/unreleased/api-notes-entity-fields.yml create mode 100644 lib/api/v3/notes.rb create mode 100644 spec/requests/api/v3/notes_spec.rb diff --git a/changelogs/unreleased/api-notes-entity-fields.yml b/changelogs/unreleased/api-notes-entity-fields.yml new file mode 100644 index 00000000000..f7631df31e2 --- /dev/null +++ b/changelogs/unreleased/api-notes-entity-fields.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Remove deprecated fields Notes#upvotes and Notes#downvotes' +merge_request: 9384 +author: Robert Schilling diff --git a/doc/api/notes.md b/doc/api/notes.md index 214dfa4068d..dced821cc6d 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -34,8 +34,6 @@ Parameters: "created_at": "2013-10-02T09:22:45Z", "updated_at": "2013-10-02T10:22:45Z", "system": true, - "upvote": false, - "downvote": false, "noteable_id": 377, "noteable_type": "Issue" }, @@ -54,8 +52,6 @@ Parameters: "created_at": "2013-10-02T09:56:03Z", "updated_at": "2013-10-02T09:56:03Z", "system": true, - "upvote": false, - "downvote": false, "noteable_id": 121, "noteable_type": "Issue" } @@ -147,9 +143,7 @@ Example Response: "created_at": "2016-04-05T22:10:44.164Z", "system": false, "noteable_id": 11, - "noteable_type": "Issue", - "upvote": false, - "downvote": false + "noteable_type": "Issue" } ``` @@ -271,9 +265,7 @@ Example Response: "created_at": "2016-04-06T16:51:53.239Z", "system": false, "noteable_id": 52, - "noteable_type": "Snippet", - "upvote": false, - "downvote": false + "noteable_type": "Snippet" } ``` @@ -322,8 +314,6 @@ Parameters: "created_at": "2013-10-02T08:57:14Z", "updated_at": "2013-10-02T08:57:14Z", "system": false, - "upvote": false, - "downvote": false, "noteable_id": 2, "noteable_type": "MergeRequest" } @@ -400,8 +390,6 @@ Example Response: "created_at": "2016-04-05T22:11:59.923Z", "system": false, "noteable_id": 7, - "noteable_type": "MergeRequest", - "upvote": false, - "downvote": false + "noteable_type": "MergeRequest" } ``` diff --git a/doc/api/projects.md b/doc/api/projects.md index e9ef03a0c0c..7fe36d27804 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -407,8 +407,6 @@ Parameters: }, "created_at": "2015-12-04T10:33:56.698Z", "system": false, - "upvote": false, - "downvote": false, "noteable_id": 377, "noteable_type": "Issue" }, diff --git a/doc/api/users.md b/doc/api/users.md index 852c7ac8ec2..d14548e8bbb 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -812,8 +812,6 @@ Example response: }, "created_at": "2015-12-04T10:33:56.698Z", "system": false, - "upvote": false, - "downvote": false, "noteable_id": 377, "noteable_type": "Issue" }, diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 59d7f0634b2..ff8d227a794 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -36,4 +36,4 @@ changes are in V4: - POST `:id/repository/commits` - POST/PUT/DELETE `:id/repository/files` - Renamed `branch_name` to `branch` on DELETE `id/repository/branches/:branch` response - +- Notes do not return deprecated field `upvote` and `downvote` [!9384](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9384) diff --git a/lib/api/api.rb b/lib/api/api.rb index a0282ff8deb..1803387bb8c 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -15,6 +15,7 @@ module API mount ::API::V3::Members mount ::API::V3::MergeRequestDiffs mount ::API::V3::MergeRequests + mount ::API::V3::Notes mount ::API::V3::ProjectHooks mount ::API::V3::Projects mount ::API::V3::ProjectSnippets diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 400ee7c92aa..85aa6932f81 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -339,9 +339,6 @@ module API expose :created_at, :updated_at expose :system?, as: :system expose :noteable_id, :noteable_type - # upvote? and downvote? are deprecated, always return false - expose(:upvote?) { |note| false } - expose(:downvote?) { |note| false } end class AwardEmoji < Grape::Entity diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb index 3cc0dc968a8..11d0e6dbf71 100644 --- a/lib/api/v3/entities.rb +++ b/lib/api/v3/entities.rb @@ -11,6 +11,40 @@ module API Gitlab::UrlBuilder.build(snippet) end end + + class Note < Grape::Entity + expose :id + expose :note, as: :body + expose :attachment_identifier, as: :attachment + expose :author, using: ::API::Entities::UserBasic + expose :created_at, :updated_at + expose :system?, as: :system + expose :noteable_id, :noteable_type + # upvote? and downvote? are deprecated, always return false + expose(:upvote?) { |note| false } + expose(:downvote?) { |note| false } + end + + class Event < Grape::Entity + expose :title, :project_id, :action_name + expose :target_id, :target_type, :author_id + expose :data, :target_title + expose :created_at + expose :note, using: Entities::Note, if: ->(event, options) { event.note? } + expose :author, using: ::API::Entities::UserBasic, if: ->(event, options) { event.author } + + expose :author_username do |event, options| + event.author&.username + end + end + + class AwardEmoji < Grape::Entity + expose :id + expose :name + expose :user, using: ::API::Entities::UserBasic + expose :created_at, :updated_at + expose :awardable_id, :awardable_type + end end end end diff --git a/lib/api/v3/notes.rb b/lib/api/v3/notes.rb new file mode 100644 index 00000000000..6531598d590 --- /dev/null +++ b/lib/api/v3/notes.rb @@ -0,0 +1,148 @@ +module API + module V3 + class Notes < Grape::API + include PaginationParams + + before { authenticate! } + + NOTEABLE_TYPES = [Issue, MergeRequest, Snippet] + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do + NOTEABLE_TYPES.each do |noteable_type| + noteables_str = noteable_type.to_s.underscore.pluralize + + desc 'Get a list of project +noteable+ notes' do + success ::API::V3::Entities::Note + 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]) + + if can?(current_user, noteable_read_ability_name(noteable), noteable) + # We exclude notes that are cross-references and that cannot be viewed + # by the current user. By doing this exclusion at this level and not + # at the DB query level (which we cannot in that case), the current + # page can have less elements than :per_page even if + # there's more than one page. + notes = + # paginate() only works with a relation. This could lead to a + # mismatch between the pagination headers info and the actual notes + # array returned, but this is really a edge-case. + paginate(noteable.notes). + reject { |n| n.cross_reference_not_visible_for?(current_user) } + present notes, with: ::API::V3::Entities::Note + else + not_found!("Notes") + end + end + + desc 'Get a single +noteable+ note' do + success ::API::V3::Entities::Note + end + params do + requires :note_id, type: Integer, desc: 'The ID of a note' + requires :noteable_id, type: Integer, desc: 'The ID of the noteable' + end + get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do + noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id]) + note = noteable.notes.find(params[:note_id]) + can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user) + + if can_read_note + present note, with: ::API::V3::Entities::Note + else + not_found!("Note") + end + end + + desc 'Create a new +noteable+ note' do + success ::API::V3::Entities::Note + end + params do + requires :noteable_id, type: Integer, desc: 'The ID of the noteable' + requires :body, type: String, desc: 'The content of a note' + optional :created_at, type: String, desc: 'The creation date of the note' + end + post ":id/#{noteables_str}/:noteable_id/notes" do + opts = { + note: params[:body], + noteable_type: noteables_str.classify, + noteable_id: params[:noteable_id] + } + + noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id]) + + if can?(current_user, noteable_read_ability_name(noteable), noteable) + if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user) + opts[:created_at] = params[:created_at] + end + + note = ::Notes::CreateService.new(user_project, current_user, opts).execute + if note.valid? + present note, with: ::API::V3::Entities::const_get(note.class.name) + else + not_found!("Note #{note.errors.messages}") + end + else + not_found!("Note") + end + end + + desc 'Update an existing +noteable+ note' do + success ::API::V3::Entities::Note + end + params do + requires :noteable_id, type: Integer, desc: 'The ID of the noteable' + requires :note_id, type: Integer, desc: 'The ID of a note' + requires :body, type: String, desc: 'The content of a note' + end + put ":id/#{noteables_str}/:noteable_id/notes/:note_id" do + note = user_project.notes.find(params[:note_id]) + + authorize! :admin_note, note + + opts = { + note: params[:body] + } + + note = ::Notes::UpdateService.new(user_project, current_user, opts).execute(note) + + if note.valid? + present note, with: ::API::V3::Entities::Note + else + render_api_error!("Failed to save note #{note.errors.messages}", 400) + end + end + + desc 'Delete a +noteable+ note' do + success ::API::V3::Entities::Note + end + params do + requires :noteable_id, type: Integer, desc: 'The ID of the noteable' + requires :note_id, type: Integer, desc: 'The ID of a note' + end + delete ":id/#{noteables_str}/:noteable_id/notes/:note_id" do + note = user_project.notes.find(params[:note_id]) + authorize! :admin_note, note + + ::Notes::DestroyService.new(user_project, current_user).execute(note) + + present note, with: ::API::V3::Entities::Note + end + end + end + + helpers do + def noteable_read_ability_name(noteable) + "read_#{noteable.class.to_s.underscore}".to_sym + end + end + end + end +end diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb index 6796da83f07..4bc836f2a3a 100644 --- a/lib/api/v3/projects.rb +++ b/lib/api/v3/projects.rb @@ -232,13 +232,13 @@ module API end desc 'Get events for a single project' do - success ::API::Entities::Event + success ::API::V3::Entities::Event end params do use :pagination end get ":id/events" do - present paginate(user_project.events.recent), with: ::API::Entities::Event + present paginate(user_project.events.recent), with: ::API::V3::Entities::Event end desc 'Fork new project for the current user or provided namespace.' do diff --git a/lib/api/v3/users.rb b/lib/api/v3/users.rb index e05e457a5df..7838cdc46a7 100644 --- a/lib/api/v3/users.rb +++ b/lib/api/v3/users.rb @@ -71,6 +71,27 @@ module API user.activate end end + + desc 'Get the contribution events of a specified user' do + detail 'This feature was introduced in GitLab 8.13.' + success ::API::V3::Entities::Event + 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]) + not_found!('User') unless user + + events = user.events. + merge(ProjectsFinder.new.execute(current_user)). + references(:project). + with_associations. + recent + + present paginate(events), with: ::API::V3::Entities::Event + end end resource :user do diff --git a/spec/requests/api/v3/notes_spec.rb b/spec/requests/api/v3/notes_spec.rb new file mode 100644 index 00000000000..b51cb3055d5 --- /dev/null +++ b/spec/requests/api/v3/notes_spec.rb @@ -0,0 +1,432 @@ +require 'spec_helper' + +describe API::V3::Notes, api: true do + include ApiHelpers + let(:user) { create(:user) } + let!(:project) { create(:empty_project, :public, namespace: user.namespace) } + let!(:issue) { create(:issue, project: project, author: user) } + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) } + let!(:snippet) { create(:project_snippet, project: project, author: user) } + let!(:issue_note) { create(:note, noteable: issue, project: project, author: user) } + let!(:merge_request_note) { create(:note, noteable: merge_request, project: project, author: user) } + let!(:snippet_note) { create(:note, noteable: snippet, project: project, author: user) } + + # For testing the cross-reference of a private issue in a public issue + let(:private_user) { create(:user) } + let(:private_project) do + create(:empty_project, namespace: private_user.namespace). + tap { |p| p.team << [private_user, :master] } + end + let(:private_issue) { create(:issue, project: private_project) } + + let(:ext_proj) { create(:empty_project, :public) } + let(:ext_issue) { create(:issue, project: ext_proj) } + + let!(:cross_reference_note) do + create :note, + noteable: ext_issue, project: ext_proj, + note: "mentioned in issue #{private_issue.to_reference(ext_proj)}", + system: true + end + + before { project.team << [user, :reporter] } + + describe "GET /projects/:id/noteable/:noteable_id/notes" do + context "when noteable is an Issue" do + it "returns an array of issue notes" do + get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['body']).to eq(issue_note.note) + expect(json_response.first['upvote']).to be_falsey + expect(json_response.first['downvote']).to be_falsey + end + + it "returns a 404 error when issue id not found" do + get v3_api("/projects/#{project.id}/issues/12345/notes", user) + + expect(response).to have_http_status(404) + end + + context "and current user cannot view the notes" do + it "returns an empty array" do + get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response).to be_empty + end + + context "and issue is confidential" do + before { ext_issue.update_attributes(confidential: true) } + + it "returns 404" do + get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) + + expect(response).to have_http_status(404) + end + end + + context "and current user can view the note" do + it "returns an empty array" do + get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['body']).to eq(cross_reference_note.note) + end + end + end + end + + context "when noteable is a Snippet" do + it "returns an array of snippet notes" do + get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['body']).to eq(snippet_note.note) + end + + it "returns a 404 error when snippet id not found" do + get v3_api("/projects/#{project.id}/snippets/42/notes", user) + + expect(response).to have_http_status(404) + end + + it "returns 404 when not authorized" do + get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", private_user) + + expect(response).to have_http_status(404) + end + end + + context "when noteable is a Merge Request" do + it "returns an array of merge_requests notes" do + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['body']).to eq(merge_request_note.note) + end + + it "returns a 404 error if merge request id not found" do + get v3_api("/projects/#{project.id}/merge_requests/4444/notes", user) + + expect(response).to have_http_status(404) + end + + it "returns 404 when not authorized" do + get v3_api("/projects/#{project.id}/merge_requests/4444/notes", private_user) + + expect(response).to have_http_status(404) + end + end + end + + describe "GET /projects/:id/noteable/:noteable_id/notes/:note_id" do + context "when noteable is an Issue" do + it "returns an issue note by id" do + get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['body']).to eq(issue_note.note) + end + + it "returns a 404 error if issue note not found" do + get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user) + + expect(response).to have_http_status(404) + end + + context "and current user cannot view the note" do + it "returns a 404 error" do + get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user) + + expect(response).to have_http_status(404) + end + + context "when issue is confidential" do + before { issue.update_attributes(confidential: true) } + + it "returns 404" do + get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", private_user) + + expect(response).to have_http_status(404) + end + end + + context "and current user can view the note" do + it "returns an issue note by id" do + get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", private_user) + + expect(response).to have_http_status(200) + expect(json_response['body']).to eq(cross_reference_note.note) + end + end + end + end + + context "when noteable is a Snippet" do + it "returns a snippet note by id" do + get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes/#{snippet_note.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['body']).to eq(snippet_note.note) + end + + it "returns a 404 error if snippet note not found" do + get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes/12345", user) + + expect(response).to have_http_status(404) + end + end + end + + describe "POST /projects/:id/noteable/:noteable_id/notes" do + context "when noteable is an Issue" do + it "creates a new issue note" do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!' + + expect(response).to have_http_status(201) + expect(json_response['body']).to eq('hi!') + expect(json_response['author']['username']).to eq(user.username) + end + + it "returns a 400 bad request error if body not given" do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user) + + expect(response).to have_http_status(400) + end + + it "returns a 401 unauthorized error if user not authenticated" do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!' + + expect(response).to have_http_status(401) + end + + context 'when an admin or owner makes the request' do + it 'accepts the creation date to be set' do + creation_time = 2.weeks.ago + post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), + body: 'hi!', created_at: creation_time + + expect(response).to have_http_status(201) + expect(json_response['body']).to eq('hi!') + expect(json_response['author']['username']).to eq(user.username) + expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time) + end + end + + context 'when the user is posting an award emoji on an issue created by someone else' do + let(:issue2) { create(:issue, project: project) } + + it 'returns an award emoji' do + post v3_api("/projects/#{project.id}/issues/#{issue2.id}/notes", user), body: ':+1:' + + expect(response).to have_http_status(201) + expect(json_response['awardable_id']).to eq issue2.id + end + end + + context 'when the user is posting an award emoji on his/her own issue' do + it 'creates a new issue note' do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: ':+1:' + + expect(response).to have_http_status(201) + expect(json_response['body']).to eq(':+1:') + end + end + end + + context "when noteable is a Snippet" do + it "creates a new snippet note" do + post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user), body: 'hi!' + + expect(response).to have_http_status(201) + expect(json_response['body']).to eq('hi!') + expect(json_response['author']['username']).to eq(user.username) + end + + it "returns a 400 bad request error if body not given" do + post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user) + + expect(response).to have_http_status(400) + end + + it "returns a 401 unauthorized error if user not authenticated" do + post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes"), body: 'hi!' + + expect(response).to have_http_status(401) + end + end + + context 'when user does not have access to read the noteable' do + it 'responds with 404' do + project = create(:empty_project, :private) { |p| p.add_guest(user) } + issue = create(:issue, :confidential, project: project) + + post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), + body: 'Foo' + + expect(response).to have_http_status(404) + end + end + + context 'when user does not have access to create noteable' do + let(:private_issue) { create(:issue, project: create(:empty_project, :private)) } + + ## + # We are posting to project user has access to, but we use issue id + # from a different project, see #15577 + # + before do + post v3_api("/projects/#{project.id}/issues/#{private_issue.id}/notes", user), + body: 'Hi!' + end + + it 'responds with resource not found error' do + expect(response.status).to eq 404 + end + + it 'does not create new note' do + expect(private_issue.notes.reload).to be_empty + end + end + end + + describe "POST /projects/:id/noteable/:noteable_id/notes to test observer on create" do + it "creates an activity event when an issue note is created" do + expect(Event).to receive(:create) + + post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!' + end + end + + describe 'PUT /projects/:id/noteable/:noteable_id/notes/:note_id' do + context 'when noteable is an Issue' do + it 'returns modified note' do + put v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ + "notes/#{issue_note.id}", user), body: 'Hello!' + + expect(response).to have_http_status(200) + expect(json_response['body']).to eq('Hello!') + end + + it 'returns a 404 error when note id not found' do + put v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user), + body: 'Hello!' + + expect(response).to have_http_status(404) + end + + it 'returns a 400 bad request error if body not given' do + put v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ + "notes/#{issue_note.id}", user) + + expect(response).to have_http_status(400) + end + end + + context 'when noteable is a Snippet' do + it 'returns modified note' do + put v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ + "notes/#{snippet_note.id}", user), body: 'Hello!' + + expect(response).to have_http_status(200) + expect(json_response['body']).to eq('Hello!') + end + + it 'returns a 404 error when note id not found' do + put v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ + "notes/12345", user), body: "Hello!" + + expect(response).to have_http_status(404) + end + end + + context 'when noteable is a Merge Request' do + it 'returns modified note' do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ + "notes/#{merge_request_note.id}", user), body: 'Hello!' + + expect(response).to have_http_status(200) + expect(json_response['body']).to eq('Hello!') + end + + it 'returns a 404 error when note id not found' do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ + "notes/12345", user), body: "Hello!" + + expect(response).to have_http_status(404) + end + end + end + + describe 'DELETE /projects/:id/noteable/:noteable_id/notes/:note_id' do + context 'when noteable is an Issue' do + it 'deletes a note' do + delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ + "notes/#{issue_note.id}", user) + + expect(response).to have_http_status(200) + # Check if note is really deleted + delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ + "notes/#{issue_note.id}", user) + expect(response).to have_http_status(404) + end + + it 'returns a 404 error when note id not found' do + delete v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user) + + expect(response).to have_http_status(404) + end + end + + context 'when noteable is a Snippet' do + it 'deletes a note' do + delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ + "notes/#{snippet_note.id}", user) + + expect(response).to have_http_status(200) + # Check if note is really deleted + delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ + "notes/#{snippet_note.id}", user) + expect(response).to have_http_status(404) + end + + it 'returns a 404 error when note id not found' do + delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ + "notes/12345", user) + + expect(response).to have_http_status(404) + end + end + + context 'when noteable is a Merge Request' do + it 'deletes a note' do + delete v3_api("/projects/#{project.id}/merge_requests/"\ + "#{merge_request.id}/notes/#{merge_request_note.id}", user) + + expect(response).to have_http_status(200) + # Check if note is really deleted + delete v3_api("/projects/#{project.id}/merge_requests/"\ + "#{merge_request.id}/notes/#{merge_request_note.id}", user) + expect(response).to have_http_status(404) + end + + it 'returns a 404 error when note id not found' do + delete v3_api("/projects/#{project.id}/merge_requests/"\ + "#{merge_request.id}/notes/12345", user) + + expect(response).to have_http_status(404) + end + end + end +end diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb index 5020ef18a3a..17bbb0b53c1 100644 --- a/spec/requests/api/v3/users_spec.rb +++ b/spec/requests/api/v3/users_spec.rb @@ -186,4 +186,81 @@ describe API::V3::Users, api: true do expect(response).to have_http_status(404) end end + + describe 'GET /users/:id/events' do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:note) { create(:note_on_issue, note: 'What an awesome day!', project: project) } + + before do + project.add_user(user, :developer) + EventCreateService.new.leave_note(note, user) + end + + context "as a user than cannot see the event's project" do + it 'returns no events' do + other_user = create(:user) + + get api("/users/#{user.id}/events", other_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_empty + end + end + + context "as a user than can see the event's project" do + context 'joined event' do + it 'returns the "joined" event' do + get v3_api("/users/#{user.id}/events", user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + + comment_event = json_response.find { |e| e['action_name'] == 'commented on' } + + expect(comment_event['project_id'].to_i).to eq(project.id) + expect(comment_event['author_username']).to eq(user.username) + expect(comment_event['note']['id']).to eq(note.id) + expect(comment_event['note']['body']).to eq('What an awesome day!') + + joined_event = json_response.find { |e| e['action_name'] == 'joined' } + + expect(joined_event['project_id'].to_i).to eq(project.id) + expect(joined_event['author_username']).to eq(user.username) + expect(joined_event['author']['name']).to eq(user.name) + end + end + + context 'when there are multiple events from different projects' do + let(:second_note) { create(:note_on_issue, project: create(:empty_project)) } + let(:third_note) { create(:note_on_issue, project: project) } + + before do + second_note.project.add_user(user, :developer) + + [second_note, third_note].each do |note| + EventCreateService.new.leave_note(note, user) + end + end + + it 'returns events in the correct order (from newest to oldest)' do + get v3_api("/users/#{user.id}/events", user) + + comment_events = json_response.select { |e| e['action_name'] == 'commented on' } + + expect(comment_events[0]['target_id']).to eq(third_note.id) + expect(comment_events[1]['target_id']).to eq(second_note.id) + expect(comment_events[2]['target_id']).to eq(note.id) + end + end + end + + it 'returns a 404 error if not found' do + get v3_api('/users/42/events', user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + end end -- cgit v1.2.3 From d3425933dddf4e849199c06dd3ce00c212d0c6da Mon Sep 17 00:00:00 2001 From: Mark Fletcher Date: Tue, 21 Feb 2017 20:21:31 +0530 Subject: Add housekeeping endpoint for Projects API --- .../27032-add-a-house-keeping-api-call.yml | 4 ++ doc/api/projects.md | 14 +++++++ lib/api/projects.rb | 13 ++++++ spec/requests/api/projects_spec.rb | 49 ++++++++++++++++++++++ 4 files changed, 80 insertions(+) create mode 100644 changelogs/unreleased/27032-add-a-house-keeping-api-call.yml diff --git a/changelogs/unreleased/27032-add-a-house-keeping-api-call.yml b/changelogs/unreleased/27032-add-a-house-keeping-api-call.yml new file mode 100644 index 00000000000..a9f70e339c0 --- /dev/null +++ b/changelogs/unreleased/27032-add-a-house-keeping-api-call.yml @@ -0,0 +1,4 @@ +--- +title: Add housekeeping endpoint for Projects API +merge_request: 9421 +author: diff --git a/doc/api/projects.md b/doc/api/projects.md index e9ef03a0c0c..872f570e0f6 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1195,3 +1195,17 @@ Parameters: | `query` | string | yes | A string contained in the project name | | `order_by` | string | no | Return requests ordered by `id`, `name`, `created_at` or `last_activity_at` fields | | `sort` | string | no | Return requests sorted in `asc` or `desc` order | + +## Start the Housekeeping task for a Project + +>**Note:** This feature was introduced in GitLab 9.0 + +``` +POST /projects/:id/housekeeping +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME | diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 366e5679edd..f1cb1b22143 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -374,6 +374,19 @@ module API present paginate(users), with: Entities::UserBasic end + + desc 'Start the housekeeping task for a project' do + detail 'This feature was introduced in GitLab 9.0.' + end + post ':id/housekeeping' do + authorize_admin_project + + begin + ::Projects::HousekeepingService.new(user_project).execute + rescue ::Projects::HousekeepingService::LeaseTaken => error + conflict!(error.message) + end + end end end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 4e90aae9279..2f1181b2e8c 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1422,4 +1422,53 @@ describe API::Projects, api: true do end end end + + describe 'POST /projects/:id/housekeeping' do + let(:housekeeping) { Projects::HousekeepingService.new(project) } + + before do + allow(Projects::HousekeepingService).to receive(:new).with(project).and_return(housekeeping) + end + + context 'when authenticated as owner' do + it 'starts the housekeeping process' do + expect(housekeeping).to receive(:execute).once + + post api("/projects/#{project.id}/housekeeping", user) + + expect(response).to have_http_status(201) + end + + context 'when housekeeping lease is taken' do + it 'returns conflict' do + expect(housekeeping).to receive(:execute).once.and_raise(Projects::HousekeepingService::LeaseTaken) + + post api("/projects/#{project.id}/housekeeping", user) + + expect(response).to have_http_status(409) + expect(json_response['message']).to match(/Somebody already triggered housekeeping for this project/) + end + end + end + + context 'when authenticated as developer' do + before do + project_member2 + end + + it 'returns forbidden error' do + post api("/projects/#{project.id}/housekeeping", user3) + + expect(response).to have_http_status(403) + end + end + + context 'when unauthenticated' do + it 'returns authentication error' do + post api("/projects/#{project.id}/housekeeping") + + expect(response).to have_http_status(401) + end + end + end end -- cgit v1.2.3 From 5806678e711aaa7a377d96a0f8623b67616bdb6e Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 22 Feb 2017 12:20:20 +0000 Subject: Convert button into a btn group according to mockups --- .../vue_pipelines_index/pipeline_actions.js.es6 | 61 ++++++++++++---------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 index 54e8f977a47..89d501538f5 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 @@ -19,9 +19,10 @@ }, template: ` -
-
+ -- cgit v1.2.3 From c8407a151f618c0abf6d70bfeaea62cfffd4143a Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 22 Feb 2017 12:20:57 +0000 Subject: Remove unused css, align buttons with top button --- app/assets/stylesheets/pages/pipelines.scss | 36 ++++++++++++----------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 3fe1eef307e..0a0ab2abc38 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -21,10 +21,6 @@ white-space: nowrap; } - .btn { - margin: 4px; - } - .table.ci-table { min-width: 1200px; table-layout: fixed; @@ -48,6 +44,10 @@ .pipeline-actions { width: 20%; } + + .pipeline-actions { + padding-right: 0; + } } } @@ -207,19 +207,20 @@ } .pipeline-actions { - min-width: 140px; + padding-right: 0; - .btn { - margin: 0; + .btn-default { color: $gl-text-color-secondary; } - .cancel-retry-btns { - vertical-align: middle; + .btn.btn-retry:hover, + .btn.btn-retry:focus { + border-color: #c4c4c4; + background-color: #f0f0f0; + } - .btn:not(:first-child) { - margin-left: 8px; - } + svg path { + fill: $gl-text-color-secondary; } .dropdown-menu { @@ -242,10 +243,6 @@ } } - .btn-remove { - color: $white-light; - } - .btn-group { &.open { .btn-default { @@ -267,11 +264,8 @@ } } - .build-link { - - a { - color: $gl-text-color; - } + .build-link a { + color: $gl-text-color; } .btn-group.open .dropdown-toggle { -- cgit v1.2.3 From 0578ad9a081f296cfbd062bf0f5437f879d20b3e Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 22 Feb 2017 12:21:27 +0000 Subject: Remove unused file --- .../projects/ci/pipelines/_pipeline.html.haml | 92 ---------------------- 1 file changed, 92 deletions(-) delete mode 100644 app/views/projects/ci/pipelines/_pipeline.html.haml diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml deleted file mode 100644 index 3475fa5f960..00000000000 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ /dev/null @@ -1,92 +0,0 @@ -- status = pipeline.status -- show_commit = local_assigns.fetch(:show_commit, true) -- show_branch = local_assigns.fetch(:show_branch, true) - -%tr.commit - %td.commit-link - = 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 - %span.pipeline-id ##{pipeline.id} - %span by - - if pipeline.user - = user_avatar(user: pipeline.user, size: 20) - - else - %span.api.monospace API - - if pipeline.latest? - %span.label.label-success.has-tooltip{ title: 'Latest pipeline for this branch' } latest - - if pipeline.triggered? - %span.label.label-primary triggered - - if pipeline.yaml_errors.present? - %span.label.label-danger.has-tooltip{ title: "#{pipeline.yaml_errors}" } yaml invalid - - if pipeline.builds.any?(&:stuck?) - %span.label.label-warning stuck - - %td.branch-commit - - if pipeline.ref && show_branch - .icon-container - = pipeline.tag? ? icon('tag') : icon('code-fork') - = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name" - - if show_commit - .icon-container.commit-icon - = custom_icon("icon_commit") - = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace" - - %p.commit-title - - if commit = pipeline.commit - = author_avatar(commit, size: 20) - = link_to_gfm truncate(commit.title, length: 60, escape: false), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message" - - else - Cant find HEAD commit for this branch - - %td - = render 'shared/mini_pipeline_graph', pipeline: pipeline, klass: 'js-mini-pipeline-graph' - - %td - - if pipeline.duration - %p.duration - = custom_icon("icon_timer") - = duration_in_numbers(pipeline.duration) - - if pipeline.finished_at - %p.finished-at - = icon("calendar") - #{time_ago_with_tooltip(pipeline.finished_at, short_format: false)} - - %td.pipeline-actions.hidden-xs - .controls.pull-right - - artifacts = pipeline.builds.latest.with_artifacts_not_expired - - actions = pipeline.manual_actions - - if artifacts.present? || actions.any? - .btn-group.inline - - if actions.any? - .btn-group - %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual pipeline', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual pipeline' } - = custom_icon('icon_play') - = icon('caret-down', 'aria-hidden' => 'true') - %ul.dropdown-menu.dropdown-menu-align-right - - actions.each do |build| - %li - = link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do - = custom_icon('icon_play') - %span= build.name - - if artifacts.present? - .btn-group - %button.dropdown-toggle.btn.btn-default.build-artifacts.has-tooltip.js-pipeline-dropdown-download{ type: 'button', title: 'Artifacts', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Artifacts' } - = icon("download") - = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right - - artifacts.each do |build| - %li - = link_to download_namespace_project_build_artifacts_path(pipeline.project.namespace, pipeline.project, build), rel: 'nofollow', download: '' do - = icon("download") - %span Download '#{build.name}' artifacts - - - if can?(current_user, :update_pipeline, pipeline.project) - .cancel-retry-btns.inline - - if pipeline.retryable? - = link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: 'Retry', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Retry' , method: :post do - = icon("repeat") - - if pipeline.cancelable? - = link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: 'Cancel', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Cancel' , method: :post do - = icon("remove") -- cgit v1.2.3 From 3d013487cea47272ff40d3f9a4b02960d2c2591f Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Wed, 22 Feb 2017 11:03:04 +0100 Subject: Grapify the CI::Runners API --- lib/ci/api/runners.rb | 44 ++++++++++++++++++-------------------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb index bcc82969eb3..2a611a67eaf 100644 --- a/lib/ci/api/runners.rb +++ b/lib/ci/api/runners.rb @@ -1,44 +1,36 @@ module Ci module API - # Runners API class Runners < Grape::API resource :runners do - # Delete runner - # Parameters: - # token (required) - The unique token of runner - # - # Example Request: - # GET /runners/delete + desc 'Delete a runner' + params do + requires :token, type: String, desc: 'The unique token of the runner' + end delete "delete" do - required_attributes! [:token] authenticate_runner! Ci::Runner.find_by_token(params[:token]).destroy end - # Register a new runner - # - # Note: This is an "internal" API called when setting up - # runners, so it is authenticated differently. - # - # Parameters: - # token (required) - The unique token of runner - # - # Example Request: - # POST /runners/register + desc 'Register a new runner' do + success Entities::Runner + end + params do + requires :token, type: String, desc: 'The unique token of the runner' + optional :description, type: String, desc: 'The description of the runner' + optional :tag_list, type: Array[String], desc: 'A list of tags the runner should run for' + optional :run_untagged, type: Boolean, desc: 'Flag if the runner should execute untagged jobs' + optional :locked, type: Boolean, desc: 'Lock this runner for this specific project' + end post "register" do - required_attributes! [:token] - - attributes = attributes_for_keys( - [:description, :tag_list, :run_untagged, :locked] - ) + runner_params = declared(params, include_missing: false) runner = if runner_registration_token_valid? # Create shared runner. Requires admin access - Ci::Runner.create(attributes.merge(is_shared: true)) - elsif project = Project.find_by(runners_token: params[:token]) + Ci::Runner.create(runner_params.merge(is_shared: true)) + elsif project = Project.find_by(runners_token: runner_params[:token]) # Create a specific runner for project. - project.runners.create(attributes) + project.runners.create(runner_params) end return forbidden! unless runner -- cgit v1.2.3 From 80033d8c326aa2b4c4e4ed7da7ab7e174af27451 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Wed, 22 Feb 2017 11:33:23 -0300 Subject: Update CHANGELOG.md for 8.17.0 [ci skip] --- CHANGELOG.md | 180 +++++++++++++++++++++ changelogs/unreleased/17662-rename-builds.yml | 4 - ...e-require-from-request_profiler-initializer.yml | 4 - ...-project-better-blank-state-for-labels-view.yml | 4 - .../unreleased/21518_recaptcha_spam_issues.yml | 4 - .../unreleased/22007-unify-projects-search.yml | 4 - ...creating-a-branch-matching-a-wildcard-fails.yml | 4 - .../22974-trigger-service-events-through-api.yml | 4 - ...23524-notify-automerge-user-of-failed-build.yml | 4 - .../unreleased/23634-remove-project-grouping.yml | 4 - ...67-disable-storing-of-sensitive-information.yml | 4 - changelogs/unreleased/24147-delete-env-button.yml | 4 - .../24606-force-password-reset-on-next-login.yml | 4 - .../unreleased/24716-fix-ctrl-click-links.yml | 4 - .../24795_refactor_merge_request_build_service.yml | 4 - ...low-to-search-by-commit-hash-within-project.yml | 4 - changelogs/unreleased/24923_nested_tasks.yml | 4 - ...e-view-doesn-t-show-organization-membership.yml | 4 - .../25312-search-input-cmd-click-issue.yml | 4 - .../25360-remove-flash-warning-from-login-page.yml | 4 - .../25460-replace-word-users-with-members.yml | 4 - ...anticipate-obstacles-to-removing-turbolinks.yml | 4 - ...o-not-update-when-new-pipeline-is-triggered.yml | 4 - ...ion-icons-to-svg-to-propperly-position-them.yml | 4 - .../25989-fix-rogue-scrollbars-on-comments.yml | 4 - changelogs/unreleased/26059-segoe-ui-vertical.yml | 4 - changelogs/unreleased/26068_tasklist_issue.yml | 4 - .../unreleased/26117-sort-pipeline-for-commit.yml | 4 - ...plus-and-minus-signs-as-part-of-line-number.yml | 4 - .../26445-accessible-piplelines-buttons.yml | 4 - changelogs/unreleased/26447-fix-tab-list-order.yml | 4 - .../26468-fix-users-sort-in-admin-area.yml | 4 - .../unreleased/26787-add-copy-icon-hover-state.yml | 4 - ...l-visible-when-there-are-no-lines-to-unfold.yml | 5 - .../unreleased/26852-fix-slug-for-openshift.yml | 4 - ...63-Remove-hover-animation-from-row-elements.yml | 4 - .../26881-backup-fails-if-data-changes.yml | 4 - .../26920-hover-cursor-on-pagination-element.yml | 4 - .../unreleased/26947-build-status-self-link.yml | 4 - ...ove-pipeline-status-icon-linking-in-widgets.yml | 4 - .../27013-regression-in-commit-title-bar.yml | 4 - .../27014-fix-pipeline-tooltip-wrapping.yml | 4 - .../27021-line-numbers-now-in-copy-pasta-data.yml | 4 - ...7178-update-builds-link-in-project-settings.yml | 4 - .../27240-make-progress-bars-consistent.yml | 4 - ...small-mini-pipeline-graph-glitch-upon-hover.yml | 4 - .../27291-unify-mr-diff-file-buttons.yml | 4 - ...le-separator-line-in-edit-projects-settings.yml | 4 - ...s-has-no-line-spacing-in-firefox-and-safari.yml | 4 - .../27352-search-label-filter-header.yml | 4 - .../27395-reduce-group-activity-sql-queries-2.yml | 4 - .../27395-reduce-group-activity-sql-queries.yml | 4 - .../unreleased/27484-environment-show-name.yml | 4 - changelogs/unreleased/27488-fix-jwt-version.yml | 4 - .../27494-environment-list-column-headers.yml | 4 - ...-fix-avatar-border-flicker-mention-dropdown.yml | 4 - changelogs/unreleased/27632_fix_mr_widget_url.yml | 4 - ...l-under-the-side-panel-in-the-merge-request.yml | 4 - changelogs/unreleased/27656-doc-ci-enable-ci.yml | 4 - ...-visualization-graph-page-with-roboto-fonts.yml | 4 - .../27822-default-bulk-assign-labels.yml | 4 - ...-commit-comments-are-shared-across-projects.yml | 4 - ...0-pipelines-table-not-showing-commit-branch.yml | 4 - ...-character-is-part-of-an-autocompleted-text.yml | 4 - .../27922-cmd-click-todo-doesn-t-work.yml | 5 - .../27925-fix-mr-stray-pipelines-api-request.yml | 4 - ...27932-merge-request-pipelines-displays-json.yml | 4 - .../unreleased/27939-fix-current-build-arrow.yml | 4 - ...ution-list-on-profile-page-is-aligned-right.yml | 4 - ...ading-icon-and-text-in-merge-request-widget.yml | 4 - ...27955-mr-notification-use-pipeline-language.yml | 4 - changelogs/unreleased/27963-tooltips-jobs.yml | 4 - ...966-branch-ref-switcher-input-filter-broken.yml | 4 - .../unreleased/27987-skipped-pipeline-mr-graph.yml | 4 - .../27991-success-with-warnings-caret.yml | 4 - ...029-improve-blockquote-formatting-on-emails.yml | 4 - changelogs/unreleased/28032-tooltips-file-name.yml | 5 - ...28059-add-pagination-to-admin-abuse-reports.yml | 4 - ...95-fix-notification-when-group-set-to-watch.yml | 4 - changelogs/unreleased/8-15-stable.yml | 4 - changelogs/unreleased/8082-permalink-to-file.yml | 4 - changelogs/unreleased/9-0-api-changes.yml | 4 - ...ash-command-for-target-merge-request-branch.yml | 4 - changelogs/unreleased/add_project_update_hook.yml | 4 - .../unreleased/api-remove-snippets-expires-at.yml | 4 - changelogs/unreleased/babel-all-the-things.yml | 5 - ..._when_doing_offline_update_check-help_page-.yml | 4 - changelogs/unreleased/change_queue_weight.yml | 4 - .../unreleased/clipboard-button-commit-sha.yml | 3 - .../unreleased/contribution-calendar-scroll.yml | 4 - changelogs/unreleased/cop-gem-fetcher.yml | 4 - changelogs/unreleased/copy-as-md.yml | 4 - ...sable-autologin-on-email-confirmation-links.yml | 4 - changelogs/unreleased/display-project-id.yml | 4 - changelogs/unreleased/document-how-to-vue.yml | 4 - changelogs/unreleased/dynamic-todos-fixture.yml | 4 - .../unreleased/dz-nested-groups-improvements-2.yml | 4 - .../unreleased/empty-selection-reply-shortcut.yml | 4 - changelogs/unreleased/fe-commit-mr-pipelines.yml | 4 - ...ture-success-warning-icons-in-stages-builds.yml | 4 - changelogs/unreleased/fix-27479.yml | 4 - changelogs/unreleased/fix-api-mr-permissions.yml | 4 - changelogs/unreleased/fix-ar-connection-leaks.yml | 4 - changelogs/unreleased/fix-ci-build-policy.yml | 4 - .../unreleased/fix-deleting-project-again.yml | 4 - changelogs/unreleased/fix-depr-warn.yml | 4 - ...-gb-backwards-compatibility-coverage-ci-yml.yml | 4 - .../fix-guest-access-posting-to-notes.yml | 4 - changelogs/unreleased/fix-import-encrypt-atts.yml | 4 - changelogs/unreleased/fix-import-group-members.yml | 4 - .../unreleased/fix-job-to-pipeline-renaming.yml | 4 - .../unreleased/fix-references-header-parsing.yml | 5 - changelogs/unreleased/fix-scroll-test.yml | 4 - .../fix-users-deleting-public-deployment-keys.yml | 4 - .../unreleased/fix_broken_diff_discussions.yml | 4 - ...arning_message_in_admin_background_job_page.yml | 4 - changelogs/unreleased/fwn-to-find-by-full-path.yml | 4 - ...om-notification_service_spec-to-make-it-DRY.yml | 4 - changelogs/unreleased/git_to_html_redirection.yml | 4 - changelogs/unreleased/go-go-gadget-webpack.yml | 4 - changelogs/unreleased/group-label-sidebar-link.yml | 4 - .../unreleased/hardcode-title-system-note.yml | 4 - .../unreleased/improve-ci-example-php-doc.yml | 4 - .../improve-handleLocationHash-tests.yml | 4 - changelogs/unreleased/issuable-sidebar-bug.yml | 4 - changelogs/unreleased/issue-20428.yml | 4 - .../unreleased/issue-sidebar-empty-assignee.yml | 4 - changelogs/unreleased/issue_19262.yml | 4 - changelogs/unreleased/issue_23317.yml | 4 - changelogs/unreleased/issue_27211.yml | 4 - changelogs/unreleased/jej-pages-picked-from-ee.yml | 4 - changelogs/unreleased/label-promotion.yml | 4 - changelogs/unreleased/lfs-noauth-public-repo.yml | 4 - changelogs/unreleased/markdown-plantuml.yml | 4 - .../unreleased/merge-request-tabs-fixture.yml | 4 - .../misalinged-discussion-with-no-avatar-26854.yml | 4 - changelogs/unreleased/mr-tabs-container-offset.yml | 4 - changelogs/unreleased/newline-eslint-rule.yml | 4 - .../unreleased/no-sidebar-on-action-btn-click.yml | 4 - changelogs/unreleased/no_project_notes.yml | 4 - .../unreleased/pms-lowercase-system-notes.yml | 4 - .../redesign-searchbar-admin-project-26794.yml | 4 - .../redirect-to-commit-when-only-commit-found.yml | 5 - changelogs/unreleased/relative-url-assets.yml | 4 - .../unreleased/remove-deploy-key-endpoint.yml | 4 - ...remove-issue-and-mr-counts-from-labels-page.yml | 4 - changelogs/unreleased/route-map.yml | 4 - changelogs/unreleased/rs-warden-blocked-users.yml | 4 - .../sh-add-index-to-ci-trigger-requests.yml | 4 - changelogs/unreleased/sh-add-labels-index.yml | 4 - changelogs/unreleased/slash-commands-typo.yml | 4 - .../unreleased/small-screen-fullscreen-button.yml | 4 - .../unreleased/snippets-search-performance.yml | 4 - .../unreleased/tc-only-mr-button-if-allowed.yml | 4 - .../unreleased/terminal-max-session-time.yml | 4 - changelogs/unreleased/updated-pages-0-3-1.yml | 4 - changelogs/unreleased/upgrade-babel-v6.yml | 4 - changelogs/unreleased/upgrade-omniauth.yml | 4 - changelogs/unreleased/upgrade-webpack-v2-2.yml | 4 - changelogs/unreleased/wip-mr-from-commits.yml | 4 - changelogs/unreleased/zj-format-chat-messages.yml | 4 - .../unreleased/zj-remove-deprecated-ci-service.yml | 4 - .../unreleased/zj-requeue-pending-delete.yml | 4 - 163 files changed, 180 insertions(+), 653 deletions(-) delete mode 100644 changelogs/unreleased/17662-rename-builds.yml delete mode 100644 changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml delete mode 100644 changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml delete mode 100644 changelogs/unreleased/21518_recaptcha_spam_issues.yml delete mode 100644 changelogs/unreleased/22007-unify-projects-search.yml delete mode 100644 changelogs/unreleased/22638-creating-a-branch-matching-a-wildcard-fails.yml delete mode 100644 changelogs/unreleased/22974-trigger-service-events-through-api.yml delete mode 100644 changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml delete mode 100644 changelogs/unreleased/23634-remove-project-grouping.yml delete mode 100644 changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml delete mode 100644 changelogs/unreleased/24147-delete-env-button.yml delete mode 100644 changelogs/unreleased/24606-force-password-reset-on-next-login.yml delete mode 100644 changelogs/unreleased/24716-fix-ctrl-click-links.yml delete mode 100644 changelogs/unreleased/24795_refactor_merge_request_build_service.yml delete mode 100644 changelogs/unreleased/24833-Allow-to-search-by-commit-hash-within-project.yml delete mode 100644 changelogs/unreleased/24923_nested_tasks.yml delete mode 100644 changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml delete mode 100644 changelogs/unreleased/25312-search-input-cmd-click-issue.yml delete mode 100644 changelogs/unreleased/25360-remove-flash-warning-from-login-page.yml delete mode 100644 changelogs/unreleased/25460-replace-word-users-with-members.yml delete mode 100644 changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml delete mode 100644 changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml delete mode 100644 changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml delete mode 100644 changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml delete mode 100644 changelogs/unreleased/26059-segoe-ui-vertical.yml delete mode 100644 changelogs/unreleased/26068_tasklist_issue.yml delete mode 100644 changelogs/unreleased/26117-sort-pipeline-for-commit.yml delete mode 100644 changelogs/unreleased/26186-diff-view-plus-and-minus-signs-as-part-of-line-number.yml delete mode 100644 changelogs/unreleased/26445-accessible-piplelines-buttons.yml delete mode 100644 changelogs/unreleased/26447-fix-tab-list-order.yml delete mode 100644 changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml delete mode 100644 changelogs/unreleased/26787-add-copy-icon-hover-state.yml delete mode 100644 changelogs/unreleased/26824-diff-unfold-link-is-still-visible-when-there-are-no-lines-to-unfold.yml delete mode 100644 changelogs/unreleased/26852-fix-slug-for-openshift.yml delete mode 100644 changelogs/unreleased/26863-Remove-hover-animation-from-row-elements.yml delete mode 100644 changelogs/unreleased/26881-backup-fails-if-data-changes.yml delete mode 100644 changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml delete mode 100644 changelogs/unreleased/26947-build-status-self-link.yml delete mode 100644 changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml delete mode 100644 changelogs/unreleased/27013-regression-in-commit-title-bar.yml delete mode 100644 changelogs/unreleased/27014-fix-pipeline-tooltip-wrapping.yml delete mode 100644 changelogs/unreleased/27021-line-numbers-now-in-copy-pasta-data.yml delete mode 100644 changelogs/unreleased/27178-update-builds-link-in-project-settings.yml delete mode 100644 changelogs/unreleased/27240-make-progress-bars-consistent.yml delete mode 100644 changelogs/unreleased/27277-small-mini-pipeline-graph-glitch-upon-hover.yml delete mode 100644 changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml delete mode 100644 changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml delete mode 100644 changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml delete mode 100644 changelogs/unreleased/27352-search-label-filter-header.yml delete mode 100644 changelogs/unreleased/27395-reduce-group-activity-sql-queries-2.yml delete mode 100644 changelogs/unreleased/27395-reduce-group-activity-sql-queries.yml delete mode 100644 changelogs/unreleased/27484-environment-show-name.yml delete mode 100644 changelogs/unreleased/27488-fix-jwt-version.yml delete mode 100644 changelogs/unreleased/27494-environment-list-column-headers.yml delete mode 100644 changelogs/unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml delete mode 100644 changelogs/unreleased/27632_fix_mr_widget_url.yml delete mode 100644 changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml delete mode 100644 changelogs/unreleased/27656-doc-ci-enable-ci.yml delete mode 100644 changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml delete mode 100644 changelogs/unreleased/27822-default-bulk-assign-labels.yml delete mode 100644 changelogs/unreleased/27873-when-a-commit-appears-in-several-projects-commit-comments-are-shared-across-projects.yml delete mode 100644 changelogs/unreleased/27880-pipelines-table-not-showing-commit-branch.yml delete mode 100644 changelogs/unreleased/27883-autocomplete-seems-to-not-trigger-when-at-character-is-part-of-an-autocompleted-text.yml delete mode 100644 changelogs/unreleased/27922-cmd-click-todo-doesn-t-work.yml delete mode 100644 changelogs/unreleased/27925-fix-mr-stray-pipelines-api-request.yml delete mode 100644 changelogs/unreleased/27932-merge-request-pipelines-displays-json.yml delete mode 100644 changelogs/unreleased/27939-fix-current-build-arrow.yml delete mode 100644 changelogs/unreleased/27943-contribution-list-on-profile-page-is-aligned-right.yml delete mode 100644 changelogs/unreleased/27947-missing-margin-between-loading-icon-and-text-in-merge-request-widget.yml delete mode 100644 changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml delete mode 100644 changelogs/unreleased/27963-tooltips-jobs.yml delete mode 100644 changelogs/unreleased/27966-branch-ref-switcher-input-filter-broken.yml delete mode 100644 changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml delete mode 100644 changelogs/unreleased/27991-success-with-warnings-caret.yml delete mode 100644 changelogs/unreleased/28029-improve-blockquote-formatting-on-emails.yml delete mode 100644 changelogs/unreleased/28032-tooltips-file-name.yml delete mode 100644 changelogs/unreleased/28059-add-pagination-to-admin-abuse-reports.yml delete mode 100644 changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml delete mode 100644 changelogs/unreleased/8-15-stable.yml delete mode 100644 changelogs/unreleased/8082-permalink-to-file.yml delete mode 100644 changelogs/unreleased/9-0-api-changes.yml delete mode 100644 changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml delete mode 100644 changelogs/unreleased/add_project_update_hook.yml delete mode 100644 changelogs/unreleased/api-remove-snippets-expires-at.yml delete mode 100644 changelogs/unreleased/babel-all-the-things.yml delete mode 100644 changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml delete mode 100644 changelogs/unreleased/change_queue_weight.yml delete mode 100644 changelogs/unreleased/clipboard-button-commit-sha.yml delete mode 100644 changelogs/unreleased/contribution-calendar-scroll.yml delete mode 100644 changelogs/unreleased/cop-gem-fetcher.yml delete mode 100644 changelogs/unreleased/copy-as-md.yml delete mode 100644 changelogs/unreleased/disable-autologin-on-email-confirmation-links.yml delete mode 100644 changelogs/unreleased/display-project-id.yml delete mode 100644 changelogs/unreleased/document-how-to-vue.yml delete mode 100644 changelogs/unreleased/dynamic-todos-fixture.yml delete mode 100644 changelogs/unreleased/dz-nested-groups-improvements-2.yml delete mode 100644 changelogs/unreleased/empty-selection-reply-shortcut.yml delete mode 100644 changelogs/unreleased/fe-commit-mr-pipelines.yml delete mode 100644 changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml delete mode 100644 changelogs/unreleased/fix-27479.yml delete mode 100644 changelogs/unreleased/fix-api-mr-permissions.yml delete mode 100644 changelogs/unreleased/fix-ar-connection-leaks.yml delete mode 100644 changelogs/unreleased/fix-ci-build-policy.yml delete mode 100644 changelogs/unreleased/fix-deleting-project-again.yml delete mode 100644 changelogs/unreleased/fix-depr-warn.yml delete mode 100644 changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml delete mode 100644 changelogs/unreleased/fix-guest-access-posting-to-notes.yml delete mode 100644 changelogs/unreleased/fix-import-encrypt-atts.yml delete mode 100644 changelogs/unreleased/fix-import-group-members.yml delete mode 100644 changelogs/unreleased/fix-job-to-pipeline-renaming.yml delete mode 100644 changelogs/unreleased/fix-references-header-parsing.yml delete mode 100644 changelogs/unreleased/fix-scroll-test.yml delete mode 100644 changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml delete mode 100644 changelogs/unreleased/fix_broken_diff_discussions.yml delete mode 100644 changelogs/unreleased/fix_sidekiq_concurrency_warning_message_in_admin_background_job_page.yml delete mode 100644 changelogs/unreleased/fwn-to-find-by-full-path.yml delete mode 100644 changelogs/unreleased/get-rid-of-water-from-notification_service_spec-to-make-it-DRY.yml delete mode 100644 changelogs/unreleased/git_to_html_redirection.yml delete mode 100644 changelogs/unreleased/go-go-gadget-webpack.yml delete mode 100644 changelogs/unreleased/group-label-sidebar-link.yml delete mode 100644 changelogs/unreleased/hardcode-title-system-note.yml delete mode 100644 changelogs/unreleased/improve-ci-example-php-doc.yml delete mode 100644 changelogs/unreleased/improve-handleLocationHash-tests.yml delete mode 100644 changelogs/unreleased/issuable-sidebar-bug.yml delete mode 100644 changelogs/unreleased/issue-20428.yml delete mode 100644 changelogs/unreleased/issue-sidebar-empty-assignee.yml delete mode 100644 changelogs/unreleased/issue_19262.yml delete mode 100644 changelogs/unreleased/issue_23317.yml delete mode 100644 changelogs/unreleased/issue_27211.yml delete mode 100644 changelogs/unreleased/jej-pages-picked-from-ee.yml delete mode 100644 changelogs/unreleased/label-promotion.yml delete mode 100644 changelogs/unreleased/lfs-noauth-public-repo.yml delete mode 100644 changelogs/unreleased/markdown-plantuml.yml delete mode 100644 changelogs/unreleased/merge-request-tabs-fixture.yml delete mode 100644 changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml delete mode 100644 changelogs/unreleased/mr-tabs-container-offset.yml delete mode 100644 changelogs/unreleased/newline-eslint-rule.yml delete mode 100644 changelogs/unreleased/no-sidebar-on-action-btn-click.yml delete mode 100644 changelogs/unreleased/no_project_notes.yml delete mode 100644 changelogs/unreleased/pms-lowercase-system-notes.yml delete mode 100644 changelogs/unreleased/redesign-searchbar-admin-project-26794.yml delete mode 100644 changelogs/unreleased/redirect-to-commit-when-only-commit-found.yml delete mode 100644 changelogs/unreleased/relative-url-assets.yml delete mode 100644 changelogs/unreleased/remove-deploy-key-endpoint.yml delete mode 100644 changelogs/unreleased/remove-issue-and-mr-counts-from-labels-page.yml delete mode 100644 changelogs/unreleased/route-map.yml delete mode 100644 changelogs/unreleased/rs-warden-blocked-users.yml delete mode 100644 changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml delete mode 100644 changelogs/unreleased/sh-add-labels-index.yml delete mode 100644 changelogs/unreleased/slash-commands-typo.yml delete mode 100644 changelogs/unreleased/small-screen-fullscreen-button.yml delete mode 100644 changelogs/unreleased/snippets-search-performance.yml delete mode 100644 changelogs/unreleased/tc-only-mr-button-if-allowed.yml delete mode 100644 changelogs/unreleased/terminal-max-session-time.yml delete mode 100644 changelogs/unreleased/updated-pages-0-3-1.yml delete mode 100644 changelogs/unreleased/upgrade-babel-v6.yml delete mode 100644 changelogs/unreleased/upgrade-omniauth.yml delete mode 100644 changelogs/unreleased/upgrade-webpack-v2-2.yml delete mode 100644 changelogs/unreleased/wip-mr-from-commits.yml delete mode 100644 changelogs/unreleased/zj-format-chat-messages.yml delete mode 100644 changelogs/unreleased/zj-remove-deprecated-ci-service.yml delete mode 100644 changelogs/unreleased/zj-requeue-pending-delete.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 58b8cf2ad83..7f5b101ad6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,186 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 8.17.0 (2017-02-22) + +- API: Fix file downloading. !0 (8267) +- Changed composer installer script in the CI PHP example doc. !4342 (Jeffrey Cafferata) +- Display fullscreen button on small screens. !5302 (winniehell) +- Add system hook for when a project is updated (other than rename/transfer). !5711 (Tommy Beadle) +- Fix notifications when set at group level. !6813 (Alexandre Maia) +- Project labels can now be promoted to group labels. !7242 (Olaf Tomalka) +- use webpack to bundle frontend assets and use karma for frontend testing. !7288 +- Adds back ability to stop all environments. !7379 +- Added labels empty state. !7443 +- Add ability to define a coverage regex in the .gitlab-ci.yml. !7447 (Leandro Camargo) +- Disable automatic login after clicking email confirmation links. !7472 +- Search feature: redirects to commit page if query is commit sha and only commit found. !8028 (YarNayar) +- Create a TODO for user who set auto-merge when a build fails, merge conflict occurs. !8056 (twonegatives) +- Don't group issues by project on group-level and dashboard issue indexes. !8111 (Bernardo Castro) +- Mark MR as WIP when pushing WIP commits. !8124 (Jurre Stender @jurre) +- Flag multiple empty lines in eslint, fix offenses. !8137 +- Add sorting pipeline for a commit. !8319 (Takuya Noguchi) +- Adds service trigger events to api. !8324 +- Update pipeline and commit links when CI status is updated. !8351 +- Hide version check image if there is no internet connection. !8355 (Ken Ding) +- Prevent removal of input fields if it is the parent dropdown element. !8397 +- Introduce maximum session time for terminal websocket connection. !8413 +- Allow creating protected branches when user can merge to such branch. !8458 +- Refactor MergeRequests::BuildService. !8462 (Rydkin Maxim) +- Added GitLab Pages to CE. !8463 +- Support notes when a project is not specified (personal snippet notes). !8468 +- Use warning icon in mini-graph if stage passed conditionally. !8503 +- Don’t count tasks that are not defined as list items correctly. !8526 +- Reformat messages ChatOps. !8528 +- Copy commit SHA to clipboard. !8547 +- Improve button accessibility on pipelines page. !8561 +- Display project ID in project settings. !8572 (winniehell) +- PlantUML support for Markdown. !8588 (Horacio Sanson) +- Fix reply by email without sub-addressing for some clients from Microsoft and Apple. !8620 +- Fix nested tasks in ordered list. !8626 +- Fix Sort by Recent Sign-in in Admin Area. !8637 (Poornima M) +- Avoid repeated dashes in $CI_ENVIRONMENT_SLUG. !8638 +- Only show Merge Request button when user can create a MR. !8639 +- Prevent copying of line numbers in parallel diff view. !8706 +- Improve build policy and access abilities. !8711 +- API: Remove /projects/:id/keys/.. endpoints. !8716 (Robert Schilling) +- API: Remove deprecated 'expires_at' from project snippets. !8723 (Robert Schilling) +- Add `copy` backup strategy to combat file changed errors. !8728 +- adds avatar for discussion note. !8734 +- Add link verification to badge partial in order to render a badge without a link. !8740 +- Reduce hits to LDAP on Git HTTP auth by reordering auth mechanisms. !8752 +- prevent diff unfolding link from appearing when there are no more lines to show. !8761 +- Redesign searchbar in admin project list. !8776 +- Rename Builds to Pipelines, CI/CD Pipelines, or Jobs everywhere. !8787 +- dismiss sidebar on repo buttons click. !8798 (Adam Pahlevi) +- fixed small mini pipeline graph line glitch. !8804 +- Make all system notes lowercase. !8807 +- Support unauthenticated LFS object downloads for public projects. !8824 (Ben Boeckel) +- Add read-only full_path and full_name attributes to Group API. !8827 +- allow relative url change without recompiling frontend assets. !8831 +- Use vue.js Pipelines table in commit and merge request view. !8844 +- Use reCaptcha when an issue is identified as a spam. !8846 +- resolve deprecation warnings. !8855 (Adam Pahlevi) +- Cop for gem fetched from a git source. !8856 (Adam Pahlevi) +- Remove flash warning from login page. !8864 (Gerald J. Padilla) +- Adds documentation for how to use Vue.js. !8866 +- Add 'View on [env]' link to blobs and individual files in diffs. !8867 +- Replace word user with member. !8872 +- Change the reply shortcut to focus the field even without a selection. !8873 (Brian Hall) +- Unify MR diff file button style. !8874 +- Unify projects search by removing /projects/:search endpoint. !8877 +- Fix disable storing of sensitive information when importing a new repo. !8885 (Bernard Pietraga) +- Fix pipeline graph vertical spacing in Firefox and Safari. !8886 +- Fix filtered search user autocomplete for gitlab instances that are hosted on a subdirectory. !8891 +- Fix Ctrl+Click support for Todos and Merge Request page tabs. !8898 +- Fix wrong call to ProjectCacheWorker.perform. !8910 +- Don't perform Devise trackable updates on blocked User records. !8915 +- Add ability to export project inherited group members to Import/Export. !8923 +- replace `find_with_namespace` with `find_by_full_path`. !8949 (Adam Pahlevi) +- Fixes flickering of avatar border in mention dropdown. !8950 +- Remove unnecessary queries for .atom and .json in Dashboard::ProjectsController#index. !8956 +- Fix deleting projects with pipelines and builds. !8960 +- Fix broken anchor links when special characters are used. !8961 (Andrey Krivko) +- Ensure autogenerated title does not cause failing spec. !8963 (brian m. carlson) +- Update doc for enabling or disabling GitLab CI. !8965 (Takuya Noguchi) +- Remove deprecated MR and Issue endpoints and preserve V3 namespace. !8967 +- Fixed "substract" typo on /help/user/project/slash_commands. !8976 (Jason Aquino) +- Preserve backward compatibility CI/CD and disallow setting `coverage` regexp in global context. !8981 +- use babel to transpile all non-vendor javascript assets regardless of file extension. !8988 +- Fix MR widget url. !8989 +- Fixes hover cursor on pipeline pagenation. !9003 +- Layer award emoji dropdown over the right sidebar. !9004 +- Do not display deploy keys in user's own ssh keys list. !9024 +- upgrade babel 5.8.x to babel 6.22.x. !9072 +- upgrade to webpack v2.2. !9078 +- Trigger autocomplete after selecting a slash command. !9117 +- Add space between text and loading icon in Megre Request Widget. !9119 +- Fix job to pipeline renaming. !9147 +- Replace static fixture for merge_request_tabs_spec.js. !9172 (winniehell) +- Replace static fixture for right_sidebar_spec.js. !9211 (winniehell) +- Show merge errors in merge request widget. !9229 +- Increase process_commit queue weight from 2 to 3. !9326 (blackst0ne) +- Don't require lib/gitlab/request_profiler/middleware.rb in config/initializers/request_profiler.rb. +- Force new password after password reset via API. (George Andrinopoulos) +- Allows to search within project by commit hash. (YarNayar) +- Show organisation membership and delete comment on smaller viewports, plus change comment author name to username. +- Remove turbolinks. +- Convert pipeline action icons to svg to have them propperly positioned. +- Remove rogue scrollbars for issue comments with inline elements. +- Align Segoe UI label text. +- Color + and - signs in diffs to increase code legibility. +- Fix tab index order on branch commits list page. (Ryan Harris) +- Add hover style to copy icon on commit page header. (Ryan Harris) +- Remove hover animation from row elements. +- Improve pipeline status icon linking in widgets. +- Fix commit title bar and repository view copy clipboard button order on last commit in repository view. +- Fix mini-pipeline stage tooltip text wrapping. +- Updated builds info link on the project settings page. (Ryan Harris) +- 27240 Make progress bars consistent. +- Only render hr when user can't archive project. +- 27352-search-label-filter-header. +- Include :author, :project, and :target in Event.with_associations. +- Don't instantiate AR objects in Event.in_projects. +- Don't capitalize environment name in show page. +- Update and pin the `jwt` gem to ~> 1.5.6. +- Edited the column header for the environments list from created to updated and added created to environments detail page colum header titles. +- Give ci status text on pipeline graph a better font-weight. +- Add default labels to bulk assign dropdowns. +- Only return target project's comments for a commit. +- Fixes Pipelines table is not showing branch name for commit. +- Fix regression where cmd-click stopped working for todos and merge request tabs. +- Fix stray pipelines API request when showing MR. +- Fix Merge request pipelines displays JSON. +- Fix current build arrow indicator. +- Fix contribution activity alignment. +- Show Pipeline(not Job) in MR desktop notification. +- Fix tooltips in mini pipeline graph. +- Display loading indicator when filtering ref switcher dropdown. +- Show pipeline graph in MR widget if there are any stages. +- Fix icon colors in merge request widget mini graph. +- Improve blockquote formatting in notification emails. +- Adds container to tooltip in order to make it work with overflow:hidden in parent element. +- Restore pagination to admin abuse reports. +- Ensure export files are removed after a namespace is deleted. +- Add `y` keyboard shortcut to move to file permalink. +- Adds /target_branch slash command functionality for merge requests. (YarNayar) +- Patch Asciidocs rendering to block XSS. +- contribution calendar scrolls from right to left. +- Copying a rendered issue/comment will paste into GFM textareas as actual GFM. +- Don't delete assigned MRs/issues when user is deleted. +- Remove new branch button for confidential issues. +- Don't allow project guests to subscribe to merge requests through the API. (Robert Schilling) +- Don't connect in Gitlab::Database.adapter_name. +- Prevent users from creating notes on resources they can't access. +- Ignore encrypted attributes in Import/Export. +- Change rspec test to guarantee window is resized before visiting page. +- Prevent users from deleting system deploy keys via the project deploy key API. +- Fix XSS vulnerability in SVG attachments. +- Make MR-review-discussions more reliable. +- fix incorrect sidekiq concurrency count in admin background page. (wendy0402) +- Make notification_service spec DRYer by making test reusable. (YarNayar) +- Redirect http://someproject.git to http://someproject. (blackst0ne) +- Fixed group label links in issue/merge request sidebar. +- Improve gl.utils.handleLocationHash tests. +- Fixed Issuable sidebar not closing on smaller/mobile sized screens. +- Resets assignee dropdown when sidebar is open. +- Disallow system notes for closed issuables. +- Fix timezone on issue boards due date. +- Remove unused js response from refs controller. +- Prevent the GitHub importer from assigning labels and comments to merge requests or issues belonging to other projects. +- Fixed merge requests tab extra margin when fixed to window. +- Patch XSS vulnerability in RDOC support. +- Refresh authorizations when transferring projects. +- Remove issue and MR counts from labels index. +- Don't use backup Active Record connections for Sidekiq. +- Add index to ci_trigger_requests for commit_id. +- Add indices to improve loading of labels page. +- Reduced query count for snippet search. +- Update GitLab Pages to v0.3.1. +- Upgrade omniauth gem to 1.3.2. +- Remove deprecated GitlabCiService. +- Requeue pending deletion projects. + ## 8.16.6 (2017-02-17) - API: Fix file downloading. !0 (8267) diff --git a/changelogs/unreleased/17662-rename-builds.yml b/changelogs/unreleased/17662-rename-builds.yml deleted file mode 100644 index 12f2998d1c8..00000000000 --- a/changelogs/unreleased/17662-rename-builds.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Rename Builds to Pipelines, CI/CD Pipelines, or Jobs everywhere -merge_request: 8787 -author: diff --git a/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml b/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml deleted file mode 100644 index 965d0648adf..00000000000 --- a/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don't require lib/gitlab/request_profiler/middleware.rb in config/initializers/request_profiler.rb -merge_request: -author: diff --git a/changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml b/changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml deleted file mode 100644 index eda872049fd..00000000000 --- a/changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Added labels empty state -merge_request: 7443 -author: diff --git a/changelogs/unreleased/21518_recaptcha_spam_issues.yml b/changelogs/unreleased/21518_recaptcha_spam_issues.yml deleted file mode 100644 index bd6c9d7521e..00000000000 --- a/changelogs/unreleased/21518_recaptcha_spam_issues.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use reCaptcha when an issue is identified as a spam -merge_request: 8846 -author: diff --git a/changelogs/unreleased/22007-unify-projects-search.yml b/changelogs/unreleased/22007-unify-projects-search.yml deleted file mode 100644 index f43c1925ad0..00000000000 --- a/changelogs/unreleased/22007-unify-projects-search.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Unify projects search by removing /projects/:search endpoint -merge_request: 8877 -author: diff --git a/changelogs/unreleased/22638-creating-a-branch-matching-a-wildcard-fails.yml b/changelogs/unreleased/22638-creating-a-branch-matching-a-wildcard-fails.yml deleted file mode 100644 index 2c6883bcf7b..00000000000 --- a/changelogs/unreleased/22638-creating-a-branch-matching-a-wildcard-fails.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow creating protected branches when user can merge to such branch -merge_request: 8458 -author: diff --git a/changelogs/unreleased/22974-trigger-service-events-through-api.yml b/changelogs/unreleased/22974-trigger-service-events-through-api.yml deleted file mode 100644 index 57106e8c676..00000000000 --- a/changelogs/unreleased/22974-trigger-service-events-through-api.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Adds service trigger events to api -merge_request: 8324 -author: diff --git a/changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml b/changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml deleted file mode 100644 index 268be6b9b83..00000000000 --- a/changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Create a TODO for user who set auto-merge when a build fails, merge conflict occurs -merge_request: 8056 -author: twonegatives diff --git a/changelogs/unreleased/23634-remove-project-grouping.yml b/changelogs/unreleased/23634-remove-project-grouping.yml deleted file mode 100644 index dde8b2d1815..00000000000 --- a/changelogs/unreleased/23634-remove-project-grouping.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don't group issues by project on group-level and dashboard issue indexes. -merge_request: 8111 -author: Bernardo Castro diff --git a/changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml b/changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml deleted file mode 100644 index 587ef4f9a73..00000000000 --- a/changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix disable storing of sensitive information when importing a new repo -merge_request: 8885 -author: Bernard Pietraga diff --git a/changelogs/unreleased/24147-delete-env-button.yml b/changelogs/unreleased/24147-delete-env-button.yml deleted file mode 100644 index 14e80cacbfb..00000000000 --- a/changelogs/unreleased/24147-delete-env-button.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Adds back ability to stop all environments -merge_request: 7379 -author: diff --git a/changelogs/unreleased/24606-force-password-reset-on-next-login.yml b/changelogs/unreleased/24606-force-password-reset-on-next-login.yml deleted file mode 100644 index fd671d04a9f..00000000000 --- a/changelogs/unreleased/24606-force-password-reset-on-next-login.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Force new password after password reset via API -merge_request: -author: George Andrinopoulos diff --git a/changelogs/unreleased/24716-fix-ctrl-click-links.yml b/changelogs/unreleased/24716-fix-ctrl-click-links.yml deleted file mode 100644 index 13de5db5e41..00000000000 --- a/changelogs/unreleased/24716-fix-ctrl-click-links.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix Ctrl+Click support for Todos and Merge Request page tabs -merge_request: 8898 -author: diff --git a/changelogs/unreleased/24795_refactor_merge_request_build_service.yml b/changelogs/unreleased/24795_refactor_merge_request_build_service.yml deleted file mode 100644 index b735fb57649..00000000000 --- a/changelogs/unreleased/24795_refactor_merge_request_build_service.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Refactor MergeRequests::BuildService -merge_request: 8462 -author: Rydkin Maxim diff --git a/changelogs/unreleased/24833-Allow-to-search-by-commit-hash-within-project.yml b/changelogs/unreleased/24833-Allow-to-search-by-commit-hash-within-project.yml deleted file mode 100644 index be66c370f36..00000000000 --- a/changelogs/unreleased/24833-Allow-to-search-by-commit-hash-within-project.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Allows to search within project by commit hash' -merge_request: -author: YarNayar diff --git a/changelogs/unreleased/24923_nested_tasks.yml b/changelogs/unreleased/24923_nested_tasks.yml deleted file mode 100644 index de35cad3dd6..00000000000 --- a/changelogs/unreleased/24923_nested_tasks.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix nested tasks in ordered list -merge_request: 8626 -author: diff --git a/changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml b/changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml deleted file mode 100644 index d35ad0be0db..00000000000 --- a/changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Show organisation membership and delete comment on smaller viewports, plus change comment author name to username -merge_request: -author: diff --git a/changelogs/unreleased/25312-search-input-cmd-click-issue.yml b/changelogs/unreleased/25312-search-input-cmd-click-issue.yml deleted file mode 100644 index 56e03a48692..00000000000 --- a/changelogs/unreleased/25312-search-input-cmd-click-issue.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Prevent removal of input fields if it is the parent dropdown element -merge_request: 8397 -author: diff --git a/changelogs/unreleased/25360-remove-flash-warning-from-login-page.yml b/changelogs/unreleased/25360-remove-flash-warning-from-login-page.yml deleted file mode 100644 index 50a5c879446..00000000000 --- a/changelogs/unreleased/25360-remove-flash-warning-from-login-page.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove flash warning from login page -merge_request: 8864 -author: Gerald J. Padilla diff --git a/changelogs/unreleased/25460-replace-word-users-with-members.yml b/changelogs/unreleased/25460-replace-word-users-with-members.yml deleted file mode 100644 index dac90eaa34d..00000000000 --- a/changelogs/unreleased/25460-replace-word-users-with-members.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace word user with member -merge_request: 8872 -author: diff --git a/changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml b/changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml deleted file mode 100644 index d7f950d7be9..00000000000 --- a/changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove turbolinks. -merge_request: !8570 -author: diff --git a/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml b/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml deleted file mode 100644 index f74e9fa8b6d..00000000000 --- a/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Update pipeline and commit links when CI status is updated -merge_request: 8351 -author: diff --git a/changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml b/changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml deleted file mode 100644 index 9506692dd40..00000000000 --- a/changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Convert pipeline action icons to svg to have them propperly positioned -merge_request: -author: diff --git a/changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml b/changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml deleted file mode 100644 index e67a9c0da15..00000000000 --- a/changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove rogue scrollbars for issue comments with inline elements -merge_request: -author: diff --git a/changelogs/unreleased/26059-segoe-ui-vertical.yml b/changelogs/unreleased/26059-segoe-ui-vertical.yml deleted file mode 100644 index fc3f1af5b61..00000000000 --- a/changelogs/unreleased/26059-segoe-ui-vertical.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Align Segoe UI label text -merge_request: -author: diff --git a/changelogs/unreleased/26068_tasklist_issue.yml b/changelogs/unreleased/26068_tasklist_issue.yml deleted file mode 100644 index c938351b8a7..00000000000 --- a/changelogs/unreleased/26068_tasklist_issue.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don’t count tasks that are not defined as list items correctly -merge_request: 8526 -author: diff --git a/changelogs/unreleased/26117-sort-pipeline-for-commit.yml b/changelogs/unreleased/26117-sort-pipeline-for-commit.yml deleted file mode 100644 index b2f5294d380..00000000000 --- a/changelogs/unreleased/26117-sort-pipeline-for-commit.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add sorting pipeline for a commit -merge_request: 8319 -author: Takuya Noguchi diff --git a/changelogs/unreleased/26186-diff-view-plus-and-minus-signs-as-part-of-line-number.yml b/changelogs/unreleased/26186-diff-view-plus-and-minus-signs-as-part-of-line-number.yml deleted file mode 100644 index 565672917b2..00000000000 --- a/changelogs/unreleased/26186-diff-view-plus-and-minus-signs-as-part-of-line-number.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Color + and - signs in diffs to increase code legibility -merge_request: -author: diff --git a/changelogs/unreleased/26445-accessible-piplelines-buttons.yml b/changelogs/unreleased/26445-accessible-piplelines-buttons.yml deleted file mode 100644 index fb5274e5253..00000000000 --- a/changelogs/unreleased/26445-accessible-piplelines-buttons.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Improve button accessibility on pipelines page -merge_request: 8561 -author: diff --git a/changelogs/unreleased/26447-fix-tab-list-order.yml b/changelogs/unreleased/26447-fix-tab-list-order.yml deleted file mode 100644 index 351c53bd076..00000000000 --- a/changelogs/unreleased/26447-fix-tab-list-order.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix tab index order on branch commits list page -merge_request: -author: Ryan Harris diff --git a/changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml b/changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml deleted file mode 100644 index 87ae8233c4a..00000000000 --- a/changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix Sort by Recent Sign-in in Admin Area -merge_request: 8637 -author: Poornima M diff --git a/changelogs/unreleased/26787-add-copy-icon-hover-state.yml b/changelogs/unreleased/26787-add-copy-icon-hover-state.yml deleted file mode 100644 index 31f1812c6f8..00000000000 --- a/changelogs/unreleased/26787-add-copy-icon-hover-state.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add hover style to copy icon on commit page header -merge_request: -author: Ryan Harris diff --git a/changelogs/unreleased/26824-diff-unfold-link-is-still-visible-when-there-are-no-lines-to-unfold.yml b/changelogs/unreleased/26824-diff-unfold-link-is-still-visible-when-there-are-no-lines-to-unfold.yml deleted file mode 100644 index 182a9ae126b..00000000000 --- a/changelogs/unreleased/26824-diff-unfold-link-is-still-visible-when-there-are-no-lines-to-unfold.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: prevent diff unfolding link from appearing when there are no more lines to - show -merge_request: 8761 -author: diff --git a/changelogs/unreleased/26852-fix-slug-for-openshift.yml b/changelogs/unreleased/26852-fix-slug-for-openshift.yml deleted file mode 100644 index fb65b068b23..00000000000 --- a/changelogs/unreleased/26852-fix-slug-for-openshift.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Avoid repeated dashes in $CI_ENVIRONMENT_SLUG -merge_request: 8638 -author: diff --git a/changelogs/unreleased/26863-Remove-hover-animation-from-row-elements.yml b/changelogs/unreleased/26863-Remove-hover-animation-from-row-elements.yml deleted file mode 100644 index 8dfabf87c2a..00000000000 --- a/changelogs/unreleased/26863-Remove-hover-animation-from-row-elements.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove hover animation from row elements -merge_request: -author: diff --git a/changelogs/unreleased/26881-backup-fails-if-data-changes.yml b/changelogs/unreleased/26881-backup-fails-if-data-changes.yml deleted file mode 100644 index 00bf105560b..00000000000 --- a/changelogs/unreleased/26881-backup-fails-if-data-changes.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add `copy` backup strategy to combat file changed errors -merge_request: 8728 -author: diff --git a/changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml b/changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml deleted file mode 100644 index ea567437ac2..00000000000 --- a/changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixes hover cursor on pipeline pagenation -merge_request: 9003 -author: diff --git a/changelogs/unreleased/26947-build-status-self-link.yml b/changelogs/unreleased/26947-build-status-self-link.yml deleted file mode 100644 index 15c5821874e..00000000000 --- a/changelogs/unreleased/26947-build-status-self-link.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add link verification to badge partial in order to render a badge without a link -merge_request: 8740 -author: diff --git a/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml b/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml deleted file mode 100644 index c5c57af5aaf..00000000000 --- a/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Improve pipeline status icon linking in widgets -merge_request: -author: diff --git a/changelogs/unreleased/27013-regression-in-commit-title-bar.yml b/changelogs/unreleased/27013-regression-in-commit-title-bar.yml deleted file mode 100644 index 7cb5e4b273d..00000000000 --- a/changelogs/unreleased/27013-regression-in-commit-title-bar.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix commit title bar and repository view copy clipboard button order on last commit in repository view -merge_request: -author: diff --git a/changelogs/unreleased/27014-fix-pipeline-tooltip-wrapping.yml b/changelogs/unreleased/27014-fix-pipeline-tooltip-wrapping.yml deleted file mode 100644 index f0301c849b6..00000000000 --- a/changelogs/unreleased/27014-fix-pipeline-tooltip-wrapping.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix mini-pipeline stage tooltip text wrapping -merge_request: -author: diff --git a/changelogs/unreleased/27021-line-numbers-now-in-copy-pasta-data.yml b/changelogs/unreleased/27021-line-numbers-now-in-copy-pasta-data.yml deleted file mode 100644 index b5584749098..00000000000 --- a/changelogs/unreleased/27021-line-numbers-now-in-copy-pasta-data.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Prevent copying of line numbers in parallel diff view -merge_request: 8706 -author: diff --git a/changelogs/unreleased/27178-update-builds-link-in-project-settings.yml b/changelogs/unreleased/27178-update-builds-link-in-project-settings.yml deleted file mode 100644 index 52406bba464..00000000000 --- a/changelogs/unreleased/27178-update-builds-link-in-project-settings.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Updated builds info link on the project settings page -merge_request: -author: Ryan Harris diff --git a/changelogs/unreleased/27240-make-progress-bars-consistent.yml b/changelogs/unreleased/27240-make-progress-bars-consistent.yml deleted file mode 100644 index 3f902fb324e..00000000000 --- a/changelogs/unreleased/27240-make-progress-bars-consistent.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 27240 Make progress bars consistent -merge_request: -author: diff --git a/changelogs/unreleased/27277-small-mini-pipeline-graph-glitch-upon-hover.yml b/changelogs/unreleased/27277-small-mini-pipeline-graph-glitch-upon-hover.yml deleted file mode 100644 index 9456251025b..00000000000 --- a/changelogs/unreleased/27277-small-mini-pipeline-graph-glitch-upon-hover.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: fixed small mini pipeline graph line glitch -merge_request: 8804 -author: diff --git a/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml b/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml deleted file mode 100644 index 293aab67d39..00000000000 --- a/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Unify MR diff file button style -merge_request: 8874 -author: diff --git a/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml b/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml deleted file mode 100644 index 502927cd160..00000000000 --- a/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Only render hr when user can't archive project. -merge_request: !8917 -author: diff --git a/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml b/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml deleted file mode 100644 index 79316abbaf7..00000000000 --- a/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix pipeline graph vertical spacing in Firefox and Safari -merge_request: 8886 -author: diff --git a/changelogs/unreleased/27352-search-label-filter-header.yml b/changelogs/unreleased/27352-search-label-filter-header.yml deleted file mode 100644 index 191b530aee8..00000000000 --- a/changelogs/unreleased/27352-search-label-filter-header.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 27352-search-label-filter-header -merge_request: -author: diff --git a/changelogs/unreleased/27395-reduce-group-activity-sql-queries-2.yml b/changelogs/unreleased/27395-reduce-group-activity-sql-queries-2.yml deleted file mode 100644 index f3ce1709518..00000000000 --- a/changelogs/unreleased/27395-reduce-group-activity-sql-queries-2.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Include :author, :project, and :target in Event.with_associations -merge_request: -author: diff --git a/changelogs/unreleased/27395-reduce-group-activity-sql-queries.yml b/changelogs/unreleased/27395-reduce-group-activity-sql-queries.yml deleted file mode 100644 index 3f6d922f2a0..00000000000 --- a/changelogs/unreleased/27395-reduce-group-activity-sql-queries.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don't instantiate AR objects in Event.in_projects -merge_request: -author: diff --git a/changelogs/unreleased/27484-environment-show-name.yml b/changelogs/unreleased/27484-environment-show-name.yml deleted file mode 100644 index dc400d65006..00000000000 --- a/changelogs/unreleased/27484-environment-show-name.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don't capitalize environment name in show page -merge_request: -author: diff --git a/changelogs/unreleased/27488-fix-jwt-version.yml b/changelogs/unreleased/27488-fix-jwt-version.yml deleted file mode 100644 index 5135ff0fd60..00000000000 --- a/changelogs/unreleased/27488-fix-jwt-version.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Update and pin the `jwt` gem to ~> 1.5.6 -merge_request: -author: diff --git a/changelogs/unreleased/27494-environment-list-column-headers.yml b/changelogs/unreleased/27494-environment-list-column-headers.yml deleted file mode 100644 index 798c01f3238..00000000000 --- a/changelogs/unreleased/27494-environment-list-column-headers.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Edited the column header for the environments list from created to updated and added created to environments detail page colum header titles -merge_request: -author: diff --git a/changelogs/unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml b/changelogs/unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml deleted file mode 100644 index a5bb37ec8a9..00000000000 --- a/changelogs/unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixes flickering of avatar border in mention dropdown -merge_request: 8950 -author: diff --git a/changelogs/unreleased/27632_fix_mr_widget_url.yml b/changelogs/unreleased/27632_fix_mr_widget_url.yml deleted file mode 100644 index 958621a43a1..00000000000 --- a/changelogs/unreleased/27632_fix_mr_widget_url.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix MR widget url -merge_request: 8989 -author: diff --git a/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml b/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml deleted file mode 100644 index 0531ef2c038..00000000000 --- a/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Layer award emoji dropdown over the right sidebar -merge_request: 9004 -author: diff --git a/changelogs/unreleased/27656-doc-ci-enable-ci.yml b/changelogs/unreleased/27656-doc-ci-enable-ci.yml deleted file mode 100644 index e6315d683d4..00000000000 --- a/changelogs/unreleased/27656-doc-ci-enable-ci.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Update doc for enabling or disabling GitLab CI -merge_request: 8965 -author: Takuya Noguchi diff --git a/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml b/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml deleted file mode 100644 index aa89d9f9850..00000000000 --- a/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Give ci status text on pipeline graph a better font-weight -merge_request: -author: diff --git a/changelogs/unreleased/27822-default-bulk-assign-labels.yml b/changelogs/unreleased/27822-default-bulk-assign-labels.yml deleted file mode 100644 index ee2431869f0..00000000000 --- a/changelogs/unreleased/27822-default-bulk-assign-labels.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add default labels to bulk assign dropdowns -merge_request: -author: diff --git a/changelogs/unreleased/27873-when-a-commit-appears-in-several-projects-commit-comments-are-shared-across-projects.yml b/changelogs/unreleased/27873-when-a-commit-appears-in-several-projects-commit-comments-are-shared-across-projects.yml deleted file mode 100644 index 89e2bdc69bc..00000000000 --- a/changelogs/unreleased/27873-when-a-commit-appears-in-several-projects-commit-comments-are-shared-across-projects.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Only return target project's comments for a commit -merge_request: -author: diff --git a/changelogs/unreleased/27880-pipelines-table-not-showing-commit-branch.yml b/changelogs/unreleased/27880-pipelines-table-not-showing-commit-branch.yml deleted file mode 100644 index 4251754618b..00000000000 --- a/changelogs/unreleased/27880-pipelines-table-not-showing-commit-branch.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixes Pipelines table is not showing branch name for commit -merge_request: -author: diff --git a/changelogs/unreleased/27883-autocomplete-seems-to-not-trigger-when-at-character-is-part-of-an-autocompleted-text.yml b/changelogs/unreleased/27883-autocomplete-seems-to-not-trigger-when-at-character-is-part-of-an-autocompleted-text.yml deleted file mode 100644 index 52b7e96682d..00000000000 --- a/changelogs/unreleased/27883-autocomplete-seems-to-not-trigger-when-at-character-is-part-of-an-autocompleted-text.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Trigger autocomplete after selecting a slash command -merge_request: 9117 -author: diff --git a/changelogs/unreleased/27922-cmd-click-todo-doesn-t-work.yml b/changelogs/unreleased/27922-cmd-click-todo-doesn-t-work.yml deleted file mode 100644 index 79a54429ee8..00000000000 --- a/changelogs/unreleased/27922-cmd-click-todo-doesn-t-work.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix regression where cmd-click stopped working for todos and merge request - tabs -merge_request: -author: diff --git a/changelogs/unreleased/27925-fix-mr-stray-pipelines-api-request.yml b/changelogs/unreleased/27925-fix-mr-stray-pipelines-api-request.yml deleted file mode 100644 index f7bdb62b7f3..00000000000 --- a/changelogs/unreleased/27925-fix-mr-stray-pipelines-api-request.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix stray pipelines API request when showing MR -merge_request: -author: diff --git a/changelogs/unreleased/27932-merge-request-pipelines-displays-json.yml b/changelogs/unreleased/27932-merge-request-pipelines-displays-json.yml deleted file mode 100644 index b7505e28401..00000000000 --- a/changelogs/unreleased/27932-merge-request-pipelines-displays-json.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix Merge request pipelines displays JSON -merge_request: -author: diff --git a/changelogs/unreleased/27939-fix-current-build-arrow.yml b/changelogs/unreleased/27939-fix-current-build-arrow.yml deleted file mode 100644 index 280ab090f2c..00000000000 --- a/changelogs/unreleased/27939-fix-current-build-arrow.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix current build arrow indicator -merge_request: -author: diff --git a/changelogs/unreleased/27943-contribution-list-on-profile-page-is-aligned-right.yml b/changelogs/unreleased/27943-contribution-list-on-profile-page-is-aligned-right.yml deleted file mode 100644 index fcbd48b0357..00000000000 --- a/changelogs/unreleased/27943-contribution-list-on-profile-page-is-aligned-right.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix contribution activity alignment -merge_request: -author: diff --git a/changelogs/unreleased/27947-missing-margin-between-loading-icon-and-text-in-merge-request-widget.yml b/changelogs/unreleased/27947-missing-margin-between-loading-icon-and-text-in-merge-request-widget.yml deleted file mode 100644 index 1dfabd3813b..00000000000 --- a/changelogs/unreleased/27947-missing-margin-between-loading-icon-and-text-in-merge-request-widget.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add space between text and loading icon in Megre Request Widget -merge_request: 9119 -author: diff --git a/changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml b/changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml deleted file mode 100644 index d9f78db4bec..00000000000 --- a/changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Show Pipeline(not Job) in MR desktop notification -merge_request: -author: diff --git a/changelogs/unreleased/27963-tooltips-jobs.yml b/changelogs/unreleased/27963-tooltips-jobs.yml deleted file mode 100644 index ba418d86433..00000000000 --- a/changelogs/unreleased/27963-tooltips-jobs.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix tooltips in mini pipeline graph -merge_request: -author: diff --git a/changelogs/unreleased/27966-branch-ref-switcher-input-filter-broken.yml b/changelogs/unreleased/27966-branch-ref-switcher-input-filter-broken.yml deleted file mode 100644 index 6fa13395a7d..00000000000 --- a/changelogs/unreleased/27966-branch-ref-switcher-input-filter-broken.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Display loading indicator when filtering ref switcher dropdown -merge_request: -author: diff --git a/changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml b/changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml deleted file mode 100644 index e4287d6276c..00000000000 --- a/changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Show pipeline graph in MR widget if there are any stages -merge_request: -author: diff --git a/changelogs/unreleased/27991-success-with-warnings-caret.yml b/changelogs/unreleased/27991-success-with-warnings-caret.yml deleted file mode 100644 index 703d34a5ede..00000000000 --- a/changelogs/unreleased/27991-success-with-warnings-caret.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix icon colors in merge request widget mini graph -merge_request: -author: diff --git a/changelogs/unreleased/28029-improve-blockquote-formatting-on-emails.yml b/changelogs/unreleased/28029-improve-blockquote-formatting-on-emails.yml deleted file mode 100644 index be2a0afbc52..00000000000 --- a/changelogs/unreleased/28029-improve-blockquote-formatting-on-emails.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Improve blockquote formatting in notification emails -merge_request: -author: diff --git a/changelogs/unreleased/28032-tooltips-file-name.yml b/changelogs/unreleased/28032-tooltips-file-name.yml deleted file mode 100644 index 9fe11e7c2b6..00000000000 --- a/changelogs/unreleased/28032-tooltips-file-name.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Adds container to tooltip in order to make it work with overflow:hidden in - parent element -merge_request: -author: diff --git a/changelogs/unreleased/28059-add-pagination-to-admin-abuse-reports.yml b/changelogs/unreleased/28059-add-pagination-to-admin-abuse-reports.yml deleted file mode 100644 index 1b2e678bbed..00000000000 --- a/changelogs/unreleased/28059-add-pagination-to-admin-abuse-reports.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Restore pagination to admin abuse reports -merge_request: -author: diff --git a/changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml b/changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml deleted file mode 100644 index 11d1f55172b..00000000000 --- a/changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix notifications when set at group level -merge_request: 6813 -author: Alexandre Maia diff --git a/changelogs/unreleased/8-15-stable.yml b/changelogs/unreleased/8-15-stable.yml deleted file mode 100644 index 75502e139e7..00000000000 --- a/changelogs/unreleased/8-15-stable.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Ensure export files are removed after a namespace is deleted -merge_request: -author: diff --git a/changelogs/unreleased/8082-permalink-to-file.yml b/changelogs/unreleased/8082-permalink-to-file.yml deleted file mode 100644 index 136d2108c63..00000000000 --- a/changelogs/unreleased/8082-permalink-to-file.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add `y` keyboard shortcut to move to file permalink -merge_request: -author: diff --git a/changelogs/unreleased/9-0-api-changes.yml b/changelogs/unreleased/9-0-api-changes.yml deleted file mode 100644 index 2f0f1887257..00000000000 --- a/changelogs/unreleased/9-0-api-changes.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove deprecated MR and Issue endpoints and preserve V3 namespace -merge_request: 8967 -author: diff --git a/changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml b/changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml deleted file mode 100644 index 9fd6ea5bc52..00000000000 --- a/changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Adds /target_branch slash command functionality for merge requests -merge_request: -author: YarNayar diff --git a/changelogs/unreleased/add_project_update_hook.yml b/changelogs/unreleased/add_project_update_hook.yml deleted file mode 100644 index 915c9538843..00000000000 --- a/changelogs/unreleased/add_project_update_hook.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add system hook for when a project is updated (other than rename/transfer) -merge_request: 5711 -author: Tommy Beadle diff --git a/changelogs/unreleased/api-remove-snippets-expires-at.yml b/changelogs/unreleased/api-remove-snippets-expires-at.yml deleted file mode 100644 index 67603bfab3b..00000000000 --- a/changelogs/unreleased/api-remove-snippets-expires-at.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: Remove deprecated ''expires_at'' from project snippets' -merge_request: 8723 -author: Robert Schilling diff --git a/changelogs/unreleased/babel-all-the-things.yml b/changelogs/unreleased/babel-all-the-things.yml deleted file mode 100644 index fda1c3bd562..00000000000 --- a/changelogs/unreleased/babel-all-the-things.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: use babel to transpile all non-vendor javascript assets regardless of file - extension -merge_request: 8988 -author: diff --git a/changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml b/changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml deleted file mode 100644 index 77750b55e7e..00000000000 --- a/changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Hide version check image if there is no internet connection -merge_request: 8355 -author: Ken Ding diff --git a/changelogs/unreleased/change_queue_weight.yml b/changelogs/unreleased/change_queue_weight.yml deleted file mode 100644 index e4c650e8f79..00000000000 --- a/changelogs/unreleased/change_queue_weight.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Increase process_commit queue weight from 2 to 3 -merge_request: 9326 -author: blackst0ne diff --git a/changelogs/unreleased/clipboard-button-commit-sha.yml b/changelogs/unreleased/clipboard-button-commit-sha.yml deleted file mode 100644 index 6aa4a5664e7..00000000000 --- a/changelogs/unreleased/clipboard-button-commit-sha.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -title: 'Copy commit SHA to clipboard' -merge_request: 8547 diff --git a/changelogs/unreleased/contribution-calendar-scroll.yml b/changelogs/unreleased/contribution-calendar-scroll.yml deleted file mode 100644 index a504d59e61c..00000000000 --- a/changelogs/unreleased/contribution-calendar-scroll.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: contribution calendar scrolls from right to left -merge_request: -author: diff --git a/changelogs/unreleased/cop-gem-fetcher.yml b/changelogs/unreleased/cop-gem-fetcher.yml deleted file mode 100644 index 506815a5b54..00000000000 --- a/changelogs/unreleased/cop-gem-fetcher.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Cop for gem fetched from a git source -merge_request: 8856 -author: Adam Pahlevi diff --git a/changelogs/unreleased/copy-as-md.yml b/changelogs/unreleased/copy-as-md.yml deleted file mode 100644 index 637e9dc36e2..00000000000 --- a/changelogs/unreleased/copy-as-md.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Copying a rendered issue/comment will paste into GFM textareas as actual GFM -merge_request: -author: diff --git a/changelogs/unreleased/disable-autologin-on-email-confirmation-links.yml b/changelogs/unreleased/disable-autologin-on-email-confirmation-links.yml deleted file mode 100644 index 6dd0d748001..00000000000 --- a/changelogs/unreleased/disable-autologin-on-email-confirmation-links.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Disable automatic login after clicking email confirmation links -merge_request: 7472 -author: diff --git a/changelogs/unreleased/display-project-id.yml b/changelogs/unreleased/display-project-id.yml deleted file mode 100644 index 8705ed28400..00000000000 --- a/changelogs/unreleased/display-project-id.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Display project ID in project settings -merge_request: 8572 -author: winniehell diff --git a/changelogs/unreleased/document-how-to-vue.yml b/changelogs/unreleased/document-how-to-vue.yml deleted file mode 100644 index 863e41b6413..00000000000 --- a/changelogs/unreleased/document-how-to-vue.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Adds documentation for how to use Vue.js -merge_request: 8866 -author: diff --git a/changelogs/unreleased/dynamic-todos-fixture.yml b/changelogs/unreleased/dynamic-todos-fixture.yml deleted file mode 100644 index 580bc729e3c..00000000000 --- a/changelogs/unreleased/dynamic-todos-fixture.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace static fixture for right_sidebar_spec.js -merge_request: 9211 -author: winniehell diff --git a/changelogs/unreleased/dz-nested-groups-improvements-2.yml b/changelogs/unreleased/dz-nested-groups-improvements-2.yml deleted file mode 100644 index 8e4eb7f1fff..00000000000 --- a/changelogs/unreleased/dz-nested-groups-improvements-2.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add read-only full_path and full_name attributes to Group API -merge_request: 8827 -author: diff --git a/changelogs/unreleased/empty-selection-reply-shortcut.yml b/changelogs/unreleased/empty-selection-reply-shortcut.yml deleted file mode 100644 index 5a42c98a800..00000000000 --- a/changelogs/unreleased/empty-selection-reply-shortcut.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Change the reply shortcut to focus the field even without a selection. -merge_request: 8873 -author: Brian Hall diff --git a/changelogs/unreleased/fe-commit-mr-pipelines.yml b/changelogs/unreleased/fe-commit-mr-pipelines.yml deleted file mode 100644 index b5cc6bbf8b6..00000000000 --- a/changelogs/unreleased/fe-commit-mr-pipelines.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use vue.js Pipelines table in commit and merge request view -merge_request: 8844 -author: diff --git a/changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml b/changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml deleted file mode 100644 index 5fba0332881..00000000000 --- a/changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use warning icon in mini-graph if stage passed conditionally -merge_request: 8503 -author: diff --git a/changelogs/unreleased/fix-27479.yml b/changelogs/unreleased/fix-27479.yml deleted file mode 100644 index cc72a830695..00000000000 --- a/changelogs/unreleased/fix-27479.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove new branch button for confidential issues -merge_request: -author: diff --git a/changelogs/unreleased/fix-api-mr-permissions.yml b/changelogs/unreleased/fix-api-mr-permissions.yml deleted file mode 100644 index 33b677b1f29..00000000000 --- a/changelogs/unreleased/fix-api-mr-permissions.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don't allow project guests to subscribe to merge requests through the API -merge_request: -author: Robert Schilling diff --git a/changelogs/unreleased/fix-ar-connection-leaks.yml b/changelogs/unreleased/fix-ar-connection-leaks.yml deleted file mode 100644 index 9da715560ad..00000000000 --- a/changelogs/unreleased/fix-ar-connection-leaks.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don't connect in Gitlab::Database.adapter_name -merge_request: -author: diff --git a/changelogs/unreleased/fix-ci-build-policy.yml b/changelogs/unreleased/fix-ci-build-policy.yml deleted file mode 100644 index 26003713ed4..00000000000 --- a/changelogs/unreleased/fix-ci-build-policy.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Improve build policy and access abilities -merge_request: 8711 -author: diff --git a/changelogs/unreleased/fix-deleting-project-again.yml b/changelogs/unreleased/fix-deleting-project-again.yml deleted file mode 100644 index e13215f22a7..00000000000 --- a/changelogs/unreleased/fix-deleting-project-again.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix deleting projects with pipelines and builds -merge_request: 8960 -author: diff --git a/changelogs/unreleased/fix-depr-warn.yml b/changelogs/unreleased/fix-depr-warn.yml deleted file mode 100644 index 61817027720..00000000000 --- a/changelogs/unreleased/fix-depr-warn.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: resolve deprecation warnings -merge_request: 8855 -author: Adam Pahlevi diff --git a/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml b/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml deleted file mode 100644 index df7e3776700..00000000000 --- a/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Preserve backward compatibility CI/CD and disallow setting `coverage` regexp in global context -merge_request: 8981 -author: diff --git a/changelogs/unreleased/fix-guest-access-posting-to-notes.yml b/changelogs/unreleased/fix-guest-access-posting-to-notes.yml deleted file mode 100644 index 81377c0c6f0..00000000000 --- a/changelogs/unreleased/fix-guest-access-posting-to-notes.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Prevent users from creating notes on resources they can't access -merge_request: -author: diff --git a/changelogs/unreleased/fix-import-encrypt-atts.yml b/changelogs/unreleased/fix-import-encrypt-atts.yml deleted file mode 100644 index e34d895570b..00000000000 --- a/changelogs/unreleased/fix-import-encrypt-atts.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Ignore encrypted attributes in Import/Export -merge_request: -author: diff --git a/changelogs/unreleased/fix-import-group-members.yml b/changelogs/unreleased/fix-import-group-members.yml deleted file mode 100644 index fe580af31b3..00000000000 --- a/changelogs/unreleased/fix-import-group-members.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add ability to export project inherited group members to Import/Export -merge_request: 8923 -author: diff --git a/changelogs/unreleased/fix-job-to-pipeline-renaming.yml b/changelogs/unreleased/fix-job-to-pipeline-renaming.yml deleted file mode 100644 index d5f34b4b25d..00000000000 --- a/changelogs/unreleased/fix-job-to-pipeline-renaming.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix job to pipeline renaming -merge_request: 9147 -author: diff --git a/changelogs/unreleased/fix-references-header-parsing.yml b/changelogs/unreleased/fix-references-header-parsing.yml deleted file mode 100644 index b927279cdf4..00000000000 --- a/changelogs/unreleased/fix-references-header-parsing.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix reply by email without sub-addressing for some clients from - Microsoft and Apple -merge_request: 8620 -author: diff --git a/changelogs/unreleased/fix-scroll-test.yml b/changelogs/unreleased/fix-scroll-test.yml deleted file mode 100644 index e98ac755b88..00000000000 --- a/changelogs/unreleased/fix-scroll-test.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Change rspec test to guarantee window is resized before visiting page -merge_request: -author: diff --git a/changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml b/changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml deleted file mode 100644 index c9edd1de86c..00000000000 --- a/changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Prevent users from deleting system deploy keys via the project deploy key API -merge_request: -author: diff --git a/changelogs/unreleased/fix_broken_diff_discussions.yml b/changelogs/unreleased/fix_broken_diff_discussions.yml deleted file mode 100644 index 4551212759f..00000000000 --- a/changelogs/unreleased/fix_broken_diff_discussions.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Make MR-review-discussions more reliable -merge_request: -author: diff --git a/changelogs/unreleased/fix_sidekiq_concurrency_warning_message_in_admin_background_job_page.yml b/changelogs/unreleased/fix_sidekiq_concurrency_warning_message_in_admin_background_job_page.yml deleted file mode 100644 index e09d03bb608..00000000000 --- a/changelogs/unreleased/fix_sidekiq_concurrency_warning_message_in_admin_background_job_page.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: fix incorrect sidekiq concurrency count in admin background page -merge_request: -author: wendy0402 diff --git a/changelogs/unreleased/fwn-to-find-by-full-path.yml b/changelogs/unreleased/fwn-to-find-by-full-path.yml deleted file mode 100644 index 1427e4e7624..00000000000 --- a/changelogs/unreleased/fwn-to-find-by-full-path.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: replace `find_with_namespace` with `find_by_full_path` -merge_request: 8949 -author: Adam Pahlevi diff --git a/changelogs/unreleased/get-rid-of-water-from-notification_service_spec-to-make-it-DRY.yml b/changelogs/unreleased/get-rid-of-water-from-notification_service_spec-to-make-it-DRY.yml deleted file mode 100644 index f60417d185e..00000000000 --- a/changelogs/unreleased/get-rid-of-water-from-notification_service_spec-to-make-it-DRY.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Make notification_service spec DRYer by making test reusable -merge_request: -author: YarNayar diff --git a/changelogs/unreleased/git_to_html_redirection.yml b/changelogs/unreleased/git_to_html_redirection.yml deleted file mode 100644 index b2959c02c07..00000000000 --- a/changelogs/unreleased/git_to_html_redirection.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Redirect http://someproject.git to http://someproject -merge_request: -author: blackst0ne diff --git a/changelogs/unreleased/go-go-gadget-webpack.yml b/changelogs/unreleased/go-go-gadget-webpack.yml deleted file mode 100644 index 7f372ccb428..00000000000 --- a/changelogs/unreleased/go-go-gadget-webpack.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: use webpack to bundle frontend assets and use karma for frontend testing -merge_request: 7288 -author: diff --git a/changelogs/unreleased/group-label-sidebar-link.yml b/changelogs/unreleased/group-label-sidebar-link.yml deleted file mode 100644 index c11c2d4ede1..00000000000 --- a/changelogs/unreleased/group-label-sidebar-link.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed group label links in issue/merge request sidebar -merge_request: -author: diff --git a/changelogs/unreleased/hardcode-title-system-note.yml b/changelogs/unreleased/hardcode-title-system-note.yml deleted file mode 100644 index 1b0a63efa51..00000000000 --- a/changelogs/unreleased/hardcode-title-system-note.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Ensure autogenerated title does not cause failing spec -merge_request: 8963 -author: brian m. carlson diff --git a/changelogs/unreleased/improve-ci-example-php-doc.yml b/changelogs/unreleased/improve-ci-example-php-doc.yml deleted file mode 100644 index 39a85e3d261..00000000000 --- a/changelogs/unreleased/improve-ci-example-php-doc.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Changed composer installer script in the CI PHP example doc -merge_request: 4342 -author: Jeffrey Cafferata diff --git a/changelogs/unreleased/improve-handleLocationHash-tests.yml b/changelogs/unreleased/improve-handleLocationHash-tests.yml deleted file mode 100644 index 8ae3dfe079c..00000000000 --- a/changelogs/unreleased/improve-handleLocationHash-tests.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Improve gl.utils.handleLocationHash tests -merge_request: -author: diff --git a/changelogs/unreleased/issuable-sidebar-bug.yml b/changelogs/unreleased/issuable-sidebar-bug.yml deleted file mode 100644 index 4086292eb89..00000000000 --- a/changelogs/unreleased/issuable-sidebar-bug.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed Issuable sidebar not closing on smaller/mobile sized screens -merge_request: -author: diff --git a/changelogs/unreleased/issue-20428.yml b/changelogs/unreleased/issue-20428.yml deleted file mode 100644 index 60da1c14702..00000000000 --- a/changelogs/unreleased/issue-20428.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add ability to define a coverage regex in the .gitlab-ci.yml -merge_request: 7447 -author: Leandro Camargo diff --git a/changelogs/unreleased/issue-sidebar-empty-assignee.yml b/changelogs/unreleased/issue-sidebar-empty-assignee.yml deleted file mode 100644 index 263af75b9e9..00000000000 --- a/changelogs/unreleased/issue-sidebar-empty-assignee.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Resets assignee dropdown when sidebar is open -merge_request: -author: diff --git a/changelogs/unreleased/issue_19262.yml b/changelogs/unreleased/issue_19262.yml deleted file mode 100644 index 5dea1493f23..00000000000 --- a/changelogs/unreleased/issue_19262.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Disallow system notes for closed issuables -merge_request: -author: diff --git a/changelogs/unreleased/issue_23317.yml b/changelogs/unreleased/issue_23317.yml deleted file mode 100644 index 788ae159f5e..00000000000 --- a/changelogs/unreleased/issue_23317.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix timezone on issue boards due date -merge_request: -author: diff --git a/changelogs/unreleased/issue_27211.yml b/changelogs/unreleased/issue_27211.yml deleted file mode 100644 index ad48fec5d85..00000000000 --- a/changelogs/unreleased/issue_27211.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove unused js response from refs controller -merge_request: -author: diff --git a/changelogs/unreleased/jej-pages-picked-from-ee.yml b/changelogs/unreleased/jej-pages-picked-from-ee.yml deleted file mode 100644 index ee4a43a93db..00000000000 --- a/changelogs/unreleased/jej-pages-picked-from-ee.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Added GitLab Pages to CE -merge_request: 8463 -author: diff --git a/changelogs/unreleased/label-promotion.yml b/changelogs/unreleased/label-promotion.yml deleted file mode 100644 index 2ab997bf420..00000000000 --- a/changelogs/unreleased/label-promotion.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "Project labels can now be promoted to group labels" -merge_request: 7242 -author: Olaf Tomalka diff --git a/changelogs/unreleased/lfs-noauth-public-repo.yml b/changelogs/unreleased/lfs-noauth-public-repo.yml deleted file mode 100644 index 60f62d7691b..00000000000 --- a/changelogs/unreleased/lfs-noauth-public-repo.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Support unauthenticated LFS object downloads for public projects -merge_request: 8824 -author: Ben Boeckel diff --git a/changelogs/unreleased/markdown-plantuml.yml b/changelogs/unreleased/markdown-plantuml.yml deleted file mode 100644 index c855f0cbcf7..00000000000 --- a/changelogs/unreleased/markdown-plantuml.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: PlantUML support for Markdown -merge_request: 8588 -author: Horacio Sanson diff --git a/changelogs/unreleased/merge-request-tabs-fixture.yml b/changelogs/unreleased/merge-request-tabs-fixture.yml deleted file mode 100644 index 289cd7b604a..00000000000 --- a/changelogs/unreleased/merge-request-tabs-fixture.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace static fixture for merge_request_tabs_spec.js -merge_request: 9172 -author: winniehell diff --git a/changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml b/changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml deleted file mode 100644 index f32b3aea3c8..00000000000 --- a/changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: adds avatar for discussion note -merge_request: 8734 -author: diff --git a/changelogs/unreleased/mr-tabs-container-offset.yml b/changelogs/unreleased/mr-tabs-container-offset.yml deleted file mode 100644 index c5df8abfcf2..00000000000 --- a/changelogs/unreleased/mr-tabs-container-offset.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed merge requests tab extra margin when fixed to window -merge_request: -author: diff --git a/changelogs/unreleased/newline-eslint-rule.yml b/changelogs/unreleased/newline-eslint-rule.yml deleted file mode 100644 index 5ce080b6912..00000000000 --- a/changelogs/unreleased/newline-eslint-rule.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Flag multiple empty lines in eslint, fix offenses. -merge_request: 8137 -author: diff --git a/changelogs/unreleased/no-sidebar-on-action-btn-click.yml b/changelogs/unreleased/no-sidebar-on-action-btn-click.yml deleted file mode 100644 index 09e0b3a12d8..00000000000 --- a/changelogs/unreleased/no-sidebar-on-action-btn-click.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: dismiss sidebar on repo buttons click -merge_request: 8798 -author: Adam Pahlevi diff --git a/changelogs/unreleased/no_project_notes.yml b/changelogs/unreleased/no_project_notes.yml deleted file mode 100644 index 6106c027360..00000000000 --- a/changelogs/unreleased/no_project_notes.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Support notes when a project is not specified (personal snippet notes) -merge_request: 8468 -author: diff --git a/changelogs/unreleased/pms-lowercase-system-notes.yml b/changelogs/unreleased/pms-lowercase-system-notes.yml deleted file mode 100644 index c2fa1a7fad0..00000000000 --- a/changelogs/unreleased/pms-lowercase-system-notes.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Make all system notes lowercase -merge_request: 8807 -author: diff --git a/changelogs/unreleased/redesign-searchbar-admin-project-26794.yml b/changelogs/unreleased/redesign-searchbar-admin-project-26794.yml deleted file mode 100644 index 547a7c6755c..00000000000 --- a/changelogs/unreleased/redesign-searchbar-admin-project-26794.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Redesign searchbar in admin project list -merge_request: 8776 -author: diff --git a/changelogs/unreleased/redirect-to-commit-when-only-commit-found.yml b/changelogs/unreleased/redirect-to-commit-when-only-commit-found.yml deleted file mode 100644 index e0f7e11b6d1..00000000000 --- a/changelogs/unreleased/redirect-to-commit-when-only-commit-found.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'Search feature: redirects to commit page if query is commit sha and only commit - found' -merge_request: 8028 -author: YarNayar diff --git a/changelogs/unreleased/relative-url-assets.yml b/changelogs/unreleased/relative-url-assets.yml deleted file mode 100644 index 0877664aca4..00000000000 --- a/changelogs/unreleased/relative-url-assets.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: allow relative url change without recompiling frontend assets -merge_request: 8831 -author: diff --git a/changelogs/unreleased/remove-deploy-key-endpoint.yml b/changelogs/unreleased/remove-deploy-key-endpoint.yml deleted file mode 100644 index 3ff69adb4d3..00000000000 --- a/changelogs/unreleased/remove-deploy-key-endpoint.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: Remove /projects/:id/keys/.. endpoints' -merge_request: 8716 -author: Robert Schilling diff --git a/changelogs/unreleased/remove-issue-and-mr-counts-from-labels-page.yml b/changelogs/unreleased/remove-issue-and-mr-counts-from-labels-page.yml deleted file mode 100644 index b75b4644ba3..00000000000 --- a/changelogs/unreleased/remove-issue-and-mr-counts-from-labels-page.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove issue and MR counts from labels index -merge_request: -author: diff --git a/changelogs/unreleased/route-map.yml b/changelogs/unreleased/route-map.yml deleted file mode 100644 index 9b6df0c54af..00000000000 --- a/changelogs/unreleased/route-map.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add 'View on [env]' link to blobs and individual files in diffs -merge_request: 8867 -author: diff --git a/changelogs/unreleased/rs-warden-blocked-users.yml b/changelogs/unreleased/rs-warden-blocked-users.yml deleted file mode 100644 index c0c23fb6f11..00000000000 --- a/changelogs/unreleased/rs-warden-blocked-users.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don't perform Devise trackable updates on blocked User records -merge_request: 8915 -author: diff --git a/changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml b/changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml deleted file mode 100644 index bab76812a17..00000000000 --- a/changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add index to ci_trigger_requests for commit_id -merge_request: -author: diff --git a/changelogs/unreleased/sh-add-labels-index.yml b/changelogs/unreleased/sh-add-labels-index.yml deleted file mode 100644 index b948a75081c..00000000000 --- a/changelogs/unreleased/sh-add-labels-index.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add indices to improve loading of labels page -merge_request: -author: diff --git a/changelogs/unreleased/slash-commands-typo.yml b/changelogs/unreleased/slash-commands-typo.yml deleted file mode 100644 index e6ffb94bd08..00000000000 --- a/changelogs/unreleased/slash-commands-typo.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed "substract" typo on /help/user/project/slash_commands -merge_request: 8976 -author: Jason Aquino diff --git a/changelogs/unreleased/small-screen-fullscreen-button.yml b/changelogs/unreleased/small-screen-fullscreen-button.yml deleted file mode 100644 index f4c269bc473..00000000000 --- a/changelogs/unreleased/small-screen-fullscreen-button.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Display fullscreen button on small screens -merge_request: 5302 -author: winniehell diff --git a/changelogs/unreleased/snippets-search-performance.yml b/changelogs/unreleased/snippets-search-performance.yml deleted file mode 100644 index 2895478abfd..00000000000 --- a/changelogs/unreleased/snippets-search-performance.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Reduced query count for snippet search -merge_request: -author: diff --git a/changelogs/unreleased/tc-only-mr-button-if-allowed.yml b/changelogs/unreleased/tc-only-mr-button-if-allowed.yml deleted file mode 100644 index a7f5dcb560c..00000000000 --- a/changelogs/unreleased/tc-only-mr-button-if-allowed.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Only show Merge Request button when user can create a MR -merge_request: 8639 -author: diff --git a/changelogs/unreleased/terminal-max-session-time.yml b/changelogs/unreleased/terminal-max-session-time.yml deleted file mode 100644 index db1e66770d1..00000000000 --- a/changelogs/unreleased/terminal-max-session-time.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Introduce maximum session time for terminal websocket connection -merge_request: 8413 -author: diff --git a/changelogs/unreleased/updated-pages-0-3-1.yml b/changelogs/unreleased/updated-pages-0-3-1.yml deleted file mode 100644 index 8622b823c86..00000000000 --- a/changelogs/unreleased/updated-pages-0-3-1.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Update GitLab Pages to v0.3.1 -merge_request: -author: diff --git a/changelogs/unreleased/upgrade-babel-v6.yml b/changelogs/unreleased/upgrade-babel-v6.yml deleted file mode 100644 index 55f9b3e407c..00000000000 --- a/changelogs/unreleased/upgrade-babel-v6.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: upgrade babel 5.8.x to babel 6.22.x -merge_request: 9072 -author: diff --git a/changelogs/unreleased/upgrade-omniauth.yml b/changelogs/unreleased/upgrade-omniauth.yml deleted file mode 100644 index 7e0334566dc..00000000000 --- a/changelogs/unreleased/upgrade-omniauth.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Upgrade omniauth gem to 1.3.2 -merge_request: -author: diff --git a/changelogs/unreleased/upgrade-webpack-v2-2.yml b/changelogs/unreleased/upgrade-webpack-v2-2.yml deleted file mode 100644 index 6a49859d68c..00000000000 --- a/changelogs/unreleased/upgrade-webpack-v2-2.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: upgrade to webpack v2.2 -merge_request: 9078 -author: diff --git a/changelogs/unreleased/wip-mr-from-commits.yml b/changelogs/unreleased/wip-mr-from-commits.yml deleted file mode 100644 index 0083798be08..00000000000 --- a/changelogs/unreleased/wip-mr-from-commits.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Mark MR as WIP when pushing WIP commits -merge_request: 8124 -author: Jurre Stender @jurre diff --git a/changelogs/unreleased/zj-format-chat-messages.yml b/changelogs/unreleased/zj-format-chat-messages.yml deleted file mode 100644 index 2494884f5c9..00000000000 --- a/changelogs/unreleased/zj-format-chat-messages.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Reformat messages ChatOps -merge_request: 8528 -author: diff --git a/changelogs/unreleased/zj-remove-deprecated-ci-service.yml b/changelogs/unreleased/zj-remove-deprecated-ci-service.yml deleted file mode 100644 index 044f4ae627d..00000000000 --- a/changelogs/unreleased/zj-remove-deprecated-ci-service.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove deprecated GitlabCiService -merge_request: -author: diff --git a/changelogs/unreleased/zj-requeue-pending-delete.yml b/changelogs/unreleased/zj-requeue-pending-delete.yml deleted file mode 100644 index 464c5948f8c..00000000000 --- a/changelogs/unreleased/zj-requeue-pending-delete.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Requeue pending deletion projects -merge_request: -author: -- cgit v1.2.3 From 12ac140a119b6802ebdfc3c596264f7fc292d4df Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Wed, 22 Feb 2017 11:36:02 -0300 Subject: Update VERSION to 8.18.0-pre --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 5c99c061a47..64de8316674 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.17.0-pre +8.18.0-pre -- cgit v1.2.3 From cf521c95761540f273804d23a1150dbb0af4e63b Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 22 Feb 2017 15:43:01 +0100 Subject: Allow setting of a custom connection pool host This allows you to set a custom host when calling Gitlab::Database.create_connection_pool. This is necessary for load balancing as in this case we want to inherit all settings except for the hostname. --- lib/gitlab/database.rb | 7 ++++++- spec/lib/gitlab/database_spec.rb | 21 ++++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index a47d7e98a62..d160cadc2d0 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -79,11 +79,16 @@ module Gitlab end end - def self.create_connection_pool(pool_size) + # pool_size - The size of the DB pool. + # host - An optional host name to use instead of the default one. + def self.create_connection_pool(pool_size, host = nil) # See activerecord-4.2.7.1/lib/active_record/connection_adapters/connection_specification.rb env = Rails.env original_config = ActiveRecord::Base.configurations + env_config = original_config[env].merge('pool' => pool_size) + env_config['host'] = host if host + config = original_config.merge(env => env_config) spec = diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index f01c42aff91..edd01d032c8 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -119,9 +119,24 @@ describe Gitlab::Database, lib: true do it 'creates a new connection pool with specific pool size' do pool = described_class.create_connection_pool(5) - expect(pool) - .to be_kind_of(ActiveRecord::ConnectionAdapters::ConnectionPool) - expect(pool.spec.config[:pool]).to eq(5) + begin + expect(pool) + .to be_kind_of(ActiveRecord::ConnectionAdapters::ConnectionPool) + + expect(pool.spec.config[:pool]).to eq(5) + ensure + pool.disconnect! + end + end + + it 'allows setting of a custom hostname' do + pool = described_class.create_connection_pool(5, '127.0.0.1') + + begin + expect(pool.spec.config[:host]).to eq('127.0.0.1') + ensure + pool.disconnect! + end end end -- cgit v1.2.3 From 0106f3cce134a4e7558050b90510555a3a5e7f2e Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 22 Feb 2017 12:41:36 +0000 Subject: Improvements in spacing --- app/assets/stylesheets/pages/pipelines.scss | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 0a0ab2abc38..521a9e39efc 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -1,4 +1,7 @@ .pipelines { + width: 100%; + overflow: auto; + .realtime-loading { font-size: 40px; text-align: center; @@ -22,7 +25,7 @@ } .table.ci-table { - min-width: 1200px; + width: 100%; table-layout: fixed; .label { @@ -40,12 +43,12 @@ .pipeline-info, .pipeline-commit, - .pipeline-stages, - .pipeline-actions { + .pipeline-stages { width: 20%; } .pipeline-actions { + width: 20%; padding-right: 0; } } @@ -99,7 +102,7 @@ } .commit-link { - padding: 9px 8px 10px; + padding: 9px 8px 10px 2px; } } -- cgit v1.2.3 From 27e4b9050caedcc8808dec683246051526eb5450 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 22 Feb 2017 16:24:55 +0100 Subject: Add links to previous/next articles --- doc/pages/getting_started_part_one.md | 9 ++++----- doc/pages/getting_started_part_three.md | 7 +++---- doc/pages/getting_started_part_two.md | 7 +++---- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/doc/pages/getting_started_part_one.md b/doc/pages/getting_started_part_one.md index 8a98afeeb86..8ae4779108c 100644 --- a/doc/pages/getting_started_part_one.md +++ b/doc/pages/getting_started_part_one.md @@ -26,7 +26,7 @@ GitLab Pages only supports static websites, meaning, your output files must be H To create your static site, you can either hardcode in HTML, CSS, and JS, or use a [Static Site Generator (SSG)](https://www.staticgen.com/) to simplify your code and build the static site for you, which is highly recommendable and much faster than hardcoding. -#### Further Reading +--- - Read through this technical overview on [Static versus Dynamic Websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) - Understand [how modern Static Site Generators work](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) and what you can add to your static site @@ -158,7 +158,6 @@ Now that you hopefully understand why you need all of this, it's simple: > Note: **do not** open certificates or encryption keys in regular text editors. Always use code editors (such as Sublime Text, Atom, Dreamweaver, Brackets, etc). -## Further Reading - -- Read through GitLab Pages from A to Z _[Part 2: Quick Start Guide - Setting Up GitLab Pages](getting_started_part_two.md)_ -- Read through GitLab Pages from A to Z _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md)_ +||| +|:--|--:| +||[**Part 2: Quick start guide - Setting up GitLab Pages →**](getting_started_part_two.md)| diff --git a/doc/pages/getting_started_part_three.md b/doc/pages/getting_started_part_three.md index 0bc6a601a09..28fb7f08bdf 100644 --- a/doc/pages/getting_started_part_three.md +++ b/doc/pages/getting_started_part_three.md @@ -273,7 +273,6 @@ What you can do with GitLab CI is pretty much up to your creativity. Once you ge - On this blog post, we go through the process of [pulling specific directories from different projects](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/) to deploy this website you're looking at, docs.gitlab.com. - On this blog post, we teach you [how to use GitLab Pages to produce a code coverage report](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/). -## Further Reading - -- Read through _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](getting_started_part_one.md)_ -- Read through _[Part 2: Quick Start Guide - Setting Up GitLab Pages](getting_started_part_two.md)_ +||| +|:--|--:| +|[**← Part 1: Static sites, domains, DNS records, and SSL/TLS certificates**](getting_started_part_one.md)|[**Part 2: Quick start guide - Setting up GitLab Pages →**](getting_started_part_two.md)| diff --git a/doc/pages/getting_started_part_two.md b/doc/pages/getting_started_part_two.md index b1d58b5b024..19e393f2623 100644 --- a/doc/pages/getting_started_part_two.md +++ b/doc/pages/getting_started_part_two.md @@ -106,7 +106,6 @@ On the contrary, if you deploy your website after forking one of our [default ex baseurl: "" ``` -## Further Reading - -- Read through _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](getting_started_part_one.md)_ -- Read through _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md)_ +||| +|:--|--:| +|[**← Part 1: Static sites, domains, DNS records, and SSL/TLS certificates**](getting_started_part_one.md)|[**Part 3: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages →**](getting_started_part_three.md)| -- cgit v1.2.3 From 9f56b57b55307fc3fa25737b89806564259354f8 Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira Date: Tue, 21 Feb 2017 16:47:25 -0300 Subject: Present GitLab version for each V3 to V4 API change on v3_to_v4.md --- .../28458-present-gitlab-version-for-v4-changes-on-docs.yml | 4 ++++ doc/api/v3_to_v4.md | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/28458-present-gitlab-version-for-v4-changes-on-docs.yml diff --git a/changelogs/unreleased/28458-present-gitlab-version-for-v4-changes-on-docs.yml b/changelogs/unreleased/28458-present-gitlab-version-for-v4-changes-on-docs.yml new file mode 100644 index 00000000000..dbbe8a19204 --- /dev/null +++ b/changelogs/unreleased/28458-present-gitlab-version-for-v4-changes-on-docs.yml @@ -0,0 +1,4 @@ +--- +title: Present GitLab version for each V3 to V4 API change on v3_to_v4.md +merge_request: +author: diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 59d7f0634b2..d99a52e4dc2 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -4,7 +4,7 @@ Our V4 API version is currently available as *Beta*! It means that V3 will still be supported and remain unchanged for now, but be aware that the following changes are in V4: -### Changes +### 8.17 - Removed `/projects/:search` (use: `/projects?search=x`) - `iid` filter has been removed from `projects/:id/issues` @@ -12,6 +12,9 @@ changes are in V4: - Endpoints under `projects/merge_request/:id` have been removed (use: `projects/merge_requests/:id`) - Project snippets do not return deprecated field `expires_at` - Endpoints under `projects/:id/keys` have been removed (use `projects/:id/deploy_keys`) + +### 9.0 + - Status 409 returned for POST `project/:id/members` when a member already exists - Moved `DELETE /projects/:id/star` to `POST /projects/:id/unstar` - Removed the following deprecated Templates endpoints (these are still accessible with `/templates` prefix) -- cgit v1.2.3 From eeb567d1b1b1988e9d3e07f1a6d7fa23ce7d06e3 Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Wed, 22 Feb 2017 10:24:38 +0100 Subject: Add Merge Request link to the v3 to v4 documentation Similar to changelog, mention the Merge Request id & link for each change. --- doc/api/v3_to_v4.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index d99a52e4dc2..1fea3d3407f 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -6,18 +6,18 @@ changes are in V4: ### 8.17 -- Removed `/projects/:search` (use: `/projects?search=x`) -- `iid` filter has been removed from `projects/:id/issues` -- `projects/:id/merge_requests?iid[]=x&iid[]=y` array filter has been renamed to `iids` -- Endpoints under `projects/merge_request/:id` have been removed (use: `projects/merge_requests/:id`) -- Project snippets do not return deprecated field `expires_at` -- Endpoints under `projects/:id/keys` have been removed (use `projects/:id/deploy_keys`) +- Removed `/projects/:search` (use: `/projects?search=x`) [!8877](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8877) +- `iid` filter has been removed from `projects/:id/issues` [!8967](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8967) +- `projects/:id/merge_requests?iid[]=x&iid[]=y` array filter has been renamed to `iids` [!8793](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8793) +- Endpoints under `projects/merge_request/:id` have been removed (use: `projects/merge_requests/:id`) [!8793](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8793) +- Project snippets do not return deprecated field `expires_at` [!8723](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8723) +- Endpoints under `projects/:id/keys` have been removed (use `projects/:id/deploy_keys`) [!8716](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8716) ### 9.0 -- Status 409 returned for POST `project/:id/members` when a member already exists -- Moved `DELETE /projects/:id/star` to `POST /projects/:id/unstar` -- Removed the following deprecated Templates endpoints (these are still accessible with `/templates` prefix) +- Status 409 returned for POST `project/:id/members` when a member already exists [!9093](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9093) +- Moved `DELETE /projects/:id/star` to `POST /projects/:id/unstar` [!9328](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9328) +- Removed the following deprecated Templates endpoints (these are still accessible with `/templates` prefix) [!8853](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8853) - `/licences` - `/licences/:key` - `/gitignores` @@ -26,17 +26,17 @@ changes are in V4: - `/gitignores/:key` - `/gitlab_ci_ymls/:key` - `/dockerfiles/:key` -- Moved `/projects/fork/:id` to `/projects/:id/fork` -- Moved `DELETE /todos` to `POST /todos/mark_as_done` and `DELETE /todos/:todo_id` to `POST /todos/:todo_id/mark_as_done` -- Endpoints `/projects/owned`, `/projects/visible`, `/projects/starred` & `/projects/all` are consolidated into `/projects` using query parameters -- Return pagination headers for all endpoints that return an array -- Removed `DELETE projects/:id/deploy_keys/:key_id/disable`. Use `DELETE projects/:id/deploy_keys/:key_id` instead -- Moved `PUT /users/:id/(block|unblock)` to `POST /users/:id/(block|unblock)` -- Make subscription API more RESTful. Use `post ":project_id/:subscribable_type/:subscribable_id/subscribe"` to subscribe and `post ":project_id/:subscribable_type/:subscribable_id/unsubscribe"` to unsubscribe from a resource. -- Labels filter on `projects/:id/issues` and `/issues` now matches only issues containing all labels (i.e.: Logical AND, not OR) -- Renamed param `branch_name` to `branch` on the following endpoints +- Moved `/projects/fork/:id` to `/projects/:id/fork` [!8940](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8940) +- Moved `DELETE /todos` to `POST /todos/mark_as_done` and `DELETE /todos/:todo_id` to `POST /todos/:todo_id/mark_as_done` [!9410](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9410) +- Endpoints `/projects/owned`, `/projects/visible`, `/projects/starred` & `/projects/all` are consolidated into `/projects` using query parameters [!8962](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8962) +- Return pagination headers for all endpoints that return an array [!8606](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8606) +- Removed `DELETE projects/:id/deploy_keys/:key_id/disable`. Use `DELETE projects/:id/deploy_keys/:key_id` instead [!9366](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9366) +- Moved `PUT /users/:id/(block|unblock)` to `POST /users/:id/(block|unblock)` [!9371](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9371) +- Make subscription API more RESTful. Use `post ":project_id/:subscribable_type/:subscribable_id/subscribe"` to subscribe and `post ":project_id/:subscribable_type/:subscribable_id/unsubscribe"` to unsubscribe from a resource. [!9325](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9325) +- Labels filter on `projects/:id/issues` and `/issues` now matches only issues containing all labels (i.e.: Logical AND, not OR) [!8849](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8849) +- Renamed param `branch_name` to `branch` on the following endpoints [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936) - POST `:id/repository/branches` - POST `:id/repository/commits` - POST/PUT/DELETE `:id/repository/files` -- Renamed `branch_name` to `branch` on DELETE `id/repository/branches/:branch` response - +- Renamed `branch_name` to `branch` on DELETE `id/repository/branches/:branch` response [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936) +- Remove `public` param from create and edit actions of projects [!8736](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8736) -- cgit v1.2.3 From 91965cefebfa6b2199b9b48e79752bf62cd67305 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 22 Feb 2017 23:48:49 +0800 Subject: Remove syntax help link and update screenshot Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9219#note_23943262 --- .../admin/application_settings/_form.html.haml | 2 -- .../img/admin_area_default_artifacts_expiration.png | Bin 16484 -> 14656 bytes 2 files changed, 2 deletions(-) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 0287533ab23..057b584e1bc 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -221,8 +221,6 @@ .help-block Set the default expiration time for each job's artifacts. 0 for unlimited. - = surround '(', ')' do - = link_to 'syntax', help_page_path('ci/yaml/README', anchor: 'artifactsexpire_in') = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration') - if Gitlab.config.registry.enabled diff --git a/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png b/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png index c6425ad32cd..50a86ede56b 100644 Binary files a/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png and b/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png differ -- cgit v1.2.3 From e5e7fb55ed325bedd8aab4fb2c07d514bc920f95 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Wed, 22 Feb 2017 12:59:01 -0300 Subject: remove link to unfinished video --- doc/pages/index.md | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/pages/index.md b/doc/pages/index.md index fe328748668..242fdf3147f 100644 --- a/doc/pages/index.md +++ b/doc/pages/index.md @@ -13,7 +13,6 @@ - [Part 1: Static sites, domains, DNS records, and SSL/TLS certificates](getting_started_part_one.md) - [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md) - Video tutorial: [How to publish a website with GitLab Pages on GitLab.com: from a forked project](https://youtu.be/TWqh9MtT4Bg) - - Video tutorial: [How to publish a website with GitLab Pages on GitLab.com: from scratch](#LINK) - [Part 3: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md) - Secure GitLab Pages custom domain with SSL/TLS certificates - [Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/) -- cgit v1.2.3 From 23e43ec5b18b35e6fe92adcbf3ca3d9a51a210b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 22 Feb 2017 16:00:49 +0000 Subject: Improve `Gitlab::EeCompatCheck` by using the `git apply --3way` flag This should solve 99% of the false-positive of the `ee_compat_check` job. --- lib/gitlab/ee_compat_check.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb index c8e36d8ff4a..e0fdf3f3d64 100644 --- a/lib/gitlab/ee_compat_check.rb +++ b/lib/gitlab/ee_compat_check.rb @@ -119,7 +119,7 @@ module Gitlab step("Reseting to latest master", %w[git reset --hard origin/master]) step("Checking if #{patch_path} applies cleanly to EE/master") - output, status = Gitlab::Popen.popen(%W[git apply --check #{patch_path}]) + output, status = Gitlab::Popen.popen(%W[git apply --check --3way #{patch_path}]) unless status.zero? failed_files = output.lines.reduce([]) do |memo, line| -- cgit v1.2.3 From 3dcc2ca593ecc989265f3ed9487220836e91619e Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Wed, 22 Feb 2017 13:06:44 -0300 Subject: fix spelling, add intermediate cert link --- doc/pages/getting_started_part_one.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/pages/getting_started_part_one.md b/doc/pages/getting_started_part_one.md index 8ae4779108c..c3bee1f164c 100644 --- a/doc/pages/getting_started_part_one.md +++ b/doc/pages/getting_started_part_one.md @@ -135,7 +135,7 @@ Regardless the CA you choose, the steps to add your certificate to your Pages pr #### What do you need 1. A PEM certificate -1. An intermediary certificate +1. An intermediate certificate 1. A public key ![Pages project - adding certificates](img/add_certificate_to_pages.png) @@ -145,7 +145,7 @@ These fields are found under your **Project**'s **Settings** > **Pages** > **New #### What's what? - A PEM certificate is the certificate generated by the CA, which needs to be added to the field **Certificate (PEM)**. -- An [intermediary certificate][] \(aka "root certificate"\) is the part of the encryption keychain that identifies the CA. Usually it's combined with the PEM certificate, but there are some cases in which you need to add them manually. [CloudFlare certs](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) are one of these cases. +- An [intermediate certificate](https://en.wikipedia.org/wiki/Intermediate_certificate_authority) \(aka "root certificate"\) is the part of the encryption keychain that identifies the CA. Usually it's combined with the PEM certificate, but there are some cases in which you need to add them manually. [CloudFlare certs](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) are one of these cases. - A public key is an encrypted key which validates your PEM against your domain. #### Now what? @@ -153,7 +153,7 @@ These fields are found under your **Project**'s **Settings** > **Pages** > **New Now that you hopefully understand why you need all of this, it's simple: - Your PEM certificate needs to be added to the first field -- If your certificate is missing its intermediary, copy and paste the root certificate (usually available from your CA website) and paste it in the [same field as your PEM certificate](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/), just jumping a line between them. +- If your certificate is missing its intermediate, copy and paste the root certificate (usually available from your CA website) and paste it in the [same field as your PEM certificate](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/), just jumping a line between them. - Copy your public key and paste it in the last field > Note: **do not** open certificates or encryption keys in regular text editors. Always use code editors (such as Sublime Text, Atom, Dreamweaver, Brackets, etc). -- cgit v1.2.3 From 20009ca9a29cc1011c7235aae652592d77318d98 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Wed, 22 Feb 2017 13:07:14 -0300 Subject: typo --- doc/pages/getting_started_part_three.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/pages/getting_started_part_three.md b/doc/pages/getting_started_part_three.md index 28fb7f08bdf..10ae3abb9e5 100644 --- a/doc/pages/getting_started_part_three.md +++ b/doc/pages/getting_started_part_three.md @@ -187,7 +187,7 @@ test: The `test` job is running on the stage `test`, Jekyll will build the site in a directory called `test`, and this job will affect all the branches except `master`. -The best benefit of applying _stages_ to different _jobs_ is that every job in the same stage builds in parallel. So, if your web app needs more than one test before being deployed, you can run all your test at the same time, it's not necessary to wait on test to finish to run the other. Of course, this is just a brief introduction of GitLab CI and GitLab Runner, which are tools much more powerful than that. This is what you need to be able to create and tweak your builds for your GitLab Pages site. +The best benefit of applying _stages_ to different _jobs_ is that every job in the same stage builds in parallel. So, if your web app needs more than one test before being deployed, you can run all your test at the same time, it's not necessary to wait one test to finish to run the other. Of course, this is just a brief introduction of GitLab CI and GitLab Runner, which are tools much more powerful than that. This is what you need to be able to create and tweak your builds for your GitLab Pages site. #### Before Script -- cgit v1.2.3 From 94255217e9c595579b83deb0a1dd5b7bca9572c5 Mon Sep 17 00:00:00 2001 From: wendy0402 Date: Sun, 9 Oct 2016 21:17:14 +0700 Subject: on branch deletion show loading icon and disabled the button after user click delete branch, there is no processing indication, and user can click many times till. It seems flaw in UX. this will fix it fix bug in branch deletion link --- app/assets/javascripts/ajax_loading_spinner.js | 35 +++++++++++++ app/assets/javascripts/dispatcher.js.es6 | 3 ++ app/views/projects/branches/_branch.html.haml | 2 +- changelogs/unreleased/branch_deletion.yml | 4 ++ spec/javascripts/ajax_loading_spinner_spec.js | 58 ++++++++++++++++++++++ .../fixtures/ajax_loading_spinner.html.haml | 2 + 6 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/ajax_loading_spinner.js create mode 100644 changelogs/unreleased/branch_deletion.yml create mode 100644 spec/javascripts/ajax_loading_spinner_spec.js create mode 100644 spec/javascripts/fixtures/ajax_loading_spinner.html.haml diff --git a/app/assets/javascripts/ajax_loading_spinner.js b/app/assets/javascripts/ajax_loading_spinner.js new file mode 100644 index 00000000000..38a8317dbd7 --- /dev/null +++ b/app/assets/javascripts/ajax_loading_spinner.js @@ -0,0 +1,35 @@ +class AjaxLoadingSpinner { + static init() { + const $elements = $('.js-ajax-loading-spinner'); + + $elements.on('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend); + $elements.on('ajax:complete', AjaxLoadingSpinner.ajaxComplete); + } + + static ajaxBeforeSend(e) { + e.target.setAttribute('disabled', ''); + const iconElement = e.target.querySelector('i'); + // get first fa- icon + const originalIcon = iconElement.className.match(/(fa-)([^\s]+)/g).first(); + iconElement.dataset.icon = originalIcon; + AjaxLoadingSpinner.toggleLoadingIcon(iconElement); + $(e.target).off('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend); + } + + static ajaxComplete(e) { + e.target.removeAttribute('disabled'); + const iconElement = e.target.querySelector('i'); + AjaxLoadingSpinner.toggleLoadingIcon(iconElement); + $(e.target).off('ajax:complete', AjaxLoadingSpinner.ajaxComplete); + } + + static toggleLoadingIcon(iconElement) { + const classList = iconElement.classList; + classList.toggle(iconElement.dataset.icon); + classList.toggle('fa-spinner'); + classList.toggle('fa-spin'); + } +} + +window.gl = window.gl || {}; +gl.AjaxLoadingSpinner = AjaxLoadingSpinner; diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 45aa6050aed..56610544f63 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -108,6 +108,9 @@ const ShortcutsBlob = require('./shortcuts_blob'); case 'projects:compare:show': new gl.Diff(); break; + case 'projects:branches:index': + gl.AjaxLoadingSpinner.init(); + break; case 'projects:issues:new': case 'projects:issues:edit': shortcut_handler = new ShortcutsNavigation(); diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 19ffe73a08d..ae63f8184df 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -31,7 +31,7 @@ - if can?(current_user, :push_code, @project) = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), - class: "btn btn-remove remove-row #{can_remove_branch?(@project, branch.name) ? '' : 'disabled'}", + class: "btn btn-remove remove-row js-ajax-loading-spinner #{can_remove_branch?(@project, branch.name) ? '' : 'disabled'}", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?" }, remote: true, diff --git a/changelogs/unreleased/branch_deletion.yml b/changelogs/unreleased/branch_deletion.yml new file mode 100644 index 00000000000..dbc9265a1fb --- /dev/null +++ b/changelogs/unreleased/branch_deletion.yml @@ -0,0 +1,4 @@ +--- +title: on branch deletion show loading icon and disabled the button +merge_request: 6761 +author: wendy0402 diff --git a/spec/javascripts/ajax_loading_spinner_spec.js b/spec/javascripts/ajax_loading_spinner_spec.js new file mode 100644 index 00000000000..a68bccb16f4 --- /dev/null +++ b/spec/javascripts/ajax_loading_spinner_spec.js @@ -0,0 +1,58 @@ +require('~/extensions/array'); +require('jquery'); +require('jquery-ujs'); +require('~/ajax_loading_spinner'); + +describe('Ajax Loading Spinner', () => { + const fixtureTemplate = 'static/ajax_loading_spinner.html.raw'; + preloadFixtures(fixtureTemplate); + + beforeEach(() => { + loadFixtures(fixtureTemplate); + gl.AjaxLoadingSpinner.init(); + }); + + it('change current icon with spinner icon and disable link while waiting ajax response', (done) => { + spyOn(jQuery, 'ajax').and.callFake((req) => { + const xhr = new XMLHttpRequest(); + const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner'); + const icon = ajaxLoadingSpinner.querySelector('i'); + + req.beforeSend(xhr, { dataType: 'text/html' }); + + expect(icon).not.toHaveClass('fa-trash-o'); + expect(icon).toHaveClass('fa-spinner'); + expect(icon).toHaveClass('fa-spin'); + expect(icon.dataset.icon).toEqual('fa-trash-o'); + expect(ajaxLoadingSpinner.getAttribute('disabled')).toEqual(''); + + req.complete({}); + + done(); + const deferred = $.Deferred(); + return deferred.promise(); + }); + document.querySelector('.js-ajax-loading-spinner').click(); + }); + + it('use original icon again and enabled the link after complete the ajax request', (done) => { + spyOn(jQuery, 'ajax').and.callFake((req) => { + const xhr = new XMLHttpRequest(); + const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner'); + + req.beforeSend(xhr, { dataType: 'text/html' }); + req.complete({}); + + const icon = ajaxLoadingSpinner.querySelector('i'); + expect(icon).toHaveClass('fa-trash-o'); + expect(icon).not.toHaveClass('fa-spinner'); + expect(icon).not.toHaveClass('fa-spin'); + expect(ajaxLoadingSpinner.getAttribute('disabled')).toEqual(null); + + done(); + const deferred = $.Deferred(); + return deferred.promise(); + }); + document.querySelector('.js-ajax-loading-spinner').click(); + }); +}); diff --git a/spec/javascripts/fixtures/ajax_loading_spinner.html.haml b/spec/javascripts/fixtures/ajax_loading_spinner.html.haml new file mode 100644 index 00000000000..09d8c9df3b2 --- /dev/null +++ b/spec/javascripts/fixtures/ajax_loading_spinner.html.haml @@ -0,0 +1,2 @@ +%a.js-ajax-loading-spinner{href: "http://goesnowhere.nothing/whereami", data: {remote: true}} + %i.fa.fa-trash-o -- cgit v1.2.3 From 8f61290f22ba624a3a0ac9622c3ded1fa3140b90 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Tue, 21 Feb 2017 15:29:19 -0600 Subject: Put back the new project button --- app/views/layouts/header/_default.html.haml | 4 ++++ changelogs/unreleased/27354-navigation-new-button.yml | 4 ++++ features/dashboard/dashboard.feature | 1 + 3 files changed, 9 insertions(+) create mode 100644 changelogs/unreleased/27354-navigation-new-button.yml diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index f8986893776..c28661c2351 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -36,6 +36,10 @@ = icon('bell fw') %span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) } = todos_count_format(todos_pending_count) + - if current_user.can_create_project? + %li + = link_to new_project_path, title: 'New project', aria: { label: "New project" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = icon('plus fw') - if Gitlab::Sherlock.enabled? %li = link_to sherlock_transactions_path, title: 'Sherlock Transactions', diff --git a/changelogs/unreleased/27354-navigation-new-button.yml b/changelogs/unreleased/27354-navigation-new-button.yml new file mode 100644 index 00000000000..62cac9bbbd3 --- /dev/null +++ b/changelogs/unreleased/27354-navigation-new-button.yml @@ -0,0 +1,4 @@ +--- +title: Re-add the New Project button in nav bar +merge_request: +author: diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature index 92061dac7f4..b1d5e4a7acb 100644 --- a/features/dashboard/dashboard.feature +++ b/features/dashboard/dashboard.feature @@ -11,6 +11,7 @@ Feature: Dashboard And I visit dashboard page Scenario: I should see projects list + Then I should see "New Project" link Then I should see "Shop" project link Then I should see "Shop" project CI status -- cgit v1.2.3 From f9079cb5ae43fa2cae0346c8dfd77b1e817fb78f Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Wed, 22 Feb 2017 13:16:54 -0300 Subject: wrap text - part 1 - [ci skip] --- doc/pages/getting_started_part_one.md | 183 ++++++++++++++++++++++++++-------- 1 file changed, 143 insertions(+), 40 deletions(-) diff --git a/doc/pages/getting_started_part_one.md b/doc/pages/getting_started_part_one.md index c3bee1f164c..57fc5d21b96 100644 --- a/doc/pages/getting_started_part_one.md +++ b/doc/pages/getting_started_part_one.md @@ -10,11 +10,17 @@ ---- -This is a comprehensive guide, made for those who want to publish a website with GitLab Pages but aren't familiar with the entire process involved. +This is a comprehensive guide, made for those who want to +publish a website with GitLab Pages but aren't familiar with +the entire process involved. -To **enable** GitLab Pages for GitLab CE (Community Edition) and GitLab EE (Enterprise Edition), please read the [admin documentation](https://docs.gitlab.com/ce/administration/pages/index.html), and/or watch this [video tutorial](https://youtu.be/dD8c7WNcc6s). +To **enable** GitLab Pages for GitLab CE (Community Edition) +and GitLab EE (Enterprise Edition), please read the +[admin documentation](https://docs.gitlab.com/ce/administration/pages/index.html), +and/or watch this [video tutorial](https://youtu.be/dD8c7WNcc6s). -> For this guide, we assume you already have GitLab Pages server up and running for your GitLab instance. +> For this guide, we assume you already have GitLab Pages +server up and running for your GitLab instance. ## What you need to know before getting started @@ -22,9 +28,13 @@ Before we begin, let's understand a few concepts first. ### Static Sites -GitLab Pages only supports static websites, meaning, your output files must be HTML, CSS, and JavaScript only. +GitLab Pages only supports static websites, meaning, +your output files must be HTML, CSS, and JavaScript only. -To create your static site, you can either hardcode in HTML, CSS, and JS, or use a [Static Site Generator (SSG)](https://www.staticgen.com/) to simplify your code and build the static site for you, which is highly recommendable and much faster than hardcoding. +To create your static site, you can either hardcode in HTML, +CSS, and JS, or use a [Static Site Generator (SSG)](https://www.staticgen.com/) +to simplify your code and build the static site for you, +which is highly recommendable and much faster than hardcoding. --- @@ -35,37 +45,72 @@ To create your static site, you can either hardcode in HTML, CSS, and JS, or use ### GitLab Pages Domain -If you set up a GitLab Pages project on GitLab.com, it will automatically be accessible under a [subdomain of `.pages.io`](https://docs.gitlab.com/ce/user/project/pages/). The `` is defined by your username on GitLab.com, or the group name you created this project under. +If you set up a GitLab Pages project on GitLab.com, +it will automatically be accessible under a +[subdomain of `.pages.io`](https://docs.gitlab.com/ce/user/project/pages/). +The `` is defined by your username on GitLab.com, +or the group name you created this project under. -> Note: If you use your own GitLab instance to deploy your site with GitLab Pages, check with your sysadmin what's your Pages wildcard domain. This guide is valid for any GitLab instance, you just need to replace Pages wildcard domain on GitLab.com (`*.gitlab.io`) with your own. +> Note: If you use your own GitLab instance to deploy your +site with GitLab Pages, check with your sysadmin what's your +Pages wildcard domain. This guide is valid for any GitLab instance, +you just need to replace Pages wildcard domain on GitLab.com +(`*.gitlab.io`) with your own. #### Practical examples **Project Websites:** -- You created a project called `blog` under your username `john`, therefore your project URL is `https://gitlab.com/john/blog/`. Once you enable GitLab Pages for this project, and build your site, it will be available under `https://john.gitlab.io/blog/`. -- You created a group for all your websites called `websites`, and a project within this group is called `blog`. Your project URL is `https://gitlab.com/websites/blog/`. Once you enable GitLab Pages for this project, the site will live under `https://websites.gitlab.io/blog/`. +- You created a project called `blog` under your username `john`, +therefore your project URL is `https://gitlab.com/john/blog/`. +Once you enable GitLab Pages for this project, and build your site, +it will be available under `https://john.gitlab.io/blog/`. +- You created a group for all your websites called `websites`, +and a project within this group is called `blog`. Your project +URL is `https://gitlab.com/websites/blog/`. Once you enable +GitLab Pages for this project, the site will live under +`https://websites.gitlab.io/blog/`. **User and Group Websites:** -- Under your username, `john`, you created a project called `john.gitlab.io`. Your project URL will be `https://gitlab.com/john/john.gitlab.io`. Once you enable GitLab Pages for your project, your website will be published under `https://john.gitlab.io`. -- Under your group `websites`, you created a project called `websites.gitlab.io`. your project's URL will be `https://gitlab.com/websites/websites.gitlab.io`. Once you enable GitLab Pages for your project, your website will be published under `https://websites.gitlab.io`. +- Under your username, `john`, you created a project called +`john.gitlab.io`. Your project URL will be `https://gitlab.com/john/john.gitlab.io`. +Once you enable GitLab Pages for your project, your website +will be published under `https://john.gitlab.io`. +- Under your group `websites`, you created a project called +`websites.gitlab.io`. your project's URL will be `https://gitlab.com/websites/websites.gitlab.io`. Once you enable GitLab Pages for your project, +your website will be published under `https://websites.gitlab.io`. **General example:** -- On GitLab.com, a project site will always be available under `https://namespace.gitlab.io/project-name` -- On GitLab.com, a user or group website will be available under `https://namespace.gitlab.io/` -- On your GitLab instance, replace `gitlab.io` above with your Pages server domain. Ask your sysadmin for this information. +- On GitLab.com, a project site will always be available under +`https://namespace.gitlab.io/project-name` +- On GitLab.com, a user or group website will be available under +`https://namespace.gitlab.io/` +- On your GitLab instance, replace `gitlab.io` above with your +Pages server domain. Ask your sysadmin for this information. ### DNS Records -A Domain Name System (DNS) web service routes visitors to websites by translating domain names (such as `www.example.com`) into the numeric IP addresses (such as `192.0.2.1`) that computers use to connect to each other. +A Domain Name System (DNS) web service routes visitors to websites +by translating domain names (such as `www.example.com`) into the +numeric IP addresses (such as `192.0.2.1`) that computers use to +connect to each other. -A DNS record is created to point a (sub)domain to a certain location, which can be an IP address or another domain. In case you want to use GitLab Pages with your own (sub)domain, you need to access your domain's registrar control panel to add a DNS record pointing it back to your GitLab Pages site. +A DNS record is created to point a (sub)domain to a certain location, +which can be an IP address or another domain. In case you want to use +GitLab Pages with your own (sub)domain, you need to access your domain's +registrar control panel to add a DNS record pointing it back to your +GitLab Pages site. -Note that **how to** add DNS records depends on which server your domain is hosted on. Every control panel has its own place to do it. If you are not an admin of your domain, and don't have access to your registrar, you'll need to ask for the technical support of your hosting service to do it for you. +Note that **how to** add DNS records depends on which server your domain +is hosted on. Every control panel has its own place to do it. If you are +not an admin of your domain, and don't have access to your registrar, +you'll need to ask for the technical support of your hosting service +to do it for you. -To help you out, we've gathered some instructions on how to do that for the most popular hosting services: +To help you out, we've gathered some instructions on how to do that +for the most popular hosting services: - [Amazon](http://docs.aws.amazon.com/gettingstarted/latest/swh/getting-started-configure-route53.html) - [Bluehost](https://my.bluehost.com/cgi/help/559) @@ -78,11 +123,19 @@ To help you out, we've gathered some instructions on how to do that for the most - [Media Temple](https://mediatemple.net/community/products/dv/204403794/how-can-i-change-the-dns-records-for-my-domain) - [Microsoft](https://msdn.microsoft.com/en-us/library/bb727018.aspx) -If your hosting service is not listed above, you can just try to search the web for "how to add dns record on ". +If your hosting service is not listed above, you can just try to +search the web for "how to add dns record on ". #### DNS A record -In case you want to point a root domain (`example.com`) to your GitLab Pages site, deployed to `namespace.gitlab.io`, you need to log into your domain's admin control panel and add a DNS `A` record pointing your domain to Pages' server IP address. For projects on GitLab.com, this IP is `104.208.235.32`. For projects leaving in other GitLab instances (CE or EE), please contact your sysadmin asking for this information (which IP address is Pages server running on your instance). +In case you want to point a root domain (`example.com`) to your +GitLab Pages site, deployed to `namespace.gitlab.io`, you need to +log into your domain's admin control panel and add a DNS `A` record +pointing your domain to Pages' server IP address. For projects on +GitLab.com, this IP is `104.208.235.32`. For projects leaving in +other GitLab instances (CE or EE), please contact your sysadmin +asking for this information (which IP address is Pages server +running on your instance). **Practical Example:** @@ -90,9 +143,15 @@ In case you want to point a root domain (`example.com`) to your GitLab Pages sit #### DNS CNAME record -In case you want to point a subdomain (`hello-world.example.com`) to your GitLab Pages site initially deployed to `namespace.gitlab.io`, you need to log into your domain's admin control panel and add a DNS `CNAME` record pointing your subdomain to your website URL (`namespace.gitlab.io`) address. +In case you want to point a subdomain (`hello-world.example.com`) +to your GitLab Pages site initially deployed to `namespace.gitlab.io`, +you need to log into your domain's admin control panel and add a DNS +`CNAME` record pointing your subdomain to your website URL +(`namespace.gitlab.io`) address. -Notice that, despite it's a user or project website, the `CNAME` should point to your Pages domain (`namespace.gitlab.io`), without any `/project-name`. +Notice that, despite it's a user or project website, the `CNAME` +should point to your Pages domain (`namespace.gitlab.io`), +without any `/project-name`. **Practical Example:** @@ -107,30 +166,61 @@ Notice that, despite it's a user or project website, the `CNAME` should point to > **Notes**: > -> - **Do not** use a CNAME record if you want to point your `domain.com` to your GitLab Pages site. Use an `A` record instead. -> - **Do not** add any special chars after the default Pages domain. E.g., **do not** point your `subdomain.domain.com` to `namespace.gitlab.io.` or `namespace.gitlab.io/`. +> - **Do not** use a CNAME record if you want to point your +`domain.com` to your GitLab Pages site. Use an `A` record instead. +> - **Do not** add any special chars after the default Pages +domain. E.g., **do not** point your `subdomain.domain.com` to +`namespace.gitlab.io.` or `namespace.gitlab.io/`. ### SSL/TLS Certificates -Every GitLab Pages project on GitLab.com will be available under HTTPS for the default Pages domain (`*.gitlab.io`). Once you set up your Pages project with your custom (sub)domain, if you want it secured by HTTPS, you will have to issue a certificate for that (sub)domain and install it on your project. +Every GitLab Pages project on GitLab.com will be available under +HTTPS for the default Pages domain (`*.gitlab.io`). Once you set +up your Pages project with your custom (sub)domain, if you want +it secured by HTTPS, you will have to issue a certificate for that +(sub)domain and install it on your project. -> Note: certificates are NOT required to add to your custom (sub)domain on your GitLab Pages project, though they are highly recommendable. +> Note: certificates are NOT required to add to your custom +(sub)domain on your GitLab Pages project, though they are +highly recommendable. -The importance of having any website securely served under HTTPS is explained on the introductory section of the blog post [Secure GitLab Pages with StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/#https-a-quick-overview). +The importance of having any website securely served under HTTPS +is explained on the introductory section of the blog post +[Secure GitLab Pages with StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/#https-a-quick-overview). -The reason why certificates are so important is that they encrypt the connection between the **client** (you, me, your visitors) and the **server** (where you site lives), through a keychain of authentications and validations. +The reason why certificates are so important is that they encrypt +the connection between the **client** (you, me, your visitors) +and the **server** (where you site lives), through a keychain of +authentications and validations. ### Issuing Certificates -GitLab Pages accepts [PEM](https://support.quovadisglobal.com/kb/a37/what-is-pem-format.aspx) certificates issued by [Certificate Authorities (CA)](https://en.wikipedia.org/wiki/Certificate_authority) and self-signed certificates. Of course, [you'd rather issue a certificate than generate a self-signed](https://en.wikipedia.org/wiki/Self-signed_certificate), for security reasons and for having browsers trusting your site's certificate. - -There are several different kinds of certificates, each one with certain security level. A static personal website will not require the same security level as an online banking web app, for instance. There are a couple Certificate Authorities that offer free certificates, aiming to make the internet more secure to everyone. The most popular is [Let's Encrypt](https://letsencrypt.org/), which issues certificates trusted by most of browsers, it's open source, and free to use. Please read through this tutorial to understand [how to secure your GitLab Pages website with Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/). - -With the same popularity, there are [certificates issued by CloudFlare](https://www.cloudflare.com/ssl/), which also offers a [free CDN service](https://blog.cloudflare.com/cloudflares-free-cdn-and-you/). Their certs are valid up to 15 years. Read through the tutorial on [how to add a CloudFlare Certificate to your GitLab Pages website](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/). +GitLab Pages accepts [PEM](https://support.quovadisglobal.com/kb/a37/what-is-pem-format.aspx) certificates issued by +[Certificate Authorities (CA)](https://en.wikipedia.org/wiki/Certificate_authority) +and self-signed certificates. Of course, +[you'd rather issue a certificate than generate a self-signed](https://en.wikipedia.org/wiki/Self-signed_certificate), +for security reasons and for having browsers trusting your +site's certificate. + +There are several different kinds of certificates, each one +with certain security level. A static personal website will +not require the same security level as an online banking web app, +for instance. There are a couple Certificate Authorities that +offer free certificates, aiming to make the internet more secure +to everyone. The most popular is [Let's Encrypt](https://letsencrypt.org/), +which issues certificates trusted by most of browsers, it's open +source, and free to use. Please read through this tutorial to +understand [how to secure your GitLab Pages website with Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/). + +With the same popularity, there are [certificates issued by CloudFlare](https://www.cloudflare.com/ssl/), +which also offers a [free CDN service](https://blog.cloudflare.com/cloudflares-free-cdn-and-you/). +Their certs are valid up to 15 years. Read through the tutorial on +[how to add a CloudFlare Certificate to your GitLab Pages website](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/). ### Adding certificates to your project -Regardless the CA you choose, the steps to add your certificate to your Pages project are the same. +Regardless the CA you choose, the steps to add your certificate to +your Pages project are the same. #### What do you need @@ -144,19 +234,32 @@ These fields are found under your **Project**'s **Settings** > **Pages** > **New #### What's what? -- A PEM certificate is the certificate generated by the CA, which needs to be added to the field **Certificate (PEM)**. -- An [intermediate certificate](https://en.wikipedia.org/wiki/Intermediate_certificate_authority) \(aka "root certificate"\) is the part of the encryption keychain that identifies the CA. Usually it's combined with the PEM certificate, but there are some cases in which you need to add them manually. [CloudFlare certs](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) are one of these cases. -- A public key is an encrypted key which validates your PEM against your domain. +- A PEM certificate is the certificate generated by the CA, +which needs to be added to the field **Certificate (PEM)**. +- An [intermediate certificate](https://en.wikipedia.org/wiki/Intermediate_certificate_authority) (aka "root certificate") is +the part of the encryption keychain that identifies the CA. +Usually it's combined with the PEM certificate, but there are +some cases in which you need to add them manually. +[CloudFlare certs](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) +are one of these cases. +- A public key is an encrypted key which validates +your PEM against your domain. #### Now what? -Now that you hopefully understand why you need all of this, it's simple: +Now that you hopefully understand why you need all +of this, it's simple: - Your PEM certificate needs to be added to the first field -- If your certificate is missing its intermediate, copy and paste the root certificate (usually available from your CA website) and paste it in the [same field as your PEM certificate](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/), just jumping a line between them. +- If your certificate is missing its intermediate, copy +and paste the root certificate (usually available from your CA website) +and paste it in the [same field as your PEM certificate](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/), +just jumping a line between them. - Copy your public key and paste it in the last field -> Note: **do not** open certificates or encryption keys in regular text editors. Always use code editors (such as Sublime Text, Atom, Dreamweaver, Brackets, etc). +> Note: **do not** open certificates or encryption keys in +regular text editors. Always use code editors (such as +Sublime Text, Atom, Dreamweaver, Brackets, etc). ||| |:--|--:| -- cgit v1.2.3 From ce5420ab8fde3b147d3e2e1068b6b6313883a224 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Wed, 22 Feb 2017 13:22:14 -0300 Subject: fix link --- doc/pages/getting_started_part_two.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/pages/getting_started_part_two.md b/doc/pages/getting_started_part_two.md index 19e393f2623..f211b0a3363 100644 --- a/doc/pages/getting_started_part_two.md +++ b/doc/pages/getting_started_part_two.md @@ -70,7 +70,7 @@ To turn a **project website** forked from the Pages group into a **user/group** #### Create a Project from Scratch -1. From your **Project**'s **[Dashboard](https://gitlab.com/dashboard/projects)**, click **New project**, and name it considering the [examples above](#practical-examples). +1. From your **Project**'s **[Dashboard](https://gitlab.com/dashboard/projects)**, click **New project**, and name it considering the [pratical examples](getting_started_part_one.md#practical-examples). 1. Clone it to your local computer, add your website files to your project, add, commit and push to GitLab. 1. From the your **Project**'s page, click **Set up CI**: -- cgit v1.2.3 From 08e9d2b5c8fdf813e88a0594d6f96a4767661f68 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Wed, 22 Feb 2017 13:27:02 -0300 Subject: wrapping text - part 2 [ci skip] --- doc/pages/getting_started_part_two.md | 79 +++++++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 17 deletions(-) diff --git a/doc/pages/getting_started_part_two.md b/doc/pages/getting_started_part_two.md index f211b0a3363..15f628ac1fb 100644 --- a/doc/pages/getting_started_part_two.md +++ b/doc/pages/getting_started_part_two.md @@ -12,7 +12,9 @@ ## Setting Up GitLab Pages -For a complete step-by-step tutorial, please read the blog post [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/). The following sections will explain what do you need and why do you need them. +For a complete step-by-step tutorial, please read the +blog post [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/). The following sections will explain +what do you need and why do you need them. @@ -20,17 +22,20 @@ For a complete step-by-step tutorial, please read the blog post [Hosting on GitL 1. A project 1. A configuration file (`.gitlab-ci.yml`) to deploy your site -1. A specific `job` called `pages` in the configuration file that will make GitLab aware that you are deploying a GitLab Pages website +1. A specific `job` called `pages` in the configuration file +that will make GitLab aware that you are deploying a GitLab Pages website #### Optional Features 1. A custom domain or subdomain 1. A DNS pointing your (sub)domain to your Pages site - 1. **Optional**: an SSL/TLS certificate so your custom domain is accessible under HTTPS. + 1. **Optional**: an SSL/TLS certificate so your custom + domain is accessible under HTTPS. ### Project -Your GitLab Pages project is a regular project created the same way you do for the other ones. To get started with GitLab Pages, you have two ways: +Your GitLab Pages project is a regular project created the +same way you do for the other ones. To get started with GitLab Pages, you have two ways: - Fork one of the templates from Page Examples, or - Create a new project from scratch @@ -39,9 +44,12 @@ Let's go over both options. #### Fork a Project to Get Started From -To make things easy for you, we've created this [group](https://gitlab.com/pages) of default projects containing the most popular SSGs templates. +To make things easy for you, we've created this +[group](https://gitlab.com/pages) of default projects +containing the most popular SSGs templates. -Watch the [video tutorial](https://youtu.be/TWqh9MtT4Bg) we've created for the steps below. +Watch the [video tutorial](https://youtu.be/TWqh9MtT4Bg) we've +created for the steps below. 1. Choose your SSG template 1. Fork a project from the [Pages group](https://gitlab.com/pages) @@ -62,45 +70,82 @@ To turn a **project website** forked from the Pages group into a **user/group** > >1. Why do I need to remove the fork relationship? > -> Unless you want to contribute to the original project, you won't need it connected to the upstream. A [fork](https://about.gitlab.com/2016/12/01/how-to-keep-your-fork-up-to-date-with-its-origin/#fork) is useful for submitting merge requests to the upstream. +> Unless you want to contribute to the original project, +you won't need it connected to the upstream. A +[fork](https://about.gitlab.com/2016/12/01/how-to-keep-your-fork-up-to-date-with-its-origin/#fork) +is useful for submitting merge requests to the upstream. > > 2. Why do I need to enable Shared Runners? > -> Shared Runners will run the script set by your GitLab CI configuration file. They're enabled by default to new projects, but not to forks. +> Shared Runners will run the script set by your GitLab CI +configuration file. They're enabled by default to new projects, +but not to forks. #### Create a Project from Scratch -1. From your **Project**'s **[Dashboard](https://gitlab.com/dashboard/projects)**, click **New project**, and name it considering the [pratical examples](getting_started_part_one.md#practical-examples). -1. Clone it to your local computer, add your website files to your project, add, commit and push to GitLab. +1. From your **Project**'s **[Dashboard](https://gitlab.com/dashboard/projects)**, +click **New project**, and name it considering the +[pratical examples](getting_started_part_one.md#practical-examples). +1. Clone it to your local computer, add your website +files to your project, add, commit and push to GitLab. 1. From the your **Project**'s page, click **Set up CI**: ![setup GitLab CI](img/setup_ci.png) -1. Choose one of the templates from the dropbox menu. Pick up the template corresponding to the SSG you're using (or plain HTML). +1. Choose one of the templates from the dropbox menu. +Pick up the template corresponding to the SSG you're using (or plain HTML). ![gitlab-ci templates](img/choose_ci_template.png) -Once you have both site files and `.gitlab-ci.yml` in your project's root, GitLab CI will build your site and deploy it with Pages. Once the first build passes, you see your site is live by navigating to your **Project**'s **Settings** > **Pages**, where you'll find its default URL. +Once you have both site files and `.gitlab-ci.yml` in your project's +root, GitLab CI will build your site and deploy it with Pages. +Once the first build passes, you see your site is live by +navigating to your **Project**'s **Settings** > **Pages**, +where you'll find its default URL. > **Notes:** > -> - GitLab Pages [supports any SSG](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/), but, if you don't find yours among the templates, you'll need to configure your own `.gitlab-ci.yml`. Do do that, please read through the article [Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md). New SSGs are very welcome among the [example projects](https://gitlab.com/pages). If you set up a new one, please [contribute](https://gitlab.com/pages/pages.gitlab.io/blob/master/CONTRIBUTING.md) to our examples. +> - GitLab Pages [supports any SSG](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/), but, +if you don't find yours among the templates, you'll need +to configure your own `.gitlab-ci.yml`. Do do that, please +read through the article [Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md). New SSGs are very welcome among +the [example projects](https://gitlab.com/pages). If you set +up a new one, please +[contribute](https://gitlab.com/pages/pages.gitlab.io/blob/master/CONTRIBUTING.md) +to our examples. > -> - The second step _"Clone it to your local computer"_, can be done differently, achieving the same results: instead of cloning the bare repository to you local computer and moving your site files into it, you can run `git init` in your local website directory, add the remote URL: `git remote add origin git@gitlab.com:namespace/project-name.git`, then add, commit, and push. +> - The second step _"Clone it to your local computer"_, can be done +differently, achieving the same results: instead of cloning the bare +repository to you local computer and moving your site files into it, +you can run `git init` in your local website directory, add the +remote URL: `git remote add origin git@gitlab.com:namespace/project-name.git`, +then add, commit, and push. ### URLs and Baseurls -Every Static Site Generator (SSG) default configuration expects to find your website under a (sub)domain (`example.com`), not in a subdirectory of that domain (`example.com/subdir`). Therefore, whenever you publish a project website (`namespace.gitlab.io/project-name`), you'll have to look for this configuration (base URL) on your SSG's documentation and set it up to reflect this pattern. +Every Static Site Generator (SSG) default configuration expects +to find your website under a (sub)domain (`example.com`), not +in a subdirectory of that domain (`example.com/subdir`). Therefore, +whenever you publish a project website (`namespace.gitlab.io/project-name`), +you'll have to look for this configuration (base URL) on your SSG's +documentation and set it up to reflect this pattern. -For example, for a Jekyll site, the `baseurl` is defined in the Jekyll configuration file, `_config.yml`. If your website URL is `https://john.gitlab.io/blog/`, you need to add this line to `_config.yml`: +For example, for a Jekyll site, the `baseurl` is defined in the Jekyll +configuration file, `_config.yml`. If your website URL is +`https://john.gitlab.io/blog/`, you need to add this line to `_config.yml`: ```yaml baseurl: "/blog" ``` -On the contrary, if you deploy your website after forking one of our [default examples](https://gitlab.com/pages), the baseurl will already be configured this way, as all examples there are project websites. If you decide to make yours a user or group website, you'll have to remove this configuration from your project. For the Jekyll example we've just mentioned, you'd have to change Jekyll's `_config.yml` to: +On the contrary, if you deploy your website after forking one of +our [default examples](https://gitlab.com/pages), the baseurl will +already be configured this way, as all examples there are project +websites. If you decide to make yours a user or group website, you'll +have to remove this configuration from your project. For the Jekyll +example we've just mentioned, you'd have to change Jekyll's `_config.yml` to: ```yaml baseurl: "" -- cgit v1.2.3 From 6ec9e318dc002303a02e07f972d01536bc1e884f Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Wed, 22 Feb 2017 13:32:44 -0300 Subject: wrapping text - part 3 --- doc/pages/getting_started_part_three.md | 170 ++++++++++++++++++++++++++------ 1 file changed, 139 insertions(+), 31 deletions(-) diff --git a/doc/pages/getting_started_part_three.md b/doc/pages/getting_started_part_three.md index 10ae3abb9e5..8acf8a85d5a 100644 --- a/doc/pages/getting_started_part_three.md +++ b/doc/pages/getting_started_part_three.md @@ -12,15 +12,38 @@ ### Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages -[GitLab CI](https://about.gitlab.com/gitlab-ci/) serves numerous purposes, to build, test, and deploy your app from GitLab through [Continuous Integration, Continuous Delivery, and Continuous Deployment](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) methods. You will need it to build your website with GitLab Pages, and deploy it to the Pages server. - -What this file actually does is telling the [GitLab Runner](https://docs.gitlab.com/runner/) to run scripts as you would do from the command line. The Runner acts as your terminal. GitLab CI tells the Runner which commands to run. Both are built-in in GitLab, and you don't need to set up anything for them to work. - -Explaining [every detail of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html) and GitLab Runner is out of the scope of this guide, but we'll need to understand just a few things to be able to write our own `.gitlab-ci.yml` or tweak an existing one. It's an [Yaml](http://docs.ansible.com/ansible/YAMLSyntax.html) file, with its own syntax. You can always check your CI syntax with the [GitLab CI Lint Tool](https://gitlab.com/ci/lint). +[GitLab CI](https://about.gitlab.com/gitlab-ci/) serves +numerous purposes, to build, test, and deploy your app +from GitLab through +[Continuous Integration, Continuous Delivery, and Continuous Deployment](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) +methods. You will need it to build your website with GitLab Pages, +and deploy it to the Pages server. + +What this file actually does is telling the +[GitLab Runner](https://docs.gitlab.com/runner/) to run scripts +as you would do from the command line. The Runner acts as your +terminal. GitLab CI tells the Runner which commands to run. +Both are built-in in GitLab, and you don't need to set up +anything for them to work. + +Explaining [every detail of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html) +and GitLab Runner is out of the scope of this guide, but we'll +need to understand just a few things to be able to write our own +`.gitlab-ci.yml` or tweak an existing one. It's an +[Yaml](http://docs.ansible.com/ansible/YAMLSyntax.html) file, +with its own syntax. You can always check your CI syntax with +the [GitLab CI Lint Tool](https://gitlab.com/ci/lint). **Practical Example:** -Let's consider you have a [Jekyll](https://jekyllrb.com/) site. To build it locally, you would open your terminal, and run `jekyll build`. Of course, before building it, you had to install Jekyll in your computer. For that, you had to open your terminal and run `gem install jekyll`. Right? GitLab CI + GitLab Runner do the same thing. But you need to write in the `.gitlab-ci.yml` the script you want to run so the GitLab Runner will do it for you. It looks more complicated then it is. What you need to tell the Runner: +Let's consider you have a [Jekyll](https://jekyllrb.com/) site. +To build it locally, you would open your terminal, and run `jekyll build`. +Of course, before building it, you had to install Jekyll in your computer. +For that, you had to open your terminal and run `gem install jekyll`. +Right? GitLab CI + GitLab Runner do the same thing. But you need to +write in the `.gitlab-ci.yml` the script you want to run so +GitLab Runner will do it for you. It looks more complicated then it +is. What you need to tell the Runner: ``` $ gem install jekyll @@ -39,7 +62,9 @@ script: #### Job -So far so good. Now, each `script`, in GitLab is organized by a `job`, which is a bunch of scripts and settings you want to apply to that specific task. +So far so good. Now, each `script`, in GitLab is organized by +a `job`, which is a bunch of scripts and settings you want to +apply to that specific task. ```yaml job: @@ -48,7 +73,9 @@ job: - jekyll build ``` -For GitLab Pages, this `job` has a specific name, called `pages`, which tells the Runner you want that task to deploy your website with GitLab Pages: +For GitLab Pages, this `job` has a specific name, called `pages`, +which tells the Runner you want that task to deploy your website +with GitLab Pages: ```yaml pages: @@ -59,7 +86,12 @@ pages: #### `public` Dir -We also need to tell Jekyll where do you want the website to build, and GitLab Pages will only consider files in a directory called `public`. To do that with Jekyll, we need to add a flag specifying the [destination (`-d`)](https://jekyllrb.com/docs/usage/) of the built website: `jekyll build -d public`. Of course, we need to tell this to our Runner: +We also need to tell Jekyll where do you want the website to build, +and GitLab Pages will only consider files in a directory called `public`. +To do that with Jekyll, we need to add a flag specifying the +[destination (`-d`)](https://jekyllrb.com/docs/usage/) of the +built website: `jekyll build -d public`. Of course, we need +to tell this to our Runner: ```yaml pages: @@ -70,7 +102,9 @@ pages: #### Artifacts -We also need to tell the Runner that this _job_ generates _artifacts_, which is the site built by Jekyll. Where are these artifacts stored? In the `public` directory: +We also need to tell the Runner that this _job_ generates +_artifacts_, which is the site built by Jekyll. +Where are these artifacts stored? In the `public` directory: ```yaml pages: @@ -82,7 +116,12 @@ pages: - public ``` -The script above would be enough to build your Jekyll site with GitLab Pages. But, from Jekyll 3.4.0 on, its default template originated by `jekyll new project` requires [Bundler](http://bundler.io/) to install Jekyll dependencies and the default theme. To adjust our script to meet these new requirements, we only need to install and build Jekyll with Bundler: +The script above would be enough to build your Jekyll +site with GitLab Pages. But, from Jekyll 3.4.0 on, its default +template originated by `jekyll new project` requires +[Bundler](http://bundler.io/) to install Jekyll dependencies +and the default theme. To adjust our script to meet these new +requirements, we only need to install and build Jekyll with Bundler: ```yaml pages: @@ -94,11 +133,18 @@ pages: - public ``` -That's it! A `.gitlab-ci.yml` with the content above would deploy your Jekyll 3.4.0 site with GitLab Pages. This is the minimum configuration for our example. On the steps below, we'll refine the script by adding extra options to our GitLab CI. +That's it! A `.gitlab-ci.yml` with the content above would deploy +your Jekyll 3.4.0 site with GitLab Pages. This is the minimum +configuration for our example. On the steps below, we'll refine +the script by adding extra options to our GitLab CI. #### Image -At this point, you probably ask yourself: "okay, but to install Jekyll I need Ruby. Where is Ruby on that script?". The answer is simple: the first thing GitLab Runner will look for in your `.gitlab-ci.yml` is a [Docker](https://www.docker.com/) image specifying what do you need in your container to run that script: +At this point, you probably ask yourself: "okay, but to install Jekyll +I need Ruby. Where is Ruby on that script?". The answer is simple: the +first thing GitLab Runner will look for in your `.gitlab-ci.yml` is a +[Docker](https://www.docker.com/) image specifying what do you need in +your container to run that script: ```yaml image: ruby:2.3 @@ -112,17 +158,32 @@ pages: - public ``` -In this case, you're telling the Runner to pull this image, which contains Ruby 2.3 as part of its file system. When you don't specify this image in your configuration, the Runner will use a default image, which is Ruby 2.1. +In this case, you're telling the Runner to pull this image, which +contains Ruby 2.3 as part of its file system. When you don't specify +this image in your configuration, the Runner will use a default +image, which is Ruby 2.1. -If your SSG needs [NodeJS](https://nodejs.org/) to build, you'll need to specify which image you want to use, and this image should contain NodeJS as part of its file system. E.g., for a [Hexo](https://gitlab.com/pages/hexo) site, you can use `image: node:4.2.2`. +If your SSG needs [NodeJS](https://nodejs.org/) to build, you'll +need to specify which image you want to use, and this image should +contain NodeJS as part of its file system. E.g., for a +[Hexo](https://gitlab.com/pages/hexo) site, you can use `image: node:4.2.2`. -> Note: we're not trying to explain what a Docker image is, we just need to introduce the concept with a minimum viable explanation. To know more about Docker images, please visit their website or take a look at a [summarized explanation](http://paislee.io/how-to-automate-docker-deployments/) here. +> Note: we're not trying to explain what a Docker image is, +we just need to introduce the concept with a minimum viable +explanation. To know more about Docker images, please visit +their website or take a look at a +[summarized explanation](http://paislee.io/how-to-automate-docker-deployments/) here. Let's go a little further. #### Branching -If you use GitLab as a version control platform, you will have your branching strategy to work on your project. Meaning, you will have other branches in your project, but you'll want only pushes to the default branch (usually `master`) to be deployed to your website. To do that, we need to add another line to our CI, telling the Runner to only perform that _job_ called `pages` on the `master` branch `only`: +If you use GitLab as a version control platform, you will have your +branching strategy to work on your project. Meaning, you will have +other branches in your project, but you'll want only pushes to the +default branch (usually `master`) to be deployed to your website. +To do that, we need to add another line to our CI, telling the Runner +to only perform that _job_ called `pages` on the `master` branch `only`: ```yaml image: ruby:2.3 @@ -140,7 +201,12 @@ pages: #### Stages -Another interesting concept to keep in mind are build stages. Your web app can pass through a lot of tests and other tasks until it's deployed to staging or production environments. There are three default stages on GitLab CI: build, test, and deploy. To specify which stage your _job_ is running, simply add another line to your CI: +Another interesting concept to keep in mind are build stages. +Your web app can pass through a lot of tests and other tasks +until it's deployed to staging or production environments. +There are three default stages on GitLab CI: build, test, +and deploy. To specify which stage your _job_ is running, +simply add another line to your CI: ```yaml image: ruby:2.3 @@ -157,7 +223,13 @@ pages: - master ``` -You might ask yourself: "why should I bother with stages at all?" Well, let's say you want to be able to test your script and check the built site before deploying your site to production. You want to run the test exactly as your script will do when you push to `master`. It's simple, let's add another task (_job_) to our CI, telling it to test every push to other branches, `except` the `master` branch: +You might ask yourself: "why should I bother with stages +at all?" Well, let's say you want to be able to test your +script and check the built site before deploying your site +to production. You want to run the test exactly as your +script will do when you push to `master`. It's simple, +let's add another task (_job_) to our CI, telling it to +test every push to other branches, `except` the `master` branch: ```yaml image: ruby:2.3 @@ -185,13 +257,29 @@ test: - master ``` -The `test` job is running on the stage `test`, Jekyll will build the site in a directory called `test`, and this job will affect all the branches except `master`. - -The best benefit of applying _stages_ to different _jobs_ is that every job in the same stage builds in parallel. So, if your web app needs more than one test before being deployed, you can run all your test at the same time, it's not necessary to wait one test to finish to run the other. Of course, this is just a brief introduction of GitLab CI and GitLab Runner, which are tools much more powerful than that. This is what you need to be able to create and tweak your builds for your GitLab Pages site. +The `test` job is running on the stage `test`, Jekyll +will build the site in a directory called `test`, and +this job will affect all the branches except `master`. + +The best benefit of applying _stages_ to different +_jobs_ is that every job in the same stage builds in +parallel. So, if your web app needs more than one test +before being deployed, you can run all your test at the +same time, it's not necessary to wait one test to finish +to run the other. Of course, this is just a brief +introduction of GitLab CI and GitLab Runner, which are +tools much more powerful than that. This is what you +need to be able to create and tweak your builds for +your GitLab Pages site. #### Before Script -To avoid running the same script multiple times across your _jobs_, you can add the parameter `before_script`, in which you specify which commands you want to run for every single _job_. In our example, notice that we run `bundle install` for both jobs, `pages` and `test`. We don't need to repeat it: +To avoid running the same script multiple times across +your _jobs_, you can add the parameter `before_script`, +in which you specify which commands you want to run for +every single _job_. In our example, notice that we run +`bundle install` for both jobs, `pages` and `test`. +We don't need to repeat it: ```yaml image: ruby:2.3 @@ -222,7 +310,11 @@ test: #### Caching Dependencies -If you want to cache the installation files for your projects dependencies, for building faster, you can use the parameter `cache`. For this example, we'll cache Jekyll dependencies in a `vendor` directory when we run `bundle install`: +If you want to cache the installation files for your +projects dependencies, for building faster, you can +use the parameter `cache`. For this example, we'll +cache Jekyll dependencies in a `vendor` directory +when we run `bundle install`: ```yaml image: ruby:2.3 @@ -255,22 +347,38 @@ test: - master ``` -For this specific case, we need to exclude `/vendor` from Jekyll `_config.yml` file, otherwise Jekyll will understand it as a regular directory to build together with the site: +For this specific case, we need to exclude `/vendor` +from Jekyll `_config.yml` file, otherwise Jekyll will +understand it as a regular directory to build +together with the site: ```yml exclude: - vendor ``` -There we go! Now our GitLab CI not only builds our website, but also **continuously test** pushes to feature-branches, **caches** dependencies installed with Bundler, and **continuously deploy** every push to the `master` branch. +There we go! Now our GitLab CI not only builds our website, +but also **continuously test** pushes to feature-branches, +**caches** dependencies installed with Bundler, and +**continuously deploy** every push to the `master` branch. ### Advanced GitLab CI for GitLab Pages -What you can do with GitLab CI is pretty much up to your creativity. Once you get used to it, you start creating awesome scripts that automate most of tasks you'd do manually in the past. Read through the [documentation of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html) to understand how to go even further on your scripts. - -- On this blog post, understand the concept of [using GitLab CI `environments` to deploy your web app to staging and production](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/). -- On this post, learn [how to run jobs sequentially, in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/) -- On this blog post, we go through the process of [pulling specific directories from different projects](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/) to deploy this website you're looking at, docs.gitlab.com. +What you can do with GitLab CI is pretty much up to your +creativity. Once you get used to it, you start creating +awesome scripts that automate most of tasks you'd do +manually in the past. Read through the +[documentation of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html) +to understand how to go even further on your scripts. + +- On this blog post, understand the concept of +[using GitLab CI `environments` to deploy your +web app to staging and production](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/). +- On this post, learn [how to run jobs sequentially, +in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/) +- On this blog post, we go through the process of +[pulling specific directories from different projects](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/) +to deploy this website you're looking at, docs.gitlab.com. - On this blog post, we teach you [how to use GitLab Pages to produce a code coverage report](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/). ||| -- cgit v1.2.3 From 1a802b82d6c5c8a0fca20ab3eeb526416ef076a3 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Wed, 22 Feb 2017 13:37:15 -0300 Subject: remove <> --- doc/pages/getting_started_part_one.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/pages/getting_started_part_one.md b/doc/pages/getting_started_part_one.md index 57fc5d21b96..ed39301df39 100644 --- a/doc/pages/getting_started_part_one.md +++ b/doc/pages/getting_started_part_one.md @@ -47,8 +47,8 @@ which is highly recommendable and much faster than hardcoding. If you set up a GitLab Pages project on GitLab.com, it will automatically be accessible under a -[subdomain of `.pages.io`](https://docs.gitlab.com/ce/user/project/pages/). -The `` is defined by your username on GitLab.com, +[subdomain of `namespace.pages.io`](https://docs.gitlab.com/ce/user/project/pages/). +The `namespace` is defined by your username on GitLab.com, or the group name you created this project under. > Note: If you use your own GitLab instance to deploy your -- cgit v1.2.3 From 324260cef9ac8282bfb5b729b510f47e64b680fa Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Wed, 22 Feb 2017 13:39:57 -0300 Subject: remove html comments --- doc/pages/getting_started_part_two.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/doc/pages/getting_started_part_two.md b/doc/pages/getting_started_part_two.md index 15f628ac1fb..94b9d11e5d5 100644 --- a/doc/pages/getting_started_part_two.md +++ b/doc/pages/getting_started_part_two.md @@ -16,8 +16,6 @@ For a complete step-by-step tutorial, please read the blog post [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/). The following sections will explain what do you need and why do you need them. - - ### What You Need to Get Started 1. A project @@ -123,8 +121,6 @@ then add, commit, and push. ### URLs and Baseurls - - Every Static Site Generator (SSG) default configuration expects to find your website under a (sub)domain (`example.com`), not in a subdirectory of that domain (`example.com/subdir`). Therefore, -- cgit v1.2.3 From 49ec274359012e93411bf2b585373a34b2e0a2f4 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 22 Feb 2017 16:42:26 +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 521a9e39efc..a9a967fdbfd 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -218,8 +218,8 @@ .btn.btn-retry:hover, .btn.btn-retry:focus { - border-color: #c4c4c4; - background-color: #f0f0f0; + border-color: $gray-darkest; + background-color: $white-normal; } svg path { -- cgit v1.2.3 From 969754c25ad678081dca3359fde4080022bccaec Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 22 Feb 2017 18:44:31 +0100 Subject: Reorder main index items in Pages overview [ci skip] --- doc/pages/getting_started_part_one.md | 20 ++++++++++---------- doc/pages/getting_started_part_three.md | 31 ++++++++++++++----------------- doc/pages/getting_started_part_two.md | 14 +++++++------- doc/pages/index.md | 20 +++++++++++++++----- 4 files changed, 46 insertions(+), 39 deletions(-) diff --git a/doc/pages/getting_started_part_one.md b/doc/pages/getting_started_part_one.md index ed39301df39..c5b1aa4b654 100644 --- a/doc/pages/getting_started_part_one.md +++ b/doc/pages/getting_started_part_one.md @@ -1,9 +1,5 @@ # GitLab Pages from A to Z: Part 1 -> Type: user guide -> -> Level: beginner - - **Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates** - _[Part 2: Quick Start Guide - Setting Up GitLab Pages](getting_started_part_two.md)_ - _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md)_ @@ -19,14 +15,15 @@ and GitLab EE (Enterprise Edition), please read the [admin documentation](https://docs.gitlab.com/ce/administration/pages/index.html), and/or watch this [video tutorial](https://youtu.be/dD8c7WNcc6s). -> For this guide, we assume you already have GitLab Pages +>**Note:** +For this guide, we assume you already have GitLab Pages server up and running for your GitLab instance. ## What you need to know before getting started Before we begin, let's understand a few concepts first. -### Static Sites +### Static sites GitLab Pages only supports static websites, meaning, your output files must be HTML, CSS, and JavaScript only. @@ -43,7 +40,7 @@ which is highly recommendable and much faster than hardcoding. - You can use [any SSG with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) - Fork an [example project](https://gitlab.com/pages) to build your website based upon -### GitLab Pages Domain +### GitLab Pages domain If you set up a GitLab Pages project on GitLab.com, it will automatically be accessible under a @@ -51,7 +48,8 @@ it will automatically be accessible under a The `namespace` is defined by your username on GitLab.com, or the group name you created this project under. -> Note: If you use your own GitLab instance to deploy your +>**Note:** +If you use your own GitLab instance to deploy your site with GitLab Pages, check with your sysadmin what's your Pages wildcard domain. This guide is valid for any GitLab instance, you just need to replace Pages wildcard domain on GitLab.com @@ -180,7 +178,8 @@ up your Pages project with your custom (sub)domain, if you want it secured by HTTPS, you will have to issue a certificate for that (sub)domain and install it on your project. -> Note: certificates are NOT required to add to your custom +>**Note:** +Certificates are NOT required to add to your custom (sub)domain on your GitLab Pages project, though they are highly recommendable. @@ -257,7 +256,8 @@ and paste it in the [same field as your PEM certificate](https://about.gitlab.co just jumping a line between them. - Copy your public key and paste it in the last field -> Note: **do not** open certificates or encryption keys in +>**Note:** +**Do not** open certificates or encryption keys in regular text editors. Always use code editors (such as Sublime Text, Atom, Dreamweaver, Brackets, etc). diff --git a/doc/pages/getting_started_part_three.md b/doc/pages/getting_started_part_three.md index 8acf8a85d5a..ef47abef3a0 100644 --- a/doc/pages/getting_started_part_three.md +++ b/doc/pages/getting_started_part_three.md @@ -1,16 +1,12 @@ # GitLab Pages from A to Z: Part 3 -> Type: user guide -> -> Level: intermediate - - _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](getting_started_part_one.md)_ - _[Part 2: Quick Start Guide - Setting Up GitLab Pages](getting_started_part_two.md)_ - **Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages** --- -### Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages +## Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages [GitLab CI](https://about.gitlab.com/gitlab-ci/) serves numerous purposes, to build, test, and deploy your app @@ -50,7 +46,7 @@ $ gem install jekyll $ jekyll build ``` -#### Script +### Script To transpose this script to Yaml, it would be like this: @@ -60,7 +56,7 @@ script: - jekyll build ``` -#### Job +### Job So far so good. Now, each `script`, in GitLab is organized by a `job`, which is a bunch of scripts and settings you want to @@ -84,7 +80,7 @@ pages: - jekyll build ``` -#### `public` Dir +### The `public` directory We also need to tell Jekyll where do you want the website to build, and GitLab Pages will only consider files in a directory called `public`. @@ -100,7 +96,7 @@ pages: - jekyll build -d public ``` -#### Artifacts +### Artifacts We also need to tell the Runner that this _job_ generates _artifacts_, which is the site built by Jekyll. @@ -138,7 +134,7 @@ your Jekyll 3.4.0 site with GitLab Pages. This is the minimum configuration for our example. On the steps below, we'll refine the script by adding extra options to our GitLab CI. -#### Image +### Image At this point, you probably ask yourself: "okay, but to install Jekyll I need Ruby. Where is Ruby on that script?". The answer is simple: the @@ -168,7 +164,8 @@ need to specify which image you want to use, and this image should contain NodeJS as part of its file system. E.g., for a [Hexo](https://gitlab.com/pages/hexo) site, you can use `image: node:4.2.2`. -> Note: we're not trying to explain what a Docker image is, +>**Note:** +We're not trying to explain what a Docker image is, we just need to introduce the concept with a minimum viable explanation. To know more about Docker images, please visit their website or take a look at a @@ -176,7 +173,7 @@ their website or take a look at a Let's go a little further. -#### Branching +### Branching If you use GitLab as a version control platform, you will have your branching strategy to work on your project. Meaning, you will have @@ -199,7 +196,7 @@ pages: - master ``` -#### Stages +### Stages Another interesting concept to keep in mind are build stages. Your web app can pass through a lot of tests and other tasks @@ -272,7 +269,7 @@ tools much more powerful than that. This is what you need to be able to create and tweak your builds for your GitLab Pages site. -#### Before Script +### Before Script To avoid running the same script multiple times across your _jobs_, you can add the parameter `before_script`, @@ -308,7 +305,7 @@ test: - master ``` -#### Caching Dependencies +### Caching Dependencies If you want to cache the installation files for your projects dependencies, for building faster, you can @@ -362,7 +359,7 @@ but also **continuously test** pushes to feature-branches, **caches** dependencies installed with Bundler, and **continuously deploy** every push to the `master` branch. -### Advanced GitLab CI for GitLab Pages +## Advanced GitLab CI for GitLab Pages What you can do with GitLab CI is pretty much up to your creativity. Once you get used to it, you start creating @@ -383,4 +380,4 @@ to deploy this website you're looking at, docs.gitlab.com. ||| |:--|--:| -|[**← Part 1: Static sites, domains, DNS records, and SSL/TLS certificates**](getting_started_part_one.md)|[**Part 2: Quick start guide - Setting up GitLab Pages →**](getting_started_part_two.md)| +|[**← Part 2: Quick start guide - Setting up GitLab Pages**](getting_started_part_two.md)|| diff --git a/doc/pages/getting_started_part_two.md b/doc/pages/getting_started_part_two.md index 94b9d11e5d5..07dd24122c4 100644 --- a/doc/pages/getting_started_part_two.md +++ b/doc/pages/getting_started_part_two.md @@ -10,27 +10,27 @@ ---- -## Setting Up GitLab Pages +## Setting up GitLab Pages For a complete step-by-step tutorial, please read the blog post [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/). The following sections will explain what do you need and why do you need them. -### What You Need to Get Started +## What you need to get started 1. A project 1. A configuration file (`.gitlab-ci.yml`) to deploy your site 1. A specific `job` called `pages` in the configuration file that will make GitLab aware that you are deploying a GitLab Pages website -#### Optional Features +Optional Features: 1. A custom domain or subdomain 1. A DNS pointing your (sub)domain to your Pages site 1. **Optional**: an SSL/TLS certificate so your custom domain is accessible under HTTPS. -### Project +## Project Your GitLab Pages project is a regular project created the same way you do for the other ones. To get started with GitLab Pages, you have two ways: @@ -40,7 +40,7 @@ same way you do for the other ones. To get started with GitLab Pages, you have t Let's go over both options. -#### Fork a Project to Get Started From +### Fork a project to get started from To make things easy for you, we've created this [group](https://gitlab.com/pages) of default projects @@ -79,11 +79,11 @@ is useful for submitting merge requests to the upstream. configuration file. They're enabled by default to new projects, but not to forks. -#### Create a Project from Scratch +### Create a project from scratch 1. From your **Project**'s **[Dashboard](https://gitlab.com/dashboard/projects)**, click **New project**, and name it considering the -[pratical examples](getting_started_part_one.md#practical-examples). +[practical examples](getting_started_part_one.md#practical-examples). 1. Clone it to your local computer, add your website files to your project, add, commit and push to GitLab. 1. From the your **Project**'s page, click **Set up CI**: diff --git a/doc/pages/index.md b/doc/pages/index.md index 242fdf3147f..a6f928cc243 100644 --- a/doc/pages/index.md +++ b/doc/pages/index.md @@ -1,6 +1,13 @@ # All you need to know about GitLab Pages -## Product +With GitLab Pages you can create static websites for your GitLab projects, +groups, or user accounts. You can use any static website generator: Jekyll, +Middleman, Hexo, Hugo, Pelican, you name it! Connect as many customs domains +as you like and bring your own TLS certificate to secure them. + +Here's some info we have gathered to get you started. + +## General info - [Product webpage](https://pages.gitlab.io) - [We're bringing GitLab Pages to CE](https://about.gitlab.com/2016/12/24/were-bringing-gitlab-pages-to-community-edition/) @@ -8,12 +15,11 @@ ## Getting started -- [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) a comprehensive step-by-step guide - GitLab Pages from A to Z - [Part 1: Static sites, domains, DNS records, and SSL/TLS certificates](getting_started_part_one.md) - [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md) - - Video tutorial: [How to publish a website with GitLab Pages on GitLab.com: from a forked project](https://youtu.be/TWqh9MtT4Bg) - [Part 3: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md) +- [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) a comprehensive step-by-step guide - Secure GitLab Pages custom domain with SSL/TLS certificates - [Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/) - [CloudFlare](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) @@ -24,6 +30,11 @@ - [SSGs part 3: Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) - [Posting to your GitLab Pages blog from iOS](https://about.gitlab.com/2016/08/19/posting-to-your-gitlab-pages-blog-from-ios/) +## Video tutorials + +- [How to publish a website with GitLab Pages on GitLab.com: from a forked project](https://youtu.be/TWqh9MtT4Bg) +- [How to Enable GitLab Pages for GitLab CE and EE](https://youtu.be/dD8c7WNcc6s) + ## Advanced use - Blog Posts: @@ -32,8 +43,7 @@ - [Building a new GitLab docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/) - [Publish code coverage reports with GitLab Pages](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/) -## General documentation +## Specific documentation - [User docs](../user/project/pages/index.md) - [Admin docs](../administration/pages/index.md) - - Video tutorial - [How to Enable GitLab Pages for GitLab CE and EE](https://youtu.be/dD8c7WNcc6s) -- cgit v1.2.3 From 9f322f2b3866833af1bca7ecd3624428d968b017 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Tue, 21 Feb 2017 22:48:02 +0000 Subject: Added double newline after file upload markdown insert --- app/assets/javascripts/dropzone_input.js | 5 +++-- spec/features/issues_spec.rb | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 64a7a9eaf37..646f836aff0 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -126,13 +126,14 @@ require('./preview_markdown'); }; pasteText = function(text) { var afterSelection, beforeSelection, caretEnd, caretStart, textEnd; + var formattedText = text + "\n\n"; caretStart = $(child)[0].selectionStart; caretEnd = $(child)[0].selectionEnd; textEnd = $(child).val().length; beforeSelection = $(child).val().substring(0, caretStart); afterSelection = $(child).val().substring(caretEnd, textEnd); - $(child).val(beforeSelection + text + afterSelection); - child.get(0).setSelectionRange(caretStart + text.length, caretEnd + text.length); + $(child).val(beforeSelection + formattedText + afterSelection); + child.get(0).setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length); return form_textarea.trigger("input"); }; getFilename = function(e) { diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 094f645a077..ed3826bd46e 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -577,6 +577,15 @@ describe 'Issues', feature: true do expect(page.find_field("issue_description").value).to have_content 'banana_sample' end + + it 'adds double newline to end of attachment markdown' do + drop_in_dropzone test_image_file + + # Wait for the file to upload + sleep 1 + + expect(page.find_field("issue_description").value).to match /\n\n$/ + end end end -- cgit v1.2.3 From 165be6344de51e4db27456f8977da5105f9d33d5 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 22 Feb 2017 19:13:48 +0100 Subject: Add missing index.md to Pages docs [ci skip] --- doc/user/project/pages/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md index 816600964de..276fbd26835 100644 --- a/doc/user/project/pages/index.md +++ b/doc/user/project/pages/index.md @@ -444,6 +444,6 @@ For a list of known issues, visit GitLab's [public issue tracker]. [public issue tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Pages [ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605 [quick start guide]: ../../../ci/quick_start/README.md -[pages-index-guide]: ../../../pages/ +[pages-index-guide]: ../../../pages/index.md [pages-quick]: ../../../pages/getting_started_part_one.md [video-pages-fork]: https://youtu.be/TWqh9MtT4Bg -- cgit v1.2.3 From e2af9d0ab0fb2a5d631c0bbd4425d83a6d1203dc Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 22 Feb 2017 20:14:10 +0100 Subject: Change Pages redirect [ci skip] --- doc/pages/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index c9715eed598..afe62a2ee71 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -1 +1 @@ -This document was moved to [user/project/pages](../user/project/pages/index.md). +This document was moved to [index.md](index.md). -- cgit v1.2.3 From f7cd5fd79ab94c391e1a285973855b6c7b4452a1 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Wed, 22 Feb 2017 19:30:53 +0100 Subject: Ensure mutable uploads are not cached without revalidation --- app/controllers/uploads_controller.rb | 2 + ...-require-revalidation-on-each-browser-fetch.yml | 4 ++ spec/controllers/uploads_controller_spec.rb | 82 ++++++++++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 changelogs/unreleased/27936-make-all-uploads-require-revalidation-on-each-browser-fetch.yml diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index 509f4f412ca..f1bfd574f04 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -14,6 +14,8 @@ class UploadsController < ApplicationController end disposition = uploader.image? ? 'inline' : 'attachment' + + expires_in 0.seconds, must_revalidate: true, private: true send_file uploader.file.path, disposition: disposition end diff --git a/changelogs/unreleased/27936-make-all-uploads-require-revalidation-on-each-browser-fetch.yml b/changelogs/unreleased/27936-make-all-uploads-require-revalidation-on-each-browser-fetch.yml new file mode 100644 index 00000000000..adc129d8dca --- /dev/null +++ b/changelogs/unreleased/27936-make-all-uploads-require-revalidation-on-each-browser-fetch.yml @@ -0,0 +1,4 @@ +--- +title: Uploaded files which content can change now require revalidation on each page load +merge_request: 9453 +author: diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index c9584ddf18c..f67d26da0ac 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -1,4 +1,9 @@ require 'spec_helper' +shared_examples 'content not cached without revalidation' do + it 'ensures content will not be cached without revalidation' do + expect(subject['Cache-Control']).to eq('max-age=0, private, must-revalidate') + end +end describe UploadsController do let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } @@ -50,6 +55,13 @@ describe UploadsController do expect(response).to have_http_status(200) end + + it_behaves_like 'content not cached without revalidation' do + subject do + get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'image.png' + response + end + end end end @@ -59,6 +71,13 @@ describe UploadsController do expect(response).to have_http_status(200) end + + it_behaves_like 'content not cached without revalidation' do + subject do + get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'image.png' + response + end + end end end @@ -76,6 +95,13 @@ describe UploadsController do expect(response).to have_http_status(200) end + + it_behaves_like 'content not cached without revalidation' do + subject do + get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png' + response + end + end end context "when signed in" do @@ -88,6 +114,13 @@ describe UploadsController do expect(response).to have_http_status(200) end + + it_behaves_like 'content not cached without revalidation' do + subject do + get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png' + response + end + end end end @@ -133,6 +166,13 @@ describe UploadsController do expect(response).to have_http_status(200) end + + it_behaves_like 'content not cached without revalidation' do + subject do + get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png' + response + end + end end end @@ -157,6 +197,13 @@ describe UploadsController do expect(response).to have_http_status(200) end + + it_behaves_like 'content not cached without revalidation' do + subject do + get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png' + response + end + end end context "when signed in" do @@ -169,6 +216,13 @@ describe UploadsController do expect(response).to have_http_status(200) end + + it_behaves_like 'content not cached without revalidation' do + subject do + get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png' + response + end + end end end @@ -205,6 +259,13 @@ describe UploadsController do expect(response).to have_http_status(200) end + + it_behaves_like 'content not cached without revalidation' do + subject do + get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png' + response + end + end end end @@ -234,6 +295,13 @@ describe UploadsController do expect(response).to have_http_status(200) end + + it_behaves_like 'content not cached without revalidation' do + subject do + get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png' + response + end + end end context "when signed in" do @@ -246,6 +314,13 @@ describe UploadsController do expect(response).to have_http_status(200) end + + it_behaves_like 'content not cached without revalidation' do + subject do + get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png' + response + end + end end end @@ -291,6 +366,13 @@ describe UploadsController do expect(response).to have_http_status(200) end + + it_behaves_like 'content not cached without revalidation' do + subject do + get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png' + response + end + end end end -- cgit v1.2.3 From 17aae3bd34a467e2aed4c7063bc8bc3619d87e0f Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 22 Feb 2017 20:35:44 +0100 Subject: Remove Pages readme To prevent a redirect loop in docs.gitlab.com/ce/pages/ [ci skip] --- doc/pages/README.md | 1 - 1 file changed, 1 deletion(-) delete mode 100644 doc/pages/README.md diff --git a/doc/pages/README.md b/doc/pages/README.md deleted file mode 100644 index afe62a2ee71..00000000000 --- a/doc/pages/README.md +++ /dev/null @@ -1 +0,0 @@ -This document was moved to [index.md](index.md). -- cgit v1.2.3 From a942240ef5c5bef7e0d1fcf5294209a3a6b69bdf Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 22 Feb 2017 20:42:33 +0000 Subject: Ignore two Rails CVEs in bundler:audit job --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 433b3119fba..20f410d0b4c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -281,7 +281,7 @@ bundler:audit: - master@gitlab/gitlabhq - master@gitlab/gitlab-ee script: - - "bundle exec bundle-audit check --update --ignore OSVDB-115941" + - "bundle exec bundle-audit check --update --ignore OSVDB-115941 CVE-2016-6316 CVE-2016-6317" migration paths: stage: test -- cgit v1.2.3 From fcb15c5677b5e73590f96f6fbb754b86f0077f93 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Wed, 22 Feb 2017 11:38:10 -0700 Subject: Update Licensee from 8.0.0 to 8.7.0. Changelog: https://github.com/benbalter/licensee/releases --- Gemfile | 2 +- Gemfile.lock | 6 +++--- .../projects/files/project_owner_creates_license_file_spec.rb | 8 ++++---- ...wner_sees_link_to_create_license_file_in_empty_project_spec.rb | 4 ++-- spec/requests/api/templates_spec.rb | 8 ++++---- spec/requests/api/v3/templates_spec.rb | 8 ++++---- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Gemfile b/Gemfile index 01861f1ffac..3fc92abf9a7 100644 --- a/Gemfile +++ b/Gemfile @@ -201,7 +201,7 @@ gem 'babosa', '~> 1.0.2' gem 'loofah', '~> 2.0.3' # Working with license -gem 'licensee', '~> 8.0.0' +gem 'licensee', '~> 8.7.0' # Protect against bruteforcing gem 'rack-attack', '~> 4.4.1' diff --git a/Gemfile.lock b/Gemfile.lock index 2a3be763753..9d6100f66ac 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -398,8 +398,8 @@ GEM rubyzip thor xml-simple - licensee (8.0.0) - rugged (>= 0.24b) + licensee (8.7.0) + rugged (~> 0.24) little-plugger (1.1.4) logging (2.1.0) little-plugger (~> 1.1) @@ -907,7 +907,7 @@ DEPENDENCIES kubeclient (~> 2.2.0) letter_opener_web (~> 1.3.0) license_finder (~> 2.1.0) - licensee (~> 8.0.0) + licensee (~> 8.7.0) loofah (~> 2.0.3) mail_room (~> 0.9.1) method_source (~> 0.8) diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb index 64094af29c0..f8ef4577a26 100644 --- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb +++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb @@ -25,7 +25,7 @@ feature 'project owner creates a license file', feature: true, js: true do select_template('MIT License') file_content = first('.file-editor') - expect(file_content).to have_content('The MIT License (MIT)') + expect(file_content).to have_content('MIT License') expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") fill_in :commit_message, with: 'Add a LICENSE file', visible: true @@ -33,7 +33,7 @@ feature 'project owner creates a license file', feature: true, js: true do expect(current_path).to eq( namespace_project_blob_path(project.namespace, project, 'master/LICENSE')) - expect(page).to have_content('The MIT License (MIT)') + expect(page).to have_content('MIT License') expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") end @@ -49,7 +49,7 @@ feature 'project owner creates a license file', feature: true, js: true do select_template('MIT License') file_content = first('.file-editor') - expect(file_content).to have_content('The MIT License (MIT)') + expect(file_content).to have_content('MIT License') expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") fill_in :commit_message, with: 'Add a LICENSE file', visible: true @@ -57,7 +57,7 @@ feature 'project owner creates a license file', feature: true, js: true do expect(current_path).to eq( namespace_project_blob_path(project.namespace, project, 'master/LICENSE')) - expect(page).to have_content('The MIT License (MIT)') + expect(page).to have_content('MIT License') expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") end diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb index 4453b6d485f..420db962318 100644 --- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb +++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb @@ -24,7 +24,7 @@ feature 'project owner sees a link to create a license file in empty project', f select_template('MIT License') file_content = first('.file-editor') - expect(file_content).to have_content('The MIT License (MIT)') + expect(file_content).to have_content('MIT License') expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") fill_in :commit_message, with: 'Add a LICENSE file', visible: true @@ -34,7 +34,7 @@ feature 'project owner sees a link to create a license file in empty project', f expect(current_path).to eq( namespace_project_blob_path(project.namespace, project, 'master/LICENSE')) - expect(page).to have_content('The MIT License (MIT)') + expect(page).to have_content('MIT License') expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") end diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb index 8506e8fccde..2c83e119065 100644 --- a/spec/requests/api/templates_spec.rb +++ b/spec/requests/api/templates_spec.rb @@ -58,11 +58,11 @@ describe API::Templates, api: true do expect(json_response['popular']).to be true expect(json_response['html_url']).to eq('http://choosealicense.com/licenses/mit/') expect(json_response['source_url']).to eq('https://opensource.org/licenses/MIT') - expect(json_response['description']).to include('A permissive license that is short and to the point.') + expect(json_response['description']).to include('A short and simple permissive license with conditions') expect(json_response['conditions']).to eq(%w[include-copyright]) expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use]) expect(json_response['limitations']).to eq(%w[no-liability]) - expect(json_response['content']).to include('The MIT License (MIT)') + expect(json_response['content']).to include('MIT License') end end @@ -73,7 +73,7 @@ describe API::Templates, api: true do expect(response).to have_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.size).to eq(15) + expect(json_response.size).to eq(12) expect(json_response.map { |l| l['key'] }).to include('agpl-3.0') end @@ -102,7 +102,7 @@ describe API::Templates, api: true do let(:license_type) { 'mit' } it 'returns the license text' do - expect(json_response['content']).to include('The MIT License (MIT)') + expect(json_response['content']).to include('MIT License') end it 'replaces placeholder values' do diff --git a/spec/requests/api/v3/templates_spec.rb b/spec/requests/api/v3/templates_spec.rb index 4fd4e70bedd..f1e554b98cc 100644 --- a/spec/requests/api/v3/templates_spec.rb +++ b/spec/requests/api/v3/templates_spec.rb @@ -56,11 +56,11 @@ describe API::V3::Templates, api: true do expect(json_response['popular']).to be true expect(json_response['html_url']).to eq('http://choosealicense.com/licenses/mit/') expect(json_response['source_url']).to eq('https://opensource.org/licenses/MIT') - expect(json_response['description']).to include('A permissive license that is short and to the point.') + expect(json_response['description']).to include('A short and simple permissive license with conditions') expect(json_response['conditions']).to eq(%w[include-copyright]) expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use]) expect(json_response['limitations']).to eq(%w[no-liability]) - expect(json_response['content']).to include('The MIT License (MIT)') + expect(json_response['content']).to include('MIT License') end end @@ -70,7 +70,7 @@ describe API::V3::Templates, api: true do expect(response).to have_http_status(200) expect(json_response).to be_an Array - expect(json_response.size).to eq(15) + expect(json_response.size).to eq(12) expect(json_response.map { |l| l['key'] }).to include('agpl-3.0') end @@ -98,7 +98,7 @@ describe API::V3::Templates, api: true do let(:license_type) { 'mit' } it 'returns the license text' do - expect(json_response['content']).to include('The MIT License (MIT)') + expect(json_response['content']).to include('MIT License') end it 'replaces placeholder values' do -- cgit v1.2.3 From 064d44c74b064a4c5f5d495ac75d79d4d0e5a696 Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Wed, 22 Feb 2017 21:08:54 +0000 Subject: clarify custom domain details --- doc/administration/pages/index.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 1c444cf0d50..62b0468da79 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -26,22 +26,24 @@ it works. --- -In the case of custom domains, the Pages daemon needs to listen on ports `80` -and/or `443`. For that reason, there is some flexibility in the way which you -can set it up: +In the case of [custom domains](#custom-domains) (but not +[wildcard domains](#wildcard-domains)), the Pages daemon needs to listen on +ports `80` and/or `443`. For that reason, there is some flexibility in the way +which you can set it up: -1. Run the pages daemon in the same server as GitLab, listening on a secondary IP. -1. Run the pages daemon in a separate server. In that case, the +1. Run the Pages daemon in the same server as GitLab, listening on a secondary IP. +1. Run the Pages daemon in a separate server. In that case, the [Pages path](#change-storage-path) must also be present in the server that - the pages daemon is installed, so you will have to share it via network. -1. Run the pages daemon in the same server as GitLab, listening on the same IP + the Pages daemon is installed, so you will have to share it via network. +1. Run the Pages daemon in the same server as GitLab, listening on the same IP but on different ports. In that case, you will have to proxy the traffic with a loadbalancer. If you choose that route note that you should use TCP load balancing for HTTPS. If you use TLS-termination (HTTPS-load balancing) the pages will not be able to be served with user provided certificates. For HTTP it's OK to use HTTP or TCP load balancing. -In this document, we will proceed assuming the first option. +In this document, we will proceed assuming the first option. If you are not +supporting custom domains a secondary IP is not needed. ## Prerequisites @@ -54,6 +56,7 @@ Before proceeding with the Pages configuration, you will need to: serve Pages under HTTPS. 1. (Optional but recommended) Enable [Shared runners](../../ci/runners/README.md) so that your users don't have to bring their own. +1. (Only for custom domains) Have a **secondary IP**. ### DNS configuration @@ -150,7 +153,7 @@ that without TLS certificates. > URL scheme: `http://page.example.io` and `http://domain.com` -In that case, the pages daemon is running, Nginx still proxies requests to +In that case, the Pages daemon is running, Nginx still proxies requests to the daemon but the daemon is also able to receive requests from the outside world. Custom domains are supported, but no TLS. @@ -179,7 +182,7 @@ world. Custom domains are supported, but no TLS. > URL scheme: `https://page.example.io` and `https://domain.com` -In that case, the pages daemon is running, Nginx still proxies requests to +In that case, the Pages daemon is running, Nginx still proxies requests to the daemon but the daemon is also able to receive requests from the outside world. Custom domains and TLS are supported. -- cgit v1.2.3 From fa5443e3a930c6cd2dd573f5c5965e1a563bbe2e Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 22 Feb 2017 21:23:34 +0000 Subject: Equal padding between table cells --- .../vue_pipelines_index/time_ago.js.es6 | 31 ++++++++-------- app/assets/stylesheets/pages/pipelines.scss | 42 ++++------------------ 2 files changed, 22 insertions(+), 51 deletions(-) diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 index 3598da11573..d971588af96 100644 --- a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 @@ -55,21 +55,22 @@ require('../lib/utils/datetime_utility'); }, template: ` -

- - {{duration}} -

-

- - -

+
+

+ + {{duration}} +

+

+ + +

+
`, }); diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index a9a967fdbfd..bc0382fb908 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -1,7 +1,4 @@ .pipelines { - width: 100%; - overflow: auto; - .realtime-loading { font-size: 40px; text-align: center; @@ -16,17 +13,16 @@ white-space: nowrap; } - .commit-title { - margin: 0; + .table-holder { + width: 100%; + overflow: auto; } - .controls { - white-space: nowrap; + .commit-title { + margin: 0; } .table.ci-table { - width: 100%; - table-layout: fixed; .label { margin-bottom: 3px; @@ -36,21 +32,9 @@ color: $black; } - .pipeline-date, - .pipeline-status { - width: 10%; - } - - .pipeline-info, - .pipeline-commit, - .pipeline-stages { + .stage-cell { width: 20%; } - - .pipeline-actions { - width: 20%; - padding-right: 0; - } } } @@ -64,21 +48,7 @@ } } -.content-list.pipelines .table-holder { - min-height: 300px; -} - -.pipeline-holder { - width: 100%; - overflow: auto; -} - .table.ci-table { - min-width: 900px; - - &.pipeline { - min-width: 650px; - } &.builds-page { -- cgit v1.2.3 From e6c58aff0f2915e2e17879141924efc7b386fe0d Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 22 Feb 2017 16:56:52 -0600 Subject: remove require.context from filtered_search_bundle --- .../javascripts/filtered_search/filtered_search_bundle.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js index 392f1835966..faaba994f46 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_bundle.js +++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js @@ -1,3 +1,9 @@ -function requireAll(context) { return context.keys().map(context); } - -requireAll(require.context('./', true, /^\.\/(?!filtered_search_bundle).*\.(js|es6)$/)); +require('./dropdown_hint'); +require('./dropdown_non_user'); +require('./dropdown_user'); +require('./dropdown_utils'); +require('./filtered_search_dropdown_manager'); +require('./filtered_search_dropdown'); +require('./filtered_search_manager'); +require('./filtered_search_token_keys'); +require('./filtered_search_tokenizer'); -- cgit v1.2.3 From 87b19b8cae067bd328c1e6a1cec4fa06a73e8b96 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 22 Feb 2017 17:01:41 -0600 Subject: remove require.context from graphs_bundle --- app/assets/javascripts/graphs/graphs_bundle.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js index 4f7777aa5bc..086dcb34571 100644 --- a/app/assets/javascripts/graphs/graphs_bundle.js +++ b/app/assets/javascripts/graphs/graphs_bundle.js @@ -1,3 +1,4 @@ -// require everything else in this directory -function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('.', false, /^\.\/(?!graphs_bundle).*\.(js|es6)$/)); +require('./stat_graph_contributors_graph'); +require('./stat_graph_contributors_util'); +require('./stat_graph_contributors'); +require('./stat_graph'); -- cgit v1.2.3 From f326e6fb8d806ba443c64965a564d64a2f313a19 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 22 Feb 2017 17:04:53 -0600 Subject: remove require.context from network_bundle --- app/assets/javascripts/network/network_bundle.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js index aae509caa79..e5947586583 100644 --- a/app/assets/javascripts/network/network_bundle.js +++ b/app/assets/javascripts/network/network_bundle.js @@ -2,9 +2,8 @@ /* global Network */ /* global ShortcutsNetwork */ -// require everything else in this directory -function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('.', false, /^\.\/(?!network_bundle).*\.(js|es6)$/)); +require('./branch_graph'); +require('./network'); (function() { $(function() { -- cgit v1.2.3 From 5600b17de76c8bc93313adc1aa4d9859045de0a9 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 22 Feb 2017 17:42:11 -0500 Subject: Code style improvements --- app/views/groups/_head.html.haml | 19 +++++++++++++++++++ app/views/groups/_head_issues.html.haml | 19 +++++++++++++++++++ app/views/groups/activity.html.haml | 3 ++- app/views/groups/group_members/index.html.haml | 1 + app/views/groups/issues.html.haml | 1 + app/views/groups/labels/index.html.haml | 1 + app/views/groups/milestones/index.html.haml | 1 + app/views/groups/show.html.haml | 1 + app/views/layouts/nav/_group.html.haml | 20 ++------------------ .../26348-cleanup-navigation-order-groups.yml | 4 ++++ features/steps/group/milestones.rb | 4 +--- 11 files changed, 52 insertions(+), 22 deletions(-) create mode 100644 app/views/groups/_head.html.haml create mode 100644 app/views/groups/_head_issues.html.haml create mode 100644 changelogs/unreleased/26348-cleanup-navigation-order-groups.yml diff --git a/app/views/groups/_head.html.haml b/app/views/groups/_head.html.haml new file mode 100644 index 00000000000..6b296ea8dea --- /dev/null +++ b/app/views/groups/_head.html.haml @@ -0,0 +1,19 @@ += 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(path: 'groups#show', html_options: { class: 'home' }) do + = link_to group_path(@group), title: 'Group Home' do + %span + Home + + = nav_link(path: 'groups#activity') do + = link_to activity_group_path(@group), title: 'Activity' do + %span + Activity + + = nav_link(path: 'group_members#index') do + = link_to group_group_members_path(@group), title: 'Members' do + %span + Members diff --git a/app/views/groups/_head_issues.html.haml b/app/views/groups/_head_issues.html.haml new file mode 100644 index 00000000000..d554bc23743 --- /dev/null +++ b/app/views/groups/_head_issues.html.haml @@ -0,0 +1,19 @@ += 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(path: 'groups#issues', html_options: { class: 'home' }) do + = link_to issues_group_path(@group), title: 'List' do + %span + List + + = nav_link(path: 'labels#index') do + = link_to group_labels_path(@group), title: 'Labels' do + %span + Labels + + = nav_link(path: 'milestones#index') do + = link_to group_milestones_path(@group), title: 'Milestones' do + %span + Milestones diff --git a/app/views/groups/activity.html.haml b/app/views/groups/activity.html.haml index aaad265b3ee..d7375b23524 100644 --- a/app/views/groups/activity.html.haml +++ b/app/views/groups/activity.html.haml @@ -2,7 +2,8 @@ - if current_user = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity") -- page_title "Activity" +- page_title "Activity" += render 'groups/head' %section.activities = render 'activities' diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 2e4e4511bb6..8cb56443191 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -1,4 +1,5 @@ - page_title "Members" += render 'groups/head' .project-members-page.prepend-top-default %h4 diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 83edb719692..939bddf3fe9 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -1,4 +1,5 @@ - page_title "Issues" += render "head_issues" = content_for :meta_tags do - if current_user = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@group.name} issues") diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml index 45325d6bc4b..2bc00fb16c8 100644 --- a/app/views/groups/labels/index.html.haml +++ b/app/views/groups/labels/index.html.haml @@ -1,4 +1,5 @@ - page_title 'Labels' += render "groups/head_issues" .top-area.adjust .nav-text diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index cd5388fe402..644895c56a1 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -1,4 +1,5 @@ - page_title "Milestones" += render "groups/head_issues" .top-area = render 'shared/milestones_filter' diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index b040f404ac4..3d7b469660a 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -4,6 +4,7 @@ - if current_user = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity") += render 'groups/head' = render 'groups/home_panel' diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index f3539fd372d..e0742d70fac 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -5,23 +5,11 @@ .fade-right = icon('angle-right') %ul.nav-links.scrolling-tabs - = nav_link(path: 'groups#show', html_options: {class: 'home'}) do + = nav_link(path: ['groups#show', 'groups#activity', 'group_members#index'], html_options: { class: 'home' }) do = link_to group_path(@group), title: 'Home' do %span Group - = nav_link(path: 'groups#activity') do - = link_to activity_group_path(@group), title: 'Activity' do - %span - Activity - = nav_link(controller: [:group, :labels]) do - = link_to group_labels_path(@group), title: 'Labels' do - %span - Labels - = nav_link(controller: [:group, :milestones]) do - = link_to group_milestones_path(@group), title: 'Milestones' do - %span - Milestones - = nav_link(path: 'groups#issues') do + = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do = link_to issues_group_path(@group), title: 'Issues' do %span Issues @@ -33,7 +21,3 @@ Merge Requests - 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 - %span - Members diff --git a/changelogs/unreleased/26348-cleanup-navigation-order-groups.yml b/changelogs/unreleased/26348-cleanup-navigation-order-groups.yml new file mode 100644 index 00000000000..ce888baa32f --- /dev/null +++ b/changelogs/unreleased/26348-cleanup-navigation-order-groups.yml @@ -0,0 +1,4 @@ +--- +title: Clean-up Groups navigation order +merge_request: 9309 +author: diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb index 70e23098dde..20204ad8654 100644 --- a/features/steps/group/milestones.rb +++ b/features/steps/group/milestones.rb @@ -5,9 +5,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps include SharedUser step 'I click on group milestones' do - page.within('.layout-nav') do - click_link 'Milestones' - end + visit group_milestones_path('owned') end step 'I should see group milestones index page has no milestones' do -- cgit v1.2.3 From ed986806786d0c3fa5794409566e9e18a7916ed9 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 23 Feb 2017 11:44:49 +0100 Subject: Process skipped jobs in the pipeline when retrying it --- app/services/ci/retry_pipeline_service.rb | 19 +++++++++---------- lib/gitlab/optimistic_locking.rb | 4 +++- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb index ca19b828e1c..574561adc4c 100644 --- a/app/services/ci/retry_pipeline_service.rb +++ b/app/services/ci/retry_pipeline_service.rb @@ -1,22 +1,21 @@ module Ci class RetryPipelineService < ::BaseService + include Gitlab::OptimisticLocking + def execute(pipeline) unless can?(current_user, :update_pipeline, pipeline) raise Gitlab::Access::AccessDeniedError end - pipeline.builds.failed_or_canceled.tap do |builds| - stage_idx = builds.order('stage_idx ASC') - .pluck('DISTINCT stage_idx').first - - pipeline.mark_as_processable_after_stage(stage_idx) + pipeline.builds.failed_or_canceled.find_each do |build| + next unless build.retryable? - builds.find_each do |build| - next unless build.retryable? + Ci::RetryBuildService.new(project, current_user) + .reprocess(build) + end - Ci::RetryBuildService.new(project, current_user) - .reprocess(build) - end + pipeline.builds.skipped.find_each do |skipped| + retry_optimistic_lock(skipped) { |build| build.process } end MergeRequests::AddTodoWhenBuildFailsService diff --git a/lib/gitlab/optimistic_locking.rb b/lib/gitlab/optimistic_locking.rb index 879d46446b3..e0b2286f1dc 100644 --- a/lib/gitlab/optimistic_locking.rb +++ b/lib/gitlab/optimistic_locking.rb @@ -1,6 +1,6 @@ module Gitlab module OptimisticLocking - extend self + module_function def retry_lock(subject, retries = 100, &block) loop do @@ -15,5 +15,7 @@ module Gitlab end end end + + alias :retry_optimistic_lock :retry_lock end end -- cgit v1.2.3 From 7da0da3f20ab78fc57f4d5cc1152322049f18b1a Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 23 Feb 2017 10:53:41 +0000 Subject: Fix btn group not having round corners when there's only some buttons present --- .../vue_pipelines_index/pipeline_actions.js.es6 | 29 ++++++++-------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 index 89d501538f5..04029c47668 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 @@ -21,17 +21,14 @@
-
- +
-
+
@@ -64,17 +58,15 @@
  • + :href="artifact.path"> {{download(artifact.name)}}
  • -
    + -
    +
    Date: Tue, 14 Feb 2017 17:51:30 +0200 Subject: Rebase to master for avoiding failing tests --- .../unreleased/26875-builds-api-endpoint-skipped-scope.yml | 4 ++++ lib/api/builds.rb | 2 +- spec/requests/api/builds_spec.rb | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/26875-builds-api-endpoint-skipped-scope.yml diff --git a/changelogs/unreleased/26875-builds-api-endpoint-skipped-scope.yml b/changelogs/unreleased/26875-builds-api-endpoint-skipped-scope.yml new file mode 100644 index 00000000000..3d6400cba76 --- /dev/null +++ b/changelogs/unreleased/26875-builds-api-endpoint-skipped-scope.yml @@ -0,0 +1,4 @@ +--- +title: Add all available statuses to scope filter for project builds endpoint +merge_request: +author: George Andrinopoulos diff --git a/lib/api/builds.rb b/lib/api/builds.rb index 44fe0fc4a95..5b76913fe45 100644 --- a/lib/api/builds.rb +++ b/lib/api/builds.rb @@ -11,7 +11,7 @@ module API helpers do params :optional_scope do optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show', - values: ['pending', 'running', 'failed', 'success', 'canceled'], + values: ::CommitStatus::AVAILABLE_STATUSES, coerce_with: ->(scope) { if scope.is_a?(String) [scope] diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 38aef7f2767..76a10a2374c 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -16,6 +16,8 @@ describe API::Builds, api: true do let(:query) { '' } before do + create(:ci_build, :skipped, pipeline: pipeline) + get api("/projects/#{project.id}/builds?#{query}", api_user) end @@ -49,6 +51,18 @@ describe API::Builds, api: true do end end + context 'filter project with scope skipped' do + let(:query) { 'scope=skipped' } + let(:json_build) { json_response.first } + + it 'return builds with status skipped' do + expect(response).to have_http_status 200 + expect(json_response).to be_an Array + expect(json_response.length).to eq 1 + expect(json_build['status']).to eq 'skipped' + end + end + context 'filter project with array of scope elements' do let(:query) { 'scope[0]=pending&scope[1]=running' } -- cgit v1.2.3 From 02699f26a4fe067092fdc028738e56048a375453 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 23 Feb 2017 12:07:57 +0100 Subject: Add basic specs for optimistic locking mixin --- spec/lib/gitlab/optimistic_locking_spec.rb | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/spec/lib/gitlab/optimistic_locking_spec.rb b/spec/lib/gitlab/optimistic_locking_spec.rb index 498dc514c8c..acce2be93f2 100644 --- a/spec/lib/gitlab/optimistic_locking_spec.rb +++ b/spec/lib/gitlab/optimistic_locking_spec.rb @@ -1,10 +1,10 @@ require 'spec_helper' describe Gitlab::OptimisticLocking, lib: true do - describe '#retry_lock' do - let!(:pipeline) { create(:ci_pipeline) } - let!(:pipeline2) { Ci::Pipeline.find(pipeline.id) } + let!(:pipeline) { create(:ci_pipeline) } + let!(:pipeline2) { Ci::Pipeline.find(pipeline.id) } + describe '#retry_lock' do it 'does not reload object if state changes' do expect(pipeline).not_to receive(:reload) expect(pipeline).to receive(:succeed).and_call_original @@ -36,4 +36,17 @@ describe Gitlab::OptimisticLocking, lib: true do end.to raise_error(ActiveRecord::StaleObjectError) end end + + describe '#retry_optimistic_lock' do + context 'when locking module is mixed in' do + let(:unlockable) do + Class.new.include(described_class).new + end + + it 'is an alias for retry_lock' do + expect(unlockable.method(:retry_optimistic_lock)) + .to eq unlockable.method(:retry_lock) + end + end + end end -- cgit v1.2.3 From f1971da939f211e0fff298d59e822b5e4d90aa60 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 23 Feb 2017 11:13:51 +0000 Subject: Guarantees buttons will always stay in the same line --- app/assets/stylesheets/pages/pipelines.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index bc0382fb908..b67f6b731a8 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -181,6 +181,7 @@ .pipeline-actions { padding-right: 0; + min-width: 170px; //Guarantees buttons don't break in several lines. .btn-default { color: $gl-text-color-secondary; @@ -317,7 +318,7 @@ } .pipeline-actions { - min-width: initial; + min-width: 170px; } } -- cgit v1.2.3 From 2f0599b6c851c208b5332f37509df0ada52e6d6a Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Thu, 23 Feb 2017 12:36:23 +0100 Subject: Disable unused tags count cache for Projects, Builds and Runners + remove complete leftover when Issues were tagged using acts_as_taggable --- app/models/issue.rb | 2 -- app/models/project.rb | 3 +-- changelogs/unreleased/27989-disable-counting-tags.yml | 4 ++++ config/initializers/acts_as_taggable.rb | 5 +++++ 4 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/27989-disable-counting-tags.yml create mode 100644 config/initializers/acts_as_taggable.rb diff --git a/app/models/issue.rb b/app/models/issue.rb index d8826b65fcc..de90f19f854 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -15,8 +15,6 @@ class Issue < ActiveRecord::Base DueThisWeek = DueDateStruct.new('Due This Week', 'week').freeze DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze - ActsAsTaggableOn.strict_case_match = true - belongs_to :project belongs_to :moved_to, class_name: 'Issue' diff --git a/app/models/project.rb b/app/models/project.rb index 411299eef63..96ab7fdc99d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -70,8 +70,7 @@ class Project < ActiveRecord::Base after_validation :check_pending_delete - ActsAsTaggableOn.strict_case_match = true - acts_as_taggable_on :tags + acts_as_taggable attr_accessor :new_default_branch attr_accessor :old_path_with_namespace diff --git a/changelogs/unreleased/27989-disable-counting-tags.yml b/changelogs/unreleased/27989-disable-counting-tags.yml new file mode 100644 index 00000000000..988785ac454 --- /dev/null +++ b/changelogs/unreleased/27989-disable-counting-tags.yml @@ -0,0 +1,4 @@ +--- +title: Disable unused tags count cache for Projects, Builds and Runners +merge_request: +author: diff --git a/config/initializers/acts_as_taggable.rb b/config/initializers/acts_as_taggable.rb new file mode 100644 index 00000000000..c564c0cab11 --- /dev/null +++ b/config/initializers/acts_as_taggable.rb @@ -0,0 +1,5 @@ +ActsAsTaggableOn.strict_case_match = true + +# tags_counter enables caching count of tags which results in an update whenever a tag is added or removed +# since the count is not used anywhere its better performance wise to disable this cache +ActsAsTaggableOn.tags_counter = false -- cgit v1.2.3 From 7676b1fc93d8457ed66962d5ac615c7235d18e86 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 23 Feb 2017 11:34:38 +0000 Subject: Aligns text content instead of div --- .../vue_pipelines_index/time_ago.js.es6 | 32 ++--- app/assets/stylesheets/pages/pipelines.scss | 156 +++++++++------------ 2 files changed, 82 insertions(+), 106 deletions(-) diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 index d971588af96..6048fa691dc 100644 --- a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 @@ -54,23 +54,21 @@ require('../lib/utils/datetime_utility'); }, }, template: ` - -
    -

    - - {{duration}} -

    -

    - - -

    -
    + +

    + + {{duration}} +

    +

    + + +

    `, }); diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index b67f6b731a8..9f3b3a3a3bc 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -35,6 +35,69 @@ .stage-cell { width: 20%; } + + .pipelines-time-ago { + text-align: right; + } + + .pipeline-actions { + padding-right: 0; + min-width: 170px; //Guarantees buttons don't break in several lines. + + .btn-default { + color: $gl-text-color-secondary; + } + + .btn.btn-retry:hover, + .btn.btn-retry:focus { + border-color: $gray-darkest; + background-color: $white-normal; + } + + svg path { + fill: $gl-text-color-secondary; + } + + .dropdown-menu { + max-height: 250px; + overflow-y: auto; + } + + .dropdown-toggle, + .dropdown-menu { + color: $gl-text-color-secondary; + + .fa { + color: $gl-text-color-secondary; + font-size: 14px; + } + + svg, + .fa { + margin-right: 0; + } + } + + .btn-group { + &.open { + .btn-default { + background-color: $white-normal; + border-color: $border-white-normal; + } + } + + .btn { + .icon-play { + height: 13px; + width: 12px; + } + } + } + + .tooltip { + white-space: nowrap; + } + } } } @@ -50,11 +113,8 @@ .table.ci-table { - &.builds-page { - - tr { - height: 71px; - } + &.builds-page tr { + height: 71px; } tr { @@ -179,65 +239,6 @@ } } - .pipeline-actions { - padding-right: 0; - min-width: 170px; //Guarantees buttons don't break in several lines. - - .btn-default { - color: $gl-text-color-secondary; - } - - .btn.btn-retry:hover, - .btn.btn-retry:focus { - border-color: $gray-darkest; - background-color: $white-normal; - } - - svg path { - fill: $gl-text-color-secondary; - } - - .dropdown-menu { - max-height: 250px; - overflow-y: auto; - } - - .dropdown-toggle, - .dropdown-menu { - color: $gl-text-color-secondary; - - .fa { - color: $gl-text-color-secondary; - font-size: 14px; - } - - svg, - .fa { - margin-right: 0; - } - } - - .btn-group { - &.open { - .btn-default { - background-color: $white-normal; - border-color: $border-white-normal; - } - } - - .btn { - .icon-play { - height: 13px; - width: 12px; - } - } - } - - .tooltip { - white-space: nowrap; - } - } - .build-link a { color: $gl-text-color; } @@ -303,31 +304,8 @@ } .tab-pane { - &.pipelines { - .ci-table { - min-width: 900px; - } - - .content-list.pipelines { - overflow: auto; - } - - .stage { - max-width: 100px; - width: 100px; - } - - .pipeline-actions { - min-width: 170px; - } - } - - &.builds { - .ci-table { - tr { - height: 71px; - } - } + &.builds .ci-table tr { + height: 71px; } } -- cgit v1.2.3 From 5a2c68811712aecdece01ed203cda2ae2bb32ca9 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 9 Feb 2017 12:29:56 +0000 Subject: Fix MR changes tab size count This was wrong when there were over 100 files in the diff, because we did not use the same diff options as subclasses of `Gitlab::Diff::FileCollection::Base` when getting the raw diffs. (The reason we don't use those classes directly is because they may perform highlighting, which isn't needed for just counting the diffs.) --- app/models/merge_request.rb | 6 ++- .../unreleased/fix-mr-size-with-over-100-files.yml | 4 ++ spec/models/merge_request_spec.rb | 44 ++++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/fix-mr-size-with-over-100-files.yml diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 204d2b153ad..9076179b0b3 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -203,7 +203,11 @@ class MergeRequest < ActiveRecord::Base end def diff_size - opts = diff_options || {} + # The `#diffs` method ends up at an instance of a class inheriting from + # `Gitlab::Diff::FileCollection::Base`, so use those options as defaults + # here too, to get the same diff size without performing highlighting. + # + opts = Gitlab::Diff::FileCollection::Base.default_options.merge(diff_options || {}) raw_diffs(opts).size end diff --git a/changelogs/unreleased/fix-mr-size-with-over-100-files.yml b/changelogs/unreleased/fix-mr-size-with-over-100-files.yml new file mode 100644 index 00000000000..eecf3c99a75 --- /dev/null +++ b/changelogs/unreleased/fix-mr-size-with-over-100-files.yml @@ -0,0 +1,4 @@ +--- +title: Fix MR changes tab size count when there are over 100 files in the diff +merge_request: +author: diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index a01741a9971..fa1b0396bcf 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -209,6 +209,50 @@ describe MergeRequest, models: true do end end + describe '#diff_size' do + let(:merge_request) do + build(:merge_request, source_branch: 'expand-collapse-files', target_branch: 'master') + end + + context 'when there are MR diffs' do + before do + merge_request.save + end + + it 'returns the correct count' do + expect(merge_request.diff_size).to eq(105) + end + + it 'does not perform highlighting' do + expect(Gitlab::Diff::Highlight).not_to receive(:new) + + merge_request.diff_size + end + end + + context 'when there are no MR diffs' do + before do + merge_request.compare = CompareService.new( + merge_request.source_project, + merge_request.source_branch + ).execute( + merge_request.target_project, + merge_request.target_branch + ) + end + + it 'returns the correct count' do + expect(merge_request.diff_size).to eq(105) + end + + it 'does not perform highlighting' do + expect(Gitlab::Diff::Highlight).not_to receive(:new) + + merge_request.diff_size + end + end + end + describe "#related_notes" do let!(:merge_request) { create(:merge_request) } -- cgit v1.2.3 From 4792eb7ab427ec2568b932b5b5714d55f55fdeb3 Mon Sep 17 00:00:00 2001 From: Nur Rony Date: Mon, 13 Feb 2017 16:10:39 +0600 Subject: fixes job dropdown action button error --- app/assets/javascripts/vue_pipelines_index/stage.js.es6 | 16 ++++++++++------ .../27530-fix-job-dropdown-pipeline-console-error.yml | 4 ++++ 2 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 changelogs/unreleased/27530-fix-job-dropdown-pipeline-console-error.yml diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 index 8cc417a9966..cfe6ea10140 100644 --- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 @@ -37,15 +37,19 @@ return flash; }); }, + /** + * When the user right clicks or cmd/ctrl + click in the job name or action icon, + * the dropdown should not be closed and the link should open in another tab. + * If the target is a svg we stop propagation in order to prevent + * the default behavior of the dropdown. + */ keepGraph(e) { const { target } = e; + const svgClassName = target.getAttribute('class'); + const svgParentClassName = target.parentElement && target.parentElement.getAttribute('class'); - if (target.className.indexOf('js-ci-action-icon') >= 0) return null; - - if ( - target.parentElement && - (target.parentElement.className.indexOf('js-ci-action-icon') >= 0) - ) return null; + if (svgClassName && svgClassName.indexOf('js-ci-action-icon') >= 0) return null; + if (svgParentClassName && svgParentClassName.indexOf('js-ci-action-icon') >= 0) return null; return e.stopPropagation(); }, diff --git a/changelogs/unreleased/27530-fix-job-dropdown-pipeline-console-error.yml b/changelogs/unreleased/27530-fix-job-dropdown-pipeline-console-error.yml new file mode 100644 index 00000000000..4436b4bee68 --- /dev/null +++ b/changelogs/unreleased/27530-fix-job-dropdown-pipeline-console-error.yml @@ -0,0 +1,4 @@ +--- +title: Fixes job dropdown action throws error in js console +merge_request: 9182 +author: -- cgit v1.2.3 From 238ca048db3c3b93f34e92dcb709789082646932 Mon Sep 17 00:00:00 2001 From: Nur Rony Date: Mon, 20 Feb 2017 19:37:02 +0600 Subject: adds mount function with prevent and removes keepGraph function --- .../javascripts/vue_pipelines_index/stage.js.es6 | 36 ++++++++++------------ 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 index cfe6ea10140..7082df2fcea 100644 --- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 @@ -23,6 +23,19 @@ required: true, }, }, + mounted() { + /** + * When the user right clicks or cmd/ctrl + click in the job name or action icon, + * the dropdown should not be closed and the link should open in another tab. + * If the target is a svg we stop propagation in order to prevent + * the default behavior of the dropdown. + */ + console.log('I am called'); + $('.js-builds-dropdown-list').on('click', (e) => { + console.log('i am in event'); + e.stopPropagation(); + }); + }, methods: { fetchBuilds(e) { const areaExpanded = e.currentTarget.attributes['aria-expanded']; @@ -37,22 +50,6 @@ return flash; }); }, - /** - * When the user right clicks or cmd/ctrl + click in the job name or action icon, - * the dropdown should not be closed and the link should open in another tab. - * If the target is a svg we stop propagation in order to prevent - * the default behavior of the dropdown. - */ - keepGraph(e) { - const { target } = e; - const svgClassName = target.getAttribute('class'); - const svgParentClassName = target.parentElement && target.parentElement.getAttribute('class'); - - if (svgClassName && svgClassName.indexOf('js-ci-action-icon') >= 0) return null; - if (svgParentClassName && svgParentClassName.indexOf('js-ci-action-icon') >= 0) return null; - - return e.stopPropagation(); - }, }, computed: { buildsOrSpinner() { @@ -80,13 +77,13 @@ template: `
    +
    +
    +
    +
    +

    + Customize your experience +

    +

    + Change syntax themes, default project pages, and more in preferences. +

    + Check it out +
    +
    +
    `; + class UserCallout { constructor() { this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE); + this.userCalloutBody = $(userCalloutElementName); + this.userCalloutSvg = $(userCalloutElementName).attr(userCalloutSvgAttrName); + $(userCalloutElementName).removeAttr(userCalloutSvgAttrName); this.init(); - this.toggleUserCallout(); } init() { - $(document) - .on('click', closeButton, () => this.dismissCallout()) - .on('click', userCalloutBtn, () => this.dismissCallout()); + const $template = $(USER_CALLOUT_TEMPLATE); + if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') { + $template.find('.svg-container').append(this.userCalloutSvg); + this.userCalloutBody.append($template); + $template.find(closeButton).on('click', e => this.dismissCallout(e)); + $template.find(userCalloutBtn).on('click', e => this.dismissCallout(e)); + } } - dismissCallout() { + dismissCallout(e) { Cookies.set(USER_CALLOUT_COOKIE, 'true'); - } - - toggleUserCallout() { - if (!this.isCalloutDismissed) { - $(userCalloutElementName).show(); + const $currentTarget = $(e.currentTarget); + if ($currentTarget.hasClass('close-user-callout')) { + this.userCalloutBody.empty(); } } } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index dd252bf1e57..aad1a8986b0 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -279,7 +279,6 @@ table.u2f-registrations { } .user-callout { - display: none; margin: 24px auto 0; .bordered-box { diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index 8276cce693c..b82b933c3ad 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -5,7 +5,7 @@ - page_title "Projects" - header_title "Projects", dashboard_projects_path -= render partial: 'shared/user_callout' +.user-callout{ 'callout-svg' => custom_icon('icon_customization') } - if @projects.any? || params[:filter_projects] = render 'dashboard/projects_head' diff --git a/app/views/shared/_user_callout.html.haml b/app/views/shared/_user_callout.html.haml deleted file mode 100644 index 3f310251568..00000000000 --- a/app/views/shared/_user_callout.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -.user-callout - .bordered-box.landing.content-block - %button.btn.btn-default.close.close-user-callout{ type: "button" } - = icon("times", class: "dismiss-icon") - .row - .col-sm-3.col-xs-12.svg-container - = custom_icon('icon_customization') - .col-sm-8.col-xs-12.inner-content - %h4 - Customize your experience - %p - Change syntax themes, default project pages, and more in preferences. - - = link_to "Check it out", profile_preferences_path, class: 'btn user-callout-btn' diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 6203c668ff5..c130f3d9e17 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -96,9 +96,9 @@ %li.js-snippets-tab = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do Snippets - + %div{ class: container_class } - = render partial: 'shared/user_callout' + .user-callout{ 'callout-svg' => custom_icon('icon_customization') } .tab-content #activity.tab-pane .row-content-block.calender-block.white.second-block.hidden-xs diff --git a/spec/javascripts/fixtures/user_callout.html.haml b/spec/javascripts/fixtures/user_callout.html.haml index ad564469a27..275359bde0a 100644 --- a/spec/javascripts/fixtures/user_callout.html.haml +++ b/spec/javascripts/fixtures/user_callout.html.haml @@ -1,13 +1,2 @@ -.user-callout - .bordered-box.landing.content-block - %button.btn.btn-default.close.close-user-callout{ type: "button" } - %i.fa.fa-times.dismiss-icon - .row - .col-sm-3.col-xs-12.svg-container - .col-sm-8.col-xs-12.inner-content - %h4 - Customize your experience - %p - Change syntax themes, default project pages, and more in preferences. - %a{ href: 'foo', class:'user-callout-btn' } - Check it out +.user-callout{ 'callout-svg' => custom_icon('icon_customization') } + diff --git a/spec/javascripts/user_callout_spec.js b/spec/javascripts/user_callout_spec.js index 097368db6e5..e174fdf35de 100644 --- a/spec/javascripts/user_callout_spec.js +++ b/spec/javascripts/user_callout_spec.js @@ -11,22 +11,22 @@ describe('UserCallout', function () { loadFixtures(fixtureName); this.userCallout = new UserCallout(); this.closeButton = $('.close-user-callout'); - this.userCalloutContainer = $('.user-callout'); this.userCalloutBtn = $('.user-callout-btn'); + this.userCalloutContainer = $('.user-callout'); Cookie.set(USER_CALLOUT_COOKIE, 'false'); }); - it('shows when cookie is set to false', () => { + fit('shows when cookie is set to false', () => { expect(Cookie.get(USER_CALLOUT_COOKIE)).toBeDefined(); expect(this.userCalloutContainer.is(':visible')).toBe(true); }); - it('hides when user clicks on the dismiss-icon', () => { + fit('hides when user clicks on the dismiss-icon', () => { this.closeButton.click(); expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true'); }); - it('hides when user clicks on the "check it out" button', () => { + fit('hides when user clicks on the "check it out" button', () => { this.userCalloutBtn.click(); expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true'); }); -- cgit v1.2.3 From 2b001d9e7a2136a6d670576843a953115a5c7c25 Mon Sep 17 00:00:00 2001 From: Oswaldo Date: Wed, 22 Feb 2017 14:37:13 -0300 Subject: Return 202 with JSON body on async removals on V4 API --- ...74-correctly-return-json-on-delete-responses.yml | 4 ++++ doc/api/v3_to_v4.md | 2 ++ lib/api/branches.rb | 2 +- lib/api/helpers.rb | 4 ++++ lib/api/projects.rb | 2 ++ lib/api/v3/branches.rb | 7 +++++++ spec/requests/api/branches_spec.rb | 6 ++++-- spec/requests/api/projects_spec.rb | 8 ++++++-- spec/requests/api/v3/branches_spec.rb | 21 +++++++++++++++++++++ 9 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 changelogs/unreleased/3874-correctly-return-json-on-delete-responses.yml diff --git a/changelogs/unreleased/3874-correctly-return-json-on-delete-responses.yml b/changelogs/unreleased/3874-correctly-return-json-on-delete-responses.yml new file mode 100644 index 00000000000..4a4932288b4 --- /dev/null +++ b/changelogs/unreleased/3874-correctly-return-json-on-delete-responses.yml @@ -0,0 +1,4 @@ +--- +title: Return 202 with JSON body on async removals on V4 API +merge_request: +author: diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 9a48d63c117..e141723b580 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -41,3 +41,5 @@ changes are in V4: - Renamed `branch_name` to `branch` on DELETE `id/repository/branches/:branch` response [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936) - Remove `public` param from create and edit actions of projects [!8736](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8736) - Notes do not return deprecated field `upvote` and `downvote` [!9384](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9384) +- Return 202 with JSON body on async removals on V4 API (DELETE `/projects/:id/repository/merged_branches` and DELETE `/projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449) + diff --git a/lib/api/branches.rb b/lib/api/branches.rb index c65de90cca2..34f136948c2 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -137,7 +137,7 @@ module API delete ":id/repository/merged_branches" do DeleteMergedBranchesService.new(user_project, current_user).async_execute - status(200) + accepted! end end end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index d0efa7b993b..72d2b320077 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -209,6 +209,10 @@ module API render_api_error!('204 No Content', 204) end + def accepted! + render_api_error!('202 Accepted', 202) + end + def render_validation_error!(model) if model.errors.any? render_api_error!(model.errors.messages || '400 Bad Request', 400) diff --git a/lib/api/projects.rb b/lib/api/projects.rb index e7b891bd92e..b89bddc7e29 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -282,6 +282,8 @@ module API delete ":id" do authorize! :remove_project, user_project ::Projects::DestroyService.new(user_project, current_user, {}).async_execute + + accepted! end desc 'Mark this project as forked from another' diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb index 733c6b21be5..51eb566cf7d 100644 --- a/lib/api/v3/branches.rb +++ b/lib/api/v3/branches.rb @@ -18,6 +18,13 @@ module API present branches, with: ::API::Entities::RepoBranch, project: user_project end + + desc 'Delete all merged branches' + delete ":id/repository/merged_branches" do + DeleteMergedBranchesService.new(user_project, current_user).async_execute + + status(200) + end end end end diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index 5571f6cc107..cacdb21c692 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -360,9 +360,11 @@ describe API::Branches, api: true do allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true) end - it 'returns 200' do + it 'returns 202 with json body' do delete api("/projects/#{project.id}/repository/merged_branches", user) - expect(response).to have_http_status(200) + + expect(response).to have_http_status(202) + expect(json_response['message']).to eql('202 Accepted') end it 'returns a 403 error if guest' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 8d139782fdf..5de4426f3bd 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1263,7 +1263,9 @@ describe API::Projects, api: true do context 'when authenticated as user' do it 'removes project' do delete api("/projects/#{project.id}", user) - expect(response).to have_http_status(200) + + expect(response).to have_http_status(202) + expect(json_response['message']).to eql('202 Accepted') end it 'does not remove a project if not an owner' do @@ -1287,7 +1289,9 @@ describe API::Projects, api: true do context 'when authenticated as admin' do it 'removes any existing project' do delete api("/projects/#{project.id}", admin) - expect(response).to have_http_status(200) + + expect(response).to have_http_status(202) + expect(json_response['message']).to eql('202 Accepted') end it 'does not remove a non existing project' do diff --git a/spec/requests/api/v3/branches_spec.rb b/spec/requests/api/v3/branches_spec.rb index 0e4c6bc3bc6..a3e1581fcc5 100644 --- a/spec/requests/api/v3/branches_spec.rb +++ b/spec/requests/api/v3/branches_spec.rb @@ -20,4 +20,25 @@ describe API::V3::Branches, api: true do expect(branch_names).to match_array(project.repository.branch_names) end end + + describe "DELETE /projects/:id/repository/merged_branches" do + before do + allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true) + end + + it 'returns 200' do + delete v3_api("/projects/#{project.id}/repository/merged_branches", user) + + expect(response).to have_http_status(200) + end + + it 'returns a 403 error if guest' do + user_b = create :user + create(:project_member, :guest, user: user_b, project: project) + + delete v3_api("/projects/#{project.id}/repository/merged_branches", user_b) + + expect(response).to have_http_status(403) + end + end end -- cgit v1.2.3 From 25818010b6df75409caad46f20b1c40370357e4e Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 23 Feb 2017 17:40:36 -0600 Subject: remove require.context from profile_bundle --- app/assets/javascripts/profile/profile_bundle.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js index d7f3c9fd37e..15d32825583 100644 --- a/app/assets/javascripts/profile/profile_bundle.js +++ b/app/assets/javascripts/profile/profile_bundle.js @@ -1,3 +1,2 @@ -// require everything else in this directory -function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('.', false, /^\.\/(?!profile_bundle).*\.(js|es6)$/)); +require('./gl_crop'); +require('./profile'); -- cgit v1.2.3 From 1adc59acc1252ed8e18c0299decaf277d361f43d Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 23 Feb 2017 17:46:10 -0600 Subject: remove require.context from protected_branches_bundle --- .../javascripts/protected_branches/protected_branches_bundle.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js index ffb66caf5f4..849c1e31623 100644 --- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js +++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js @@ -1,3 +1,5 @@ -// require everything else in this directory -function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('.', false, /^\.\/(?!protected_branches_bundle).*\.(js|es6)$/)); +require('./protected_branch_access_dropdown'); +require('./protected_branch_create'); +require('./protected_branch_dropdown'); +require('./protected_branch_edit'); +require('./protected_branch_edit_list'); -- cgit v1.2.3 From a930fc5b4068ddcd190a45a85da4c8d79db35fe3 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 23 Feb 2017 17:53:01 -0600 Subject: remove require.context from snippet_bundle --- app/assets/javascripts/snippet/snippet_bundle.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js index 89822246bb8..a98403f4cf2 100644 --- a/app/assets/javascripts/snippet/snippet_bundle.js +++ b/app/assets/javascripts/snippet/snippet_bundle.js @@ -1,10 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, max-len */ /* global ace */ -// require everything else in this directory -function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('.', false, /^\.\/(?!snippet_bundle).*\.(js|es6)$/)); - (function() { $(function() { var editor = ace.edit("editor"); -- cgit v1.2.3 From ad640bc5f92ef0456a53dd82004f449cd9d7475d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 23 Feb 2017 17:55:01 -0600 Subject: Use Namespace#full_path instead of #path where appropriate --- app/assets/javascripts/milestone_select.js | 5 +- app/helpers/issuables_helper.rb | 2 +- app/models/project.rb | 10 +-- app/models/user.rb | 2 +- app/views/devise/shared/_signup_box.html.haml | 2 +- app/views/projects/edit.html.haml | 2 +- app/views/shared/_group_form.html.haml | 2 +- app/views/shared/issuable/_sidebar.html.haml | 2 +- lib/gitlab/import_export.rb | 2 +- lib/gitlab/regex.rb | 11 ++-- spec/controllers/blob_controller_spec.rb | 8 +-- spec/controllers/projects/blame_controller_spec.rb | 4 +- spec/controllers/projects/blob_controller_spec.rb | 12 ++-- .../projects/boards/issues_controller_spec.rb | 6 +- .../projects/boards/lists_controller_spec.rb | 10 +-- .../controllers/projects/boards_controller_spec.rb | 8 +-- .../projects/branches_controller_spec.rb | 28 ++++----- .../controllers/projects/commit_controller_spec.rb | 52 ++++++++-------- .../projects/commits_controller_spec.rb | 8 +-- .../projects/compare_controller_spec.rb | 32 +++++----- .../projects/cycle_analytics_controller_spec.rb | 8 +-- .../projects/find_file_controller_spec.rb | 8 +-- spec/controllers/projects/forks_controller_spec.rb | 12 ++-- .../controllers/projects/graphs_controller_spec.rb | 2 +- .../projects/group_links_controller_spec.rb | 12 ++-- .../projects/imports_controller_spec.rb | 18 +++--- .../controllers/projects/issues_controller_spec.rb | 54 ++++++++-------- .../controllers/projects/labels_controller_spec.rb | 16 ++--- .../projects/mattermosts_controller_spec.rb | 4 +- .../projects/merge_requests_controller_spec.rb | 68 ++++++++++---------- .../projects/pipelines_controller_spec.rb | 8 +-- .../projects/protected_branches_controller_spec.rb | 2 +- spec/controllers/projects/raw_controller_spec.rb | 8 +-- spec/controllers/projects/refs_controller_spec.rb | 4 +- .../projects/releases_controller_spec.rb | 6 +- .../projects/repositories_controller_spec.rb | 6 +- .../projects/snippets_controller_spec.rb | 36 +++++------ spec/controllers/projects/tags_controller_spec.rb | 4 +- .../projects/templates_controller_spec.rb | 8 +-- spec/controllers/projects/todo_controller_spec.rb | 8 +-- spec/controllers/projects/tree_controller_spec.rb | 6 +- .../projects/uploads_controller_spec.rb | 8 +-- .../projects/variables_controller_spec.rb | 8 +-- spec/controllers/projects_controller_spec.rb | 72 +++++++++++----------- .../project_member_activity_index_spec.rb | 2 +- spec/features/projects/members/sorting_spec.rb | 2 +- spec/helpers/application_helper_spec.rb | 4 +- spec/javascripts/fixtures/branches.rb | 2 +- spec/javascripts/fixtures/builds.rb | 2 +- spec/javascripts/fixtures/issues.rb | 2 +- spec/javascripts/fixtures/merge_requests.rb | 2 +- spec/javascripts/fixtures/projects.rb | 2 +- spec/javascripts/fixtures/todos.rb | 4 +- .../constraints/project_url_constrainer_spec.rb | 4 +- spec/lib/gitlab/conflict/file_spec.rb | 2 +- spec/lib/gitlab/regex_spec.rb | 4 +- spec/models/ci/build_spec.rb | 4 +- spec/models/concerns/routable_spec.rb | 4 +- spec/models/namespace_spec.rb | 4 +- .../project_services/drone_ci_service_spec.rb | 2 +- spec/models/project_spec.rb | 16 +++-- spec/requests/api/commits_spec.rb | 2 +- spec/requests/api/groups_spec.rb | 2 +- spec/requests/api/v3/commits_spec.rb | 2 +- .../issuables_list_metadata_shared_examples.rb | 2 +- spec/support/markdown_feature.rb | 5 +- spec/support/test_env.rb | 4 +- spec/workers/repository_fork_worker_spec.rb | 20 +++--- 68 files changed, 344 insertions(+), 349 deletions(-) diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 8df1c8e7f94..51fa5c828b3 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -39,7 +39,7 @@ $value = $block.find('.value'); $loading = $block.find('.block-loading').fadeOut(); if (issueUpdateURL) { - milestoneLinkTemplate = _.template('<%- title %>'); + milestoneLinkTemplate = _.template('<%- title %>'); milestoneLinkNoneTemplate = 'None'; collapsedSidebarLabelTemplate = _.template(' <%- title %> '); } @@ -181,8 +181,7 @@ $selectbox.hide(); $value.css('display', ''); if (data.milestone != null) { - data.milestone.namespace = _this.currentProject.namespace; - data.milestone.path = _this.currentProject.path; + data.milestone.full_path = _this.currentProject.full_path; data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date); $value.html(milestoneLinkTemplate(data.milestone)); return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone)); diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 715072290c6..860a665ae26 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -52,7 +52,7 @@ module IssuablesHelper field_name: 'issuable_template', selected: selected_template(issuable), project_path: ref_project.path, - namespace_path: ref_project.namespace.path + namespace_path: ref_project.namespace.full_path } } diff --git a/app/models/project.rb b/app/models/project.rb index 301ff71cc01..765784de9e6 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -360,7 +360,7 @@ class Project < ActiveRecord::Base end def reference_pattern - name_pattern = Gitlab::Regex::NAMESPACE_REGEX_STR + name_pattern = Gitlab::Regex::FULL_NAMESPACE_REGEX_STR %r{ ((?#{name_pattern})\/)? @@ -848,10 +848,6 @@ class Project < ActiveRecord::Base gitlab_shell.url_to_repo(path_with_namespace) end - def namespace_dir - namespace.try(:path) || '' - end - def repo_exists? @repo_exists ||= repository.exists? rescue @@ -900,8 +896,8 @@ class Project < ActiveRecord::Base def rename_repo path_was = previous_changes['path'].first - old_path_with_namespace = File.join(namespace_dir, path_was) - new_path_with_namespace = File.join(namespace_dir, path) + old_path_with_namespace = File.join(namespace.full_path, path_was) + new_path_with_namespace = File.join(namespace.full_path, path) Rails.logger.error "Attempting to rename #{old_path_with_namespace} -> #{new_path_with_namespace}" diff --git a/app/models/user.rb b/app/models/user.rb index fada0e567f0..f0905cc305b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -334,7 +334,7 @@ class User < ActiveRecord::Base def reference_pattern %r{ #{Regexp.escape(reference_prefix)} - (?#{Gitlab::Regex::NAMESPACE_REF_REGEX_STR}) + (?#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR}) }x end end diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 5a44ec45b7b..30e63d991bb 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -8,7 +8,7 @@ = f.text_field :name, class: "form-control top", required: true, title: "This field is required." .username.form-group = f.label :username - = f.text_field :username, class: "form-control middle", pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE, required: true, title: 'Please create a username with only alphanumeric characters.' + = f.text_field :username, class: "form-control middle", pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS, required: true, title: 'Please create a username with only alphanumeric characters.' %p.validation-error.hide Username is already taken. %p.validation-success.hide Username is available. %p.validation-pending.hide Checking username availability... diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index b9300efd04f..83ae9fd10ec 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -120,7 +120,7 @@ .form-group - if @project.avatar? .avatar-container.s160 - = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160') + = project_icon(@project.full_path, alt: '', class: 'avatar project-avatar s160') %p.light - if @project.avatar_in_git Project avatar in repository: #{ @project.avatar_in_git } diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index efb207b9916..02b7b2447ed 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -17,7 +17,7 @@ %strong= parent.full_path + '/' = f.text_field :path, placeholder: 'open-source', class: 'form-control', autofocus: local_assigns[:autofocus] || false, required: true, - pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE, + pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS, title: 'Please choose a group name with no special characters.' - if parent = f.hidden_field :parent_id, value: parent.id diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 3f7f1a86b9f..6c730e16f67 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -173,7 +173,7 @@ :javascript gl.IssuableResource = new gl.SubbableResource('#{issuable_json_path(issuable)}'); new gl.IssuableTimeTracking("#{escape_javascript(serialize_issuable(issuable))}"); - new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}'); + new MilestoneSelect('{"full_path":"#{@project.full_path}"}'); new LabelsSelect(); new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}'); gl.Subscription.bindAll('.subscription'); diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index f1d1af8eee5..8b327cfc226 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -35,7 +35,7 @@ module Gitlab end def export_filename(project:) - basename = "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_#{project.namespace.full_path}_#{project.path}" + basename = "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_#{project.full_path.tr('/', '_')}" "#{basename[0..FILENAME_LIMIT]}_export.tar.gz" end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index c77fe2d8bdc..5e5f5ff1589 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -5,17 +5,18 @@ module Gitlab # The namespace regex is used in Javascript to validate usernames in the "Register" form. However, Javascript # does not support the negative lookbehind assertion (? Members > Sorting', feature: true do end def visit_members_list(sort:) - visit namespace_project_project_members_path(project.namespace.to_param, project.to_param, sort: sort) + visit namespace_project_project_members_path(project.namespace.to_param, project, sort: sort) end def first_member diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index fd40fe99941..4ffdd530171 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -58,7 +58,7 @@ describe ApplicationHelper do project = create(:empty_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). + expect(helper.project_icon(project.full_path).to_s). to eq "\"Banana" end @@ -68,7 +68,7 @@ describe ApplicationHelper do allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true) avatar_url = "http://#{Gitlab.config.gitlab.host}#{namespace_project_avatar_path(project.namespace, project)}" - expect(helper.project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s).to match( + expect(helper.project_icon(project.full_path).to_s).to match( image_tag(avatar_url)) end end diff --git a/spec/javascripts/fixtures/branches.rb b/spec/javascripts/fixtures/branches.rb index 0e7c2351b66..a059818145b 100644 --- a/spec/javascripts/fixtures/branches.rb +++ b/spec/javascripts/fixtures/branches.rb @@ -20,7 +20,7 @@ describe Projects::BranchesController, '(JavaScript fixtures)', type: :controlle it 'branches/new_branch.html.raw' do |example| get :new, namespace_id: project.namespace.to_param, - project_id: project.to_param + project_id: project expect(response).to be_success store_frontend_fixture(response, example.description) diff --git a/spec/javascripts/fixtures/builds.rb b/spec/javascripts/fixtures/builds.rb index 978e25a1c32..320de791b08 100644 --- a/spec/javascripts/fixtures/builds.rb +++ b/spec/javascripts/fixtures/builds.rb @@ -24,7 +24,7 @@ describe Projects::BuildsController, '(JavaScript fixtures)', type: :controller it 'builds/build-with-artifacts.html.raw' do |example| get :show, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: build_with_artifacts.to_param expect(response).to be_success diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb index 06f708f9e15..88e3f860809 100644 --- a/spec/javascripts/fixtures/issues.rb +++ b/spec/javascripts/fixtures/issues.rb @@ -41,7 +41,7 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller def render_issue(fixture_file_name, issue) get :show, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: issue.to_param expect(response).to be_success diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index 62984097099..ee893b76c84 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -27,7 +27,7 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont def render_merge_request(fixture_file_name, merge_request) get :show, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: merge_request.to_param expect(response).to be_success diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb index 56513219e1e..6c33b240e5c 100644 --- a/spec/javascripts/fixtures/projects.rb +++ b/spec/javascripts/fixtures/projects.rb @@ -20,7 +20,7 @@ describe ProjectsController, '(JavaScript fixtures)', type: :controller do it 'projects/dashboard.html.raw' do |example| get :show, namespace_id: project.namespace.to_param, - id: project.to_param + id: project expect(response).to be_success store_frontend_fixture(response, example.description) diff --git a/spec/javascripts/fixtures/todos.rb b/spec/javascripts/fixtures/todos.rb index 2c08b06ea9e..a81ef8c5492 100644 --- a/spec/javascripts/fixtures/todos.rb +++ b/spec/javascripts/fixtures/todos.rb @@ -39,8 +39,8 @@ describe 'Todos (JavaScript fixtures)' do it 'todos/todos.json' do |example| post :create, - namespace_id: namespace.path, - project_id: project.path, + namespace_id: namespace, + project_id: project, issuable_type: 'issue', issuable_id: issue_2.id, format: 'json' diff --git a/spec/lib/constraints/project_url_constrainer_spec.rb b/spec/lib/constraints/project_url_constrainer_spec.rb index a5251e9a8c2..4f25ad88960 100644 --- a/spec/lib/constraints/project_url_constrainer_spec.rb +++ b/spec/lib/constraints/project_url_constrainer_spec.rb @@ -6,7 +6,7 @@ describe ProjectUrlConstrainer, lib: true do describe '#matches?' do context 'valid request' do - let(:request) { build_request(namespace.path, project.path) } + let(:request) { build_request(namespace.full_path, project.path) } it { expect(subject.matches?(request)).to be_truthy } end @@ -19,7 +19,7 @@ describe ProjectUrlConstrainer, lib: true do end context "project id ending with .git" do - let(:request) { build_request(namespace.path, project.path + '.git') } + let(:request) { build_request(namespace.full_path, project.path + '.git') } it { expect(subject.matches?(request)).to be_falsey } end diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb index 7e5531d92dc..780ac0ad97e 100644 --- a/spec/lib/gitlab/conflict/file_spec.rb +++ b/spec/lib/gitlab/conflict/file_spec.rb @@ -251,7 +251,7 @@ FILE describe '#as_json' do it 'includes the blob path for the file' do expect(conflict_file.as_json[:blob_path]). - to eq("/#{project.namespace.to_param}/#{merge_request.project.to_param}/blob/#{our_commit.oid}/files/ruby/regex.rb") + to eq("/#{project.full_path}/blob/#{our_commit.oid}/files/ruby/regex.rb") end it 'includes the blob icon for the file' do diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index 089ec4e2737..ba45e2d758c 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -51,8 +51,8 @@ describe Gitlab::Regex, lib: true do it { is_expected.not_to match('foo-') } end - describe 'NAMESPACE_REF_REGEX_STR' do - subject { %r{\A#{Gitlab::Regex::NAMESPACE_REF_REGEX_STR}\z} } + describe 'FULL_NAMESPACE_REGEX_STR' do + subject { %r{\A#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR}\z} } it { is_expected.to match('gitlab.org') } it { is_expected.to match('gitlab.org/gitlab-git') } diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 63b6c3c65a6..71cc617e761 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1239,8 +1239,8 @@ describe Ci::Build, :models do { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }, { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true }, { key: 'CI_PROJECT_NAME', value: project.path, public: true }, - { key: 'CI_PROJECT_PATH', value: project.path_with_namespace, public: true }, - { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.path, public: true }, + { key: 'CI_PROJECT_PATH', value: project.full_path, public: true }, + { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true }, { key: 'CI_PROJECT_URL', value: project.web_url, public: true }, { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true } ] diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb index e008ec28fa4..677e60e1282 100644 --- a/spec/models/concerns/routable_spec.rb +++ b/spec/models/concerns/routable_spec.rb @@ -86,7 +86,7 @@ describe Group, 'Routable' do 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}") } + it { expect(nested_group.full_path).to eq("#{group.full_path}/#{nested_group.path}") } end describe '#full_name' do @@ -102,7 +102,7 @@ describe Project, 'Routable' do describe '#full_path' do let(:project) { build_stubbed(:empty_project) } - it { expect(project.full_path).to eq "#{project.namespace.path}/#{project.path}" } + it { expect(project.full_path).to eq "#{project.namespace.full_path}/#{project.path}" } end describe '#full_name' do diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 35d932f1c64..3f9c4289de9 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -36,7 +36,7 @@ describe Namespace, models: true do end describe '#to_param' do - it { expect(namespace.to_param).to eq(namespace.path) } + it { expect(namespace.to_param).to eq(namespace.full_path) } end describe '#human_name' do @@ -151,7 +151,7 @@ describe Namespace, models: true do describe :rm_dir do let!(:project) { create(:empty_project, namespace: namespace) } - let!(:path) { File.join(Gitlab.config.repositories.storages.default, namespace.path) } + let!(:path) { File.join(Gitlab.config.repositories.storages.default, namespace.full_path) } it "removes its dirs when deleted" do namespace.destroy diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb index 47ca802ebc2..044737c6026 100644 --- a/spec/models/project_services/drone_ci_service_spec.rb +++ b/spec/models/project_services/drone_ci_service_spec.rb @@ -28,7 +28,7 @@ describe DroneCiService, models: true, caching: true do shared_context :drone_ci_service do let(:drone) { DroneCiService.new } let(:project) { create(:project, :repository, name: 'project') } - let(:path) { "#{project.namespace.path}/#{project.path}" } + let(:path) { project.full_path } let(:drone_url) { 'http://drone.example.com' } let(:sha) { '2ab7834c' } let(:branch) { 'dev' } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index b0087a9e15d..8a27dfff203 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -402,7 +402,7 @@ describe Project, models: true do let(:project) { create(:empty_project, path: "somewhere") } it 'returns the full web URL for this repo' do - expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.path}/somewhere") + expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.full_path}/somewhere") end end @@ -803,7 +803,7 @@ describe Project, models: true do end let(:avatar_path) do - "/#{project.namespace.name}/#{project.path}/avatar" + "/#{project.full_path}/avatar" end it { should eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" } @@ -1148,16 +1148,14 @@ describe Project, models: true do end it 'renames a repository' do - ns = project.namespace_dir - expect(gitlab_shell).to receive(:mv_repository). ordered. - with(project.repository_storage_path, "#{ns}/foo", "#{ns}/#{project.path}"). + with(project.repository_storage_path, "#{project.namespace.full_path}/foo", "#{project.full_path}"). and_return(true) expect(gitlab_shell).to receive(:mv_repository). ordered. - with(project.repository_storage_path, "#{ns}/foo.wiki", "#{ns}/#{project.path}.wiki"). + with(project.repository_storage_path, "#{project.namespace.full_path}/foo.wiki", "#{project.full_path}.wiki"). and_return(true) expect_any_instance_of(SystemHooksService). @@ -1166,7 +1164,7 @@ describe Project, models: true do expect_any_instance_of(Gitlab::UploadsTransfer). to receive(:rename_project). - with('foo', project.path, ns) + with('foo', project.path, project.namespace.full_path) expect(project).to receive(:expire_caches_before_rename) @@ -1538,7 +1536,7 @@ describe Project, models: true do it 'schedules a RepositoryForkWorker job' do expect(RepositoryForkWorker).to receive(:perform_async). with(project.id, forked_from_project.repository_storage_path, - forked_from_project.path_with_namespace, project.namespace.path) + forked_from_project.path_with_namespace, project.namespace.full_path) project.add_import_job end @@ -1752,7 +1750,7 @@ describe Project, models: true do describe 'inside_path' do let!(:project1) { create(:empty_project) } let!(:project2) { create(:empty_project) } - let!(:path) { project1.namespace.path } + let!(:path) { project1.namespace.full_path } it { expect(Project.inside_path(path)).to eq([project1]) } end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 8b3dfedc5a9..5190fcca2d1 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -148,7 +148,7 @@ describe API::Commits, api: true do end context 'with project path in URL' do - let(:url) { "/projects/#{project.namespace.path}%2F#{project.path}/repository/commits" } + let(:url) { "/projects/#{project.full_path.gsub('/', '%2F')}/repository/commits" } it 'a new file in project repo' do post api(url, user), valid_c_params diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 9c3a92bedbd..cd6bc928f43 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -519,7 +519,7 @@ describe API::Groups, api: true do describe "POST /groups/:id/projects/:project_id" do let(:project) { create(:empty_project) } - let(:project_path) { "#{project.namespace.path}%2F#{project.path}" } + let(:project_path) { project.full_path.gsub('/', '%2F') } before(:each) do allow_any_instance_of(Projects::TransferService). diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb index 2d7584c3e59..e298ef055e1 100644 --- a/spec/requests/api/v3/commits_spec.rb +++ b/spec/requests/api/v3/commits_spec.rb @@ -148,7 +148,7 @@ describe API::V3::Commits, api: true do end context 'with project path in URL' do - let(:url) { "/projects/#{project.namespace.path}%2F#{project.path}/repository/commits" } + let(:url) { "/projects/#{project.full_path.gsub('/', '%2F')}/repository/commits" } it 'a new file in project repo' do post v3_api(url, user), valid_c_params diff --git a/spec/support/issuables_list_metadata_shared_examples.rb b/spec/support/issuables_list_metadata_shared_examples.rb index 4644c7a6b86..4c0f556e736 100644 --- a/spec/support/issuables_list_metadata_shared_examples.rb +++ b/spec/support/issuables_list_metadata_shared_examples.rb @@ -22,7 +22,7 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil| if action get action else - get :index, namespace_id: project.namespace.path, project_id: project.path + get :index, namespace_id: project.namespace, project_id: project end meta_data = assigns(:issuable_meta_data) diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb index a79386b5db9..5980d14c8ae 100644 --- a/spec/support/markdown_feature.rb +++ b/spec/support/markdown_feature.rb @@ -79,8 +79,9 @@ class MarkdownFeature def xproject @xproject ||= begin - namespace = create(:namespace, name: 'cross-reference') - create(:project, namespace: namespace) do |project| + group = create(:group, name: 'cross-reference') + group2 = create(:group, parent: group, name: 'nested-group') + create(:project, namespace: group2) do |project| project.team << [user, :developer] end end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 4e63a4cd537..c3aa3ef44c2 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -135,7 +135,7 @@ module TestEnv def copy_repo(project) base_repo_path = File.expand_path(factory_repo_path_bare) - target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.namespace.path}/#{project.path}.git") + target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.full_path}.git") FileUtils.mkdir_p(target_repo_path) FileUtils.cp_r("#{base_repo_path}/.", target_repo_path) FileUtils.chmod_R 0755, target_repo_path @@ -152,7 +152,7 @@ module TestEnv def copy_forked_repo_with_submodules(project) base_repo_path = File.expand_path(forked_repo_path_bare) - target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.namespace.path}/#{project.path}.git") + target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.full_path}.git") FileUtils.mkdir_p(target_repo_path) FileUtils.cp_r("#{base_repo_path}/.", target_repo_path) FileUtils.chmod_R 0755, target_repo_path diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb index 60605460adb..87521ae408e 100644 --- a/spec/workers/repository_fork_worker_spec.rb +++ b/spec/workers/repository_fork_worker_spec.rb @@ -15,24 +15,24 @@ describe RepositoryForkWorker do it "creates a new repository from a fork" do expect(shell).to receive(:fork_repository).with( '/test/path', - project.path_with_namespace, + project.full_path, project.repository_storage_path, - fork_project.namespace.path + fork_project.namespace.full_path ).and_return(true) subject.perform( project.id, '/test/path', - project.path_with_namespace, - fork_project.namespace.path) + project.full_path, + fork_project.namespace.full_path) end it 'flushes various caches' do expect(shell).to receive(:fork_repository).with( '/test/path', - project.path_with_namespace, + project.full_path, project.repository_storage_path, - fork_project.namespace.path + fork_project.namespace.full_path ).and_return(true) expect_any_instance_of(Repository).to receive(:expire_emptiness_caches). @@ -41,8 +41,8 @@ describe RepositoryForkWorker do expect_any_instance_of(Repository).to receive(:expire_exists_cache). and_call_original - subject.perform(project.id, '/test/path', project.path_with_namespace, - fork_project.namespace.path) + subject.perform(project.id, '/test/path', project.full_path, + fork_project.namespace.full_path) end it "handles bad fork" do @@ -53,8 +53,8 @@ describe RepositoryForkWorker do subject.perform( project.id, '/test/path', - project.path_with_namespace, - fork_project.namespace.path) + project.full_path, + fork_project.namespace.full_path) end end end -- cgit v1.2.3 From 08f08a34cfa4d0afe78f8c8afda16693bb96da45 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 23 Feb 2017 17:57:53 -0600 Subject: remove require.context from users_bundle --- app/assets/javascripts/users/users_bundle.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js index 4cad60a59b1..580e2d84be5 100644 --- a/app/assets/javascripts/users/users_bundle.js +++ b/app/assets/javascripts/users/users_bundle.js @@ -1,3 +1 @@ -// require everything else in this directory -function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('.', false, /^\.\/(?!users_bundle).*\.(js|es6)$/)); +require('./calendar'); -- cgit v1.2.3 From 1097724fbb58d3c9cabd1babd318d4670f54d555 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 23 Feb 2017 19:22:26 -0500 Subject: Fix failing specs --- spec/javascripts/user_callout_spec.js | 33 --------------------------- spec/javascripts/user_callout_spec.js.es6 | 37 +++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 33 deletions(-) delete mode 100644 spec/javascripts/user_callout_spec.js create mode 100644 spec/javascripts/user_callout_spec.js.es6 diff --git a/spec/javascripts/user_callout_spec.js b/spec/javascripts/user_callout_spec.js deleted file mode 100644 index e174fdf35de..00000000000 --- a/spec/javascripts/user_callout_spec.js +++ /dev/null @@ -1,33 +0,0 @@ -const UserCallout = require('~/user_callout'); - -const USER_CALLOUT_COOKIE = 'user_callout_dismissed'; -const Cookie = window.Cookies; - -describe('UserCallout', function () { - const fixtureName = 'static/user_callout.html.raw'; - preloadFixtures(fixtureName); - - beforeEach(() => { - loadFixtures(fixtureName); - this.userCallout = new UserCallout(); - this.closeButton = $('.close-user-callout'); - this.userCalloutBtn = $('.user-callout-btn'); - this.userCalloutContainer = $('.user-callout'); - Cookie.set(USER_CALLOUT_COOKIE, 'false'); - }); - - fit('shows when cookie is set to false', () => { - expect(Cookie.get(USER_CALLOUT_COOKIE)).toBeDefined(); - expect(this.userCalloutContainer.is(':visible')).toBe(true); - }); - - fit('hides when user clicks on the dismiss-icon', () => { - this.closeButton.click(); - expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true'); - }); - - fit('hides when user clicks on the "check it out" button', () => { - this.userCalloutBtn.click(); - expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true'); - }); -}); diff --git a/spec/javascripts/user_callout_spec.js.es6 b/spec/javascripts/user_callout_spec.js.es6 new file mode 100644 index 00000000000..6ee63f56a26 --- /dev/null +++ b/spec/javascripts/user_callout_spec.js.es6 @@ -0,0 +1,37 @@ +const UserCallout = require('~/user_callout'); + +const USER_CALLOUT_COOKIE = 'user_callout_dismissed'; +const Cookie = window.Cookies; + +describe('UserCallout', () => { + const fixtureName = 'static/user_callout.html.raw'; + preloadFixtures(fixtureName); + + beforeEach(function () { + loadFixtures(fixtureName); + this.userCallout = new UserCallout(); + this.closeButton = $('.close-user-callout'); + this.userCalloutBtn = $('.user-callout-btn'); + this.userCalloutContainer = $('.user-callout'); + Cookie.set(USER_CALLOUT_COOKIE, 'false'); + }); + + afterEach(function () { + Cookie.set(USER_CALLOUT_COOKIE, 'false'); + }); + + it('shows when cookie is set to false', function () { + expect(Cookie.get(USER_CALLOUT_COOKIE)).toBeDefined(); + expect(this.userCalloutContainer.is(':visible')).toBe(true); + }); + + it('hides when user clicks on the dismiss-icon', function () { + this.closeButton.click(); + expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true'); + }); + + it('hides when user clicks on the "check it out" button', function () { + this.userCalloutBtn.click(); + expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true'); + }); +}); -- cgit v1.2.3 From b9305852012112bcd044e23091ebd2febef998c8 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 23 Feb 2017 23:05:07 -0600 Subject: add missing require statement and don't require self --- app/assets/javascripts/application.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 4a9db2763ef..ed3778eb434 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -105,8 +105,8 @@ require('./droplab/droplab_filter'); require('./abuse_reports'); require('./activities'); require('./admin'); +require('./ajax_loading_spinner'); require('./api'); -require('./application'); require('./aside'); require('./autosave'); require('./awards_handler'); -- cgit v1.2.3 From 90cd66a51d7f40e980f4934e2e1a717983b5e4fe Mon Sep 17 00:00:00 2001 From: Pratik Borsadiya Date: Mon, 20 Feb 2017 20:06:40 +0530 Subject: Fix #27840 - Improve the search bar experience on mobile --- .../filtered_search_dropdown.js.es6 | 6 ++- app/assets/stylesheets/framework/filters.scss | 46 +++++++++++++++++++++- app/views/shared/issuable/_search_bar.html.haml | 2 +- .../27840-improve-search-bar-experience.yml | 4 ++ 4 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/27840-improve-search-bar-experience.yml diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index fbc72a3001a..dd565da507e 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -48,7 +48,11 @@ } setOffset(offset = 0) { - this.dropdown.style.left = `${offset}px`; + if (window.innerWidth > 480) { + this.dropdown.style.left = `${offset}px`; + } else { + this.dropdown.style.left = '0px'; + } } renderContent(forceShowList = false) { diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index e3da467a27c..d2be8dc7a39 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -26,6 +26,11 @@ .filtered-search-container { display: -webkit-flex; display: flex; + + @media (max-width: $screen-xs-min) { + -webkit-flex-direction: column; + flex-direction: column; + } } .filtered-search-input-container { @@ -34,6 +39,20 @@ position: relative; width: 100%; + @media (max-width: $screen-xs-min) { + -webkit-flex: 1 1 100%; + flex: 1 1 100%; + margin-bottom: 10px; + + .dropdown-menu { + width: auto; + left: 0; + right: 0; + max-width: none; + min-width: 100%; + } + } + .form-control { padding-left: 25px; padding-right: 25px; @@ -79,6 +98,31 @@ overflow: auto; } +@media (max-width: $screen-xs-min) { + .issues-details-filters { + padding: 0 0 10px; + background-color: $white-light; + border-top: 0; + } + + .filter-dropdown-container { + .dropdown-toggle, + .dropdown { + width: 100%; + } + + .dropdown { + margin-left: 0; + } + + .fa-chevron-down { + position: absolute; + right: 10px; + top: 10px; + } + } +} + %filter-dropdown-item-btn-hover { background-color: $dropdown-hover-color; color: $white-light; @@ -148,4 +192,4 @@ .filter-dropdown-loading { padding: 8px 16px; -} +} \ No newline at end of file diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 8e04b50bb8a..62f09cc2dc1 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -82,7 +82,7 @@ %span.dropdown-label-box{ style: 'background: {{color}}' } %span.label-title.js-data-value {{title}} - .pull-right + .pull-right.filter-dropdown-container = render 'shared/sort_dropdown' - if @bulk_edit diff --git a/changelogs/unreleased/27840-improve-search-bar-experience.yml b/changelogs/unreleased/27840-improve-search-bar-experience.yml new file mode 100644 index 00000000000..87b1f0c5572 --- /dev/null +++ b/changelogs/unreleased/27840-improve-search-bar-experience.yml @@ -0,0 +1,4 @@ +--- +title: Enhanced filter issues layout for better mobile experiance +merge_request: 9280 +author: Pratik Borsadiya -- cgit v1.2.3 From c2497d8b35d77d8a14cf085534e028970468db72 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 23 Feb 2017 23:31:14 -0600 Subject: remove unused StatGraph class --- app/assets/javascripts/graphs/graphs_bundle.js | 1 - app/assets/javascripts/graphs/stat_graph.js | 18 ------------------ spec/javascripts/graphs/stat_graph_spec.js | 20 -------------------- 3 files changed, 39 deletions(-) delete mode 100644 app/assets/javascripts/graphs/stat_graph.js delete mode 100644 spec/javascripts/graphs/stat_graph_spec.js diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js index 086dcb34571..230e0706500 100644 --- a/app/assets/javascripts/graphs/graphs_bundle.js +++ b/app/assets/javascripts/graphs/graphs_bundle.js @@ -1,4 +1,3 @@ require('./stat_graph_contributors_graph'); require('./stat_graph_contributors_util'); require('./stat_graph_contributors'); -require('./stat_graph'); diff --git a/app/assets/javascripts/graphs/stat_graph.js b/app/assets/javascripts/graphs/stat_graph.js deleted file mode 100644 index 75a53aae33c..00000000000 --- a/app/assets/javascripts/graphs/stat_graph.js +++ /dev/null @@ -1,18 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-return-assign, max-len */ -(function() { - this.StatGraph = (function() { - function StatGraph() {} - - StatGraph.log = {}; - - StatGraph.get_log = function() { - return this.log; - }; - - StatGraph.set_log = function(data) { - return this.log = data; - }; - - return StatGraph; - })(); -}).call(window); diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js deleted file mode 100644 index 876c23361bc..00000000000 --- a/spec/javascripts/graphs/stat_graph_spec.js +++ /dev/null @@ -1,20 +0,0 @@ -/* eslint-disable quotes */ -/* global StatGraph */ - -require('~/graphs/stat_graph'); - -describe("StatGraph", function () { - describe("#get_log", function () { - it("returns log", function () { - StatGraph.log = "test"; - expect(StatGraph.get_log()).toBe("test"); - }); - }); - - describe("#set_log", function () { - it("sets the log", function () { - StatGraph.set_log("test"); - expect(StatGraph.log).toBe("test"); - }); - }); -}); -- cgit v1.2.3 From 2f09c23c1afd55e17ef6acb6c0ec23323e05b4f6 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 23 Feb 2017 23:28:54 -0600 Subject: refactor stat_graph_contributors_util to es6 module syntax --- app/assets/javascripts/graphs/graphs_bundle.js | 1 - .../javascripts/graphs/stat_graph_contributors.js | 3 +- .../graphs/stat_graph_contributors_util.js | 265 ++++++++++----------- .../graphs/stat_graph_contributors_util_spec.js | 3 +- 4 files changed, 135 insertions(+), 137 deletions(-) diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js index 230e0706500..a455cebc0f6 100644 --- a/app/assets/javascripts/graphs/graphs_bundle.js +++ b/app/assets/javascripts/graphs/graphs_bundle.js @@ -1,3 +1,2 @@ require('./stat_graph_contributors_graph'); -require('./stat_graph_contributors_util'); require('./stat_graph_contributors'); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js index bbfb467ad50..3023e97fd74 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js @@ -2,7 +2,8 @@ /* global ContributorsGraph */ /* global ContributorsAuthorGraph */ /* global ContributorsMasterGraph */ -/* global ContributorsStatGraphUtil */ +import ContributorsStatGraphUtil from './stat_graph_contributors_util'; + /* global d3 */ window.d3 = require('d3'); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js index 7954c583598..c583757f3f2 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js @@ -1,138 +1,137 @@ /* eslint-disable func-names, space-before-function-paren, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, max-len */ -(function() { - window.ContributorsStatGraphUtil = { - parse_log: function(log) { - var by_author, by_email, data, entry, i, len, total, normalized_email; - total = {}; - by_author = {}; - by_email = {}; - for (i = 0, len = log.length; i < len; i += 1) { - entry = log[i]; - if (total[entry.date] == null) { - this.add_date(entry.date, total); - } - 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); - } - if (!data[entry.date]) { - this.add_date(entry.date, data); - } - this.store_data(entry, total[entry.date], data[entry.date]); - } - total = _.toArray(total); - by_author = _.toArray(by_author); - return { - total: total, - by_author: by_author - }; - }, - add_date: function(date, collection) { - collection[date] = {}; - return collection[date].date = date; - }, - add_author: function(author, by_author, by_email) { - 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; - by_email[normalized_email] = data; - return data; - }, - store_data: function(entry, total, by_author) { - this.store_commits(total, by_author); - this.store_additions(entry, total, by_author); - return this.store_deletions(entry, total, by_author); - }, - store_commits: function(total, by_author) { - this.add(total, "commits", 1); - return this.add(by_author, "commits", 1); - }, - add: function(collection, field, value) { - if (collection[field] == null) { - collection[field] = 0; - } - return collection[field] += value; - }, - store_additions: function(entry, total, by_author) { - if (entry.additions == null) { - entry.additions = 0; + +export default { + parse_log: function(log) { + var by_author, by_email, data, entry, i, len, total, normalized_email; + total = {}; + by_author = {}; + by_email = {}; + for (i = 0, len = log.length; i < len; i += 1) { + entry = log[i]; + if (total[entry.date] == null) { + this.add_date(entry.date, total); } - this.add(total, "additions", entry.additions); - return this.add(by_author, "additions", entry.additions); - }, - store_deletions: function(entry, total, by_author) { - if (entry.deletions == null) { - entry.deletions = 0; + 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); } - this.add(total, "deletions", entry.deletions); - return this.add(by_author, "deletions", entry.deletions); - }, - get_total_data: function(parsed_log, field) { - var log, total_data; - log = parsed_log.total; - total_data = this.pick_field(log, field); - return _.sortBy(total_data, function(d) { - return d.date; - }); - }, - pick_field: function(log, field) { - var total_data; - total_data = []; - _.each(log, function(d) { - return total_data.push(_.pick(d, [field, 'date'])); - }); - return total_data; - }, - get_author_data: function(parsed_log, field, date_range) { - var author_data, log; - if (date_range == null) { - date_range = null; - } - log = parsed_log.by_author; - author_data = []; - _.each(log, (function(_this) { - return function(log_entry) { - var parsed_log_entry; - parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range); - if (!_.isEmpty(parsed_log_entry.dates)) { - return author_data.push(parsed_log_entry); - } - }; - })(this)); - return _.sortBy(author_data, function(d) { - return d[field]; - }).reverse(); - }, - parse_log_entry: function(log_entry, field, date_range) { - var parsed_entry; - parsed_entry = {}; - parsed_entry.author_name = log_entry.author_name; - parsed_entry.author_email = log_entry.author_email; - parsed_entry.dates = {}; - parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0; - _.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) { - return function(value, key) { - if (_this.in_range(value.date, date_range)) { - parsed_entry.dates[value.date] = value[field]; - parsed_entry.commits += value.commits; - parsed_entry.additions += value.additions; - return parsed_entry.deletions += value.deletions; - } - }; - })(this)); - return parsed_entry; - }, - in_range: function(date, date_range) { - var ref; - if (date_range === null || (date_range[0] <= (ref = new Date(date)) && ref <= date_range[1])) { - return true; - } else { - return false; + if (!data[entry.date]) { + this.add_date(entry.date, data); } + this.store_data(entry, total[entry.date], data[entry.date]); + } + total = _.toArray(total); + by_author = _.toArray(by_author); + return { + total: total, + by_author: by_author + }; + }, + add_date: function(date, collection) { + collection[date] = {}; + return collection[date].date = date; + }, + add_author: function(author, by_author, by_email) { + 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; + by_email[normalized_email] = data; + return data; + }, + store_data: function(entry, total, by_author) { + this.store_commits(total, by_author); + this.store_additions(entry, total, by_author); + return this.store_deletions(entry, total, by_author); + }, + store_commits: function(total, by_author) { + this.add(total, "commits", 1); + return this.add(by_author, "commits", 1); + }, + add: function(collection, field, value) { + if (collection[field] == null) { + collection[field] = 0; + } + return collection[field] += value; + }, + store_additions: function(entry, total, by_author) { + if (entry.additions == null) { + entry.additions = 0; + } + this.add(total, "additions", entry.additions); + return this.add(by_author, "additions", entry.additions); + }, + store_deletions: function(entry, total, by_author) { + if (entry.deletions == null) { + entry.deletions = 0; + } + this.add(total, "deletions", entry.deletions); + return this.add(by_author, "deletions", entry.deletions); + }, + get_total_data: function(parsed_log, field) { + var log, total_data; + log = parsed_log.total; + total_data = this.pick_field(log, field); + return _.sortBy(total_data, function(d) { + return d.date; + }); + }, + pick_field: function(log, field) { + var total_data; + total_data = []; + _.each(log, function(d) { + return total_data.push(_.pick(d, [field, 'date'])); + }); + return total_data; + }, + get_author_data: function(parsed_log, field, date_range) { + var author_data, log; + if (date_range == null) { + date_range = null; + } + log = parsed_log.by_author; + author_data = []; + _.each(log, (function(_this) { + return function(log_entry) { + var parsed_log_entry; + parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range); + if (!_.isEmpty(parsed_log_entry.dates)) { + return author_data.push(parsed_log_entry); + } + }; + })(this)); + return _.sortBy(author_data, function(d) { + return d[field]; + }).reverse(); + }, + parse_log_entry: function(log_entry, field, date_range) { + var parsed_entry; + parsed_entry = {}; + parsed_entry.author_name = log_entry.author_name; + parsed_entry.author_email = log_entry.author_email; + parsed_entry.dates = {}; + parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0; + _.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) { + return function(value, key) { + if (_this.in_range(value.date, date_range)) { + parsed_entry.dates[value.date] = value[field]; + parsed_entry.commits += value.commits; + parsed_entry.additions += value.additions; + return parsed_entry.deletions += value.deletions; + } + }; + })(this)); + return parsed_entry; + }, + in_range: function(date, date_range) { + var ref; + if (date_range === null || (date_range[0] <= (ref = new Date(date)) && ref <= date_range[1])) { + return true; + } else { + return false; } - }; -}).call(window); + } +}; diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js index b15764abe8c..9b47ab62181 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js @@ -1,7 +1,6 @@ /* eslint-disable quotes, no-var, camelcase, object-property-newline, comma-dangle, max-len, vars-on-top, quote-props */ -/* global ContributorsStatGraphUtil */ -require('~/graphs/stat_graph_contributors_util'); +import ContributorsStatGraphUtil from '~/graphs/stat_graph_contributors_util'; describe("ContributorsStatGraphUtil", function () { describe("#parse_log", function () { -- cgit v1.2.3 From 2d2e0b7de8350721e53dc62dc239d90f4f147a14 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 23 Feb 2017 23:48:25 -0600 Subject: refactor stat_graph_contributors_graph to es6 module syntax --- app/assets/javascripts/graphs/graphs_bundle.js | 1 - .../javascripts/graphs/stat_graph_contributors.js | 5 +- .../graphs/stat_graph_contributors_graph.js | 542 ++++++++++----------- .../graphs/stat_graph_contributors_graph_spec.js | 8 +- 4 files changed, 274 insertions(+), 282 deletions(-) diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js index a455cebc0f6..914716a5147 100644 --- a/app/assets/javascripts/graphs/graphs_bundle.js +++ b/app/assets/javascripts/graphs/graphs_bundle.js @@ -1,2 +1 @@ -require('./stat_graph_contributors_graph'); require('./stat_graph_contributors'); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js index 3023e97fd74..a11ee5967e7 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js @@ -1,7 +1,6 @@ /* 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 */ -/* global ContributorsGraph */ -/* global ContributorsAuthorGraph */ -/* global ContributorsMasterGraph */ + +import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph'; import ContributorsStatGraphUtil from './stat_graph_contributors_util'; /* global d3 */ diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js index 228771da4ee..521bc77db66 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js @@ -1,276 +1,272 @@ -/* eslint-disable func-names, space-before-function-paren, one-var, no-var, 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, newline-per-chained-call, no-else-return */ -/* global d3 */ -/* global ContributorsGraph */ - -window.d3 = require('d3'); - -(function() { - var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }, - extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, - hasProp = {}.hasOwnProperty; - - this.ContributorsGraph = (function() { - function ContributorsGraph() {} - - ContributorsGraph.prototype.MARGIN = { - top: 20, - right: 20, - bottom: 30, - left: 50 - }; - - ContributorsGraph.prototype.x_domain = null; - - ContributorsGraph.prototype.y_domain = null; - - ContributorsGraph.prototype.dates = []; - - ContributorsGraph.set_x_domain = function(data) { - return ContributorsGraph.prototype.x_domain = data; - }; - - ContributorsGraph.set_y_domain = function(data) { - return ContributorsGraph.prototype.y_domain = [ - 0, d3.max(data, function(d) { - return d.commits = d.commits || d.additions || d.deletions; - }) - ]; - }; - - ContributorsGraph.init_x_domain = function(data) { - return ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) { - return d.date; - }); - }; - - ContributorsGraph.init_y_domain = function(data) { - return ContributorsGraph.prototype.y_domain = [ - 0, d3.max(data, function(d) { - return d.commits = d.commits || d.additions || d.deletions; - }) - ]; - }; - - ContributorsGraph.init_domain = function(data) { - ContributorsGraph.init_x_domain(data); - return ContributorsGraph.init_y_domain(data); - }; - - ContributorsGraph.set_dates = function(data) { - return ContributorsGraph.prototype.dates = data; - }; - - ContributorsGraph.prototype.set_x_domain = function() { - return this.x.domain(this.x_domain); - }; - - ContributorsGraph.prototype.set_y_domain = function() { - return this.y.domain(this.y_domain); - }; - - ContributorsGraph.prototype.set_domain = function() { - this.set_x_domain(); - return this.set_y_domain(); - }; - - ContributorsGraph.prototype.create_scale = function(width, height) { - this.x = d3.time.scale().range([0, width]).clamp(true); - return this.y = d3.scale.linear().range([height, 0]).nice(); - }; - - ContributorsGraph.prototype.draw_x_axis = function() { - return this.svg.append("g").attr("class", "x axis").attr("transform", "translate(0, " + this.height + ")").call(this.x_axis); - }; - - ContributorsGraph.prototype.draw_y_axis = function() { - return this.svg.append("g").attr("class", "y axis").call(this.y_axis); - }; - - ContributorsGraph.prototype.set_data = function(data) { - return this.data = data; - }; - - return ContributorsGraph; - })(); - - this.ContributorsMasterGraph = (function(superClass) { - extend(ContributorsMasterGraph, superClass); - - function ContributorsMasterGraph(data1) { - this.data = data1; - this.update_content = bind(this.update_content, this); - this.width = $('.content').width() - 70; - this.height = 200; - this.x = null; - this.y = null; - this.x_axis = null; - this.y_axis = null; - this.area = null; - this.svg = null; - this.brush = null; - this.x_max_domain = null; +/* eslint-disable func-names, space-before-function-paren, no-var, 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, newline-per-chained-call, no-else-return, no-shadow */ + +import d3 from 'd3'; + +const bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; +const extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; +const hasProp = {}.hasOwnProperty; + +export const ContributorsGraph = (function() { + function ContributorsGraph() {} + + ContributorsGraph.prototype.MARGIN = { + top: 20, + right: 20, + bottom: 30, + left: 50 + }; + + ContributorsGraph.prototype.x_domain = null; + + ContributorsGraph.prototype.y_domain = null; + + ContributorsGraph.prototype.dates = []; + + ContributorsGraph.set_x_domain = function(data) { + return ContributorsGraph.prototype.x_domain = data; + }; + + ContributorsGraph.set_y_domain = function(data) { + return ContributorsGraph.prototype.y_domain = [ + 0, d3.max(data, function(d) { + return d.commits = d.commits || d.additions || d.deletions; + }) + ]; + }; + + ContributorsGraph.init_x_domain = function(data) { + return ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) { + return d.date; + }); + }; + + ContributorsGraph.init_y_domain = function(data) { + return ContributorsGraph.prototype.y_domain = [ + 0, d3.max(data, function(d) { + return d.commits = d.commits || d.additions || d.deletions; + }) + ]; + }; + + ContributorsGraph.init_domain = function(data) { + ContributorsGraph.init_x_domain(data); + return ContributorsGraph.init_y_domain(data); + }; + + ContributorsGraph.set_dates = function(data) { + return ContributorsGraph.prototype.dates = data; + }; + + ContributorsGraph.prototype.set_x_domain = function() { + return this.x.domain(this.x_domain); + }; + + ContributorsGraph.prototype.set_y_domain = function() { + return this.y.domain(this.y_domain); + }; + + ContributorsGraph.prototype.set_domain = function() { + this.set_x_domain(); + return this.set_y_domain(); + }; + + ContributorsGraph.prototype.create_scale = function(width, height) { + this.x = d3.time.scale().range([0, width]).clamp(true); + return this.y = d3.scale.linear().range([height, 0]).nice(); + }; + + ContributorsGraph.prototype.draw_x_axis = function() { + return this.svg.append("g").attr("class", "x axis").attr("transform", "translate(0, " + this.height + ")").call(this.x_axis); + }; + + ContributorsGraph.prototype.draw_y_axis = function() { + return this.svg.append("g").attr("class", "y axis").call(this.y_axis); + }; + + ContributorsGraph.prototype.set_data = function(data) { + return this.data = data; + }; + + return ContributorsGraph; +})(); + +export const ContributorsMasterGraph = (function(superClass) { + extend(ContributorsMasterGraph, superClass); + + function ContributorsMasterGraph(data1) { + this.data = data1; + this.update_content = bind(this.update_content, this); + this.width = $('.content').width() - 70; + this.height = 200; + this.x = null; + this.y = null; + this.x_axis = null; + this.y_axis = null; + this.area = null; + this.svg = null; + this.brush = null; + this.x_max_domain = null; + } + + ContributorsMasterGraph.prototype.process_dates = function(data) { + var dates; + dates = this.get_dates(data); + this.parse_dates(data); + return ContributorsGraph.set_dates(dates); + }; + + ContributorsMasterGraph.prototype.get_dates = function(data) { + return _.pluck(data, 'date'); + }; + + ContributorsMasterGraph.prototype.parse_dates = function(data) { + var parseDate; + parseDate = d3.time.format("%Y-%m-%d").parse; + return data.forEach(function(d) { + return d.date = parseDate(d.date); + }); + }; + + ContributorsMasterGraph.prototype.create_scale = function() { + return ContributorsMasterGraph.__super__.create_scale.call(this, this.width, this.height); + }; + + ContributorsMasterGraph.prototype.create_axes = function() { + this.x_axis = d3.svg.axis().scale(this.x).orient("bottom"); + return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5); + }; + + ContributorsMasterGraph.prototype.create_svg = function() { + return this.svg = d3.select("#contributors-master").append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "tint-box").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + }; + + ContributorsMasterGraph.prototype.create_area = function(x, y) { + return this.area = d3.svg.area().x(function(d) { + return x(d.date); + }).y0(this.height).y1(function(d) { + d.commits = d.commits || d.additions || d.deletions; + return y(d.commits); + }).interpolate("basis"); + }; + + ContributorsMasterGraph.prototype.create_brush = function() { + return this.brush = d3.svg.brush().x(this.x).on("brushend", this.update_content); + }; + + ContributorsMasterGraph.prototype.draw_path = function(data) { + return this.svg.append("path").datum(data).attr("class", "area").attr("d", this.area); + }; + + ContributorsMasterGraph.prototype.add_brush = function() { + return this.svg.append("g").attr("class", "selection").call(this.brush).selectAll("rect").attr("height", this.height); + }; + + ContributorsMasterGraph.prototype.update_content = function() { + ContributorsGraph.set_x_domain(this.brush.empty() ? this.x_max_domain : this.brush.extent()); + return $("#brush_change").trigger('change'); + }; + + ContributorsMasterGraph.prototype.draw = function() { + this.process_dates(this.data); + this.create_scale(); + this.create_axes(); + ContributorsGraph.init_domain(this.data); + this.x_max_domain = this.x_domain; + this.set_domain(); + this.create_area(this.x, this.y); + this.create_svg(); + this.create_brush(); + this.draw_path(this.data); + this.draw_x_axis(); + this.draw_y_axis(); + return this.add_brush(); + }; + + ContributorsMasterGraph.prototype.redraw = function() { + this.process_dates(this.data); + ContributorsGraph.set_y_domain(this.data); + this.set_y_domain(); + this.svg.select("path").datum(this.data); + this.svg.select("path").attr("d", this.area); + return this.svg.select(".y.axis").call(this.y_axis); + }; + + return ContributorsMasterGraph; +})(ContributorsGraph); + +export const ContributorsAuthorGraph = (function(superClass) { + extend(ContributorsAuthorGraph, superClass); + + function ContributorsAuthorGraph(data1) { + this.data = data1; + // Don't split graph size in half for mobile devices. + if ($(window).width() < 768) { + this.width = $('.content').width() - 80; + } else { + this.width = ($('.content').width() / 2) - 100; } - - ContributorsMasterGraph.prototype.process_dates = function(data) { - var dates; - dates = this.get_dates(data); - this.parse_dates(data); - return ContributorsGraph.set_dates(dates); - }; - - ContributorsMasterGraph.prototype.get_dates = function(data) { - return _.pluck(data, 'date'); - }; - - ContributorsMasterGraph.prototype.parse_dates = function(data) { + this.height = 200; + this.x = null; + this.y = null; + this.x_axis = null; + this.y_axis = null; + this.area = null; + this.svg = null; + this.list_item = null; + } + + ContributorsAuthorGraph.prototype.create_scale = function() { + return ContributorsAuthorGraph.__super__.create_scale.call(this, this.width, this.height); + }; + + ContributorsAuthorGraph.prototype.create_axes = function() { + this.x_axis = d3.svg.axis().scale(this.x).orient("bottom").ticks(8); + return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5); + }; + + ContributorsAuthorGraph.prototype.create_area = function(x, y) { + return this.area = d3.svg.area().x(function(d) { var parseDate; parseDate = d3.time.format("%Y-%m-%d").parse; - return data.forEach(function(d) { - return d.date = parseDate(d.date); - }); - }; - - ContributorsMasterGraph.prototype.create_scale = function() { - return ContributorsMasterGraph.__super__.create_scale.call(this, this.width, this.height); - }; - - ContributorsMasterGraph.prototype.create_axes = function() { - this.x_axis = d3.svg.axis().scale(this.x).orient("bottom"); - return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5); - }; - - ContributorsMasterGraph.prototype.create_svg = function() { - return this.svg = d3.select("#contributors-master").append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "tint-box").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); - }; - - ContributorsMasterGraph.prototype.create_area = function(x, y) { - return this.area = d3.svg.area().x(function(d) { - return x(d.date); - }).y0(this.height).y1(function(d) { - d.commits = d.commits || d.additions || d.deletions; - return y(d.commits); - }).interpolate("basis"); - }; - - ContributorsMasterGraph.prototype.create_brush = function() { - return this.brush = d3.svg.brush().x(this.x).on("brushend", this.update_content); - }; - - ContributorsMasterGraph.prototype.draw_path = function(data) { - return this.svg.append("path").datum(data).attr("class", "area").attr("d", this.area); - }; - - ContributorsMasterGraph.prototype.add_brush = function() { - return this.svg.append("g").attr("class", "selection").call(this.brush).selectAll("rect").attr("height", this.height); - }; - - ContributorsMasterGraph.prototype.update_content = function() { - ContributorsGraph.set_x_domain(this.brush.empty() ? this.x_max_domain : this.brush.extent()); - return $("#brush_change").trigger('change'); - }; - - ContributorsMasterGraph.prototype.draw = function() { - this.process_dates(this.data); - this.create_scale(); - this.create_axes(); - ContributorsGraph.init_domain(this.data); - this.x_max_domain = this.x_domain; - this.set_domain(); - this.create_area(this.x, this.y); - this.create_svg(); - this.create_brush(); - this.draw_path(this.data); - this.draw_x_axis(); - this.draw_y_axis(); - return this.add_brush(); - }; - - ContributorsMasterGraph.prototype.redraw = function() { - this.process_dates(this.data); - ContributorsGraph.set_y_domain(this.data); - this.set_y_domain(); - this.svg.select("path").datum(this.data); - this.svg.select("path").attr("d", this.area); - return this.svg.select(".y.axis").call(this.y_axis); - }; - - return ContributorsMasterGraph; - })(ContributorsGraph); - - this.ContributorsAuthorGraph = (function(superClass) { - extend(ContributorsAuthorGraph, superClass); - - function ContributorsAuthorGraph(data1) { - this.data = data1; - // Don't split graph size in half for mobile devices. - if ($(window).width() < 768) { - this.width = $('.content').width() - 80; - } else { - this.width = ($('.content').width() / 2) - 100; - } - this.height = 200; - this.x = null; - this.y = null; - this.x_axis = null; - this.y_axis = null; - this.area = null; - this.svg = null; - this.list_item = null; - } - - ContributorsAuthorGraph.prototype.create_scale = function() { - return ContributorsAuthorGraph.__super__.create_scale.call(this, this.width, this.height); - }; - - ContributorsAuthorGraph.prototype.create_axes = function() { - this.x_axis = d3.svg.axis().scale(this.x).orient("bottom").ticks(8); - return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5); - }; - - ContributorsAuthorGraph.prototype.create_area = function(x, y) { - return this.area = d3.svg.area().x(function(d) { - var parseDate; - parseDate = d3.time.format("%Y-%m-%d").parse; - return x(parseDate(d)); - }).y0(this.height).y1((function(_this) { - return function(d) { - if (_this.data[d] != null) { - return y(_this.data[d]); - } else { - return y(0); - } - }; - })(this)).interpolate("basis"); - }; - - ContributorsAuthorGraph.prototype.create_svg = function() { - this.list_item = d3.selectAll(".person")[0].pop(); - return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); - }; - - ContributorsAuthorGraph.prototype.draw_path = function(data) { - return this.svg.append("path").datum(data).attr("class", "area-contributor").attr("d", this.area); - }; - - ContributorsAuthorGraph.prototype.draw = function() { - this.create_scale(); - this.create_axes(); - this.set_domain(); - this.create_area(this.x, this.y); - this.create_svg(); - this.draw_path(this.dates); - this.draw_x_axis(); - return this.draw_y_axis(); - }; - - ContributorsAuthorGraph.prototype.redraw = function() { - this.set_domain(); - this.svg.select("path").datum(this.dates); - this.svg.select("path").attr("d", this.area); - this.svg.select(".x.axis").call(this.x_axis); - return this.svg.select(".y.axis").call(this.y_axis); - }; - - return ContributorsAuthorGraph; - })(ContributorsGraph); -}).call(window); + return x(parseDate(d)); + }).y0(this.height).y1((function(_this) { + return function(d) { + if (_this.data[d] != null) { + return y(_this.data[d]); + } else { + return y(0); + } + }; + })(this)).interpolate("basis"); + }; + + ContributorsAuthorGraph.prototype.create_svg = function() { + this.list_item = d3.selectAll(".person")[0].pop(); + return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + }; + + ContributorsAuthorGraph.prototype.draw_path = function(data) { + return this.svg.append("path").datum(data).attr("class", "area-contributor").attr("d", this.area); + }; + + ContributorsAuthorGraph.prototype.draw = function() { + this.create_scale(); + this.create_axes(); + this.set_domain(); + this.create_area(this.x, this.y); + this.create_svg(); + this.draw_path(this.dates); + this.draw_x_axis(); + return this.draw_y_axis(); + }; + + ContributorsAuthorGraph.prototype.redraw = function() { + this.set_domain(); + this.svg.select("path").datum(this.dates); + this.svg.select("path").attr("d", this.area); + this.svg.select(".x.axis").call(this.x_axis); + return this.svg.select(".y.axis").call(this.y_axis); + }; + + return ContributorsAuthorGraph; +})(ContributorsGraph); diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js index a954bb60560..861f26e162f 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js @@ -1,9 +1,7 @@ -/* eslint-disable quotes, jasmine/no-suite-dupes, vars-on-top, no-var, max-len */ -/* global d3 */ -/* global ContributorsGraph */ -/* global ContributorsMasterGraph */ +/* eslint-disable quotes, jasmine/no-suite-dupes, vars-on-top, no-var */ -require('~/graphs/stat_graph_contributors_graph'); +import d3 from 'd3'; +import { ContributorsGraph, ContributorsMasterGraph } from '~/graphs/stat_graph_contributors_graph'; describe("ContributorsGraph", function () { describe("#set_x_domain", function () { -- cgit v1.2.3 From 52426c65ac5a608a692c660c4309f97df7a98242 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 23 Feb 2017 23:55:34 -0600 Subject: refactor stat_graph_contributors to es6 module syntax --- app/assets/javascripts/graphs/graphs_bundle.js | 5 +- .../javascripts/graphs/stat_graph_contributors.js | 197 ++++++++++----------- 2 files changed, 100 insertions(+), 102 deletions(-) diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js index 914716a5147..ea5afbd9d29 100644 --- a/app/assets/javascripts/graphs/graphs_bundle.js +++ b/app/assets/javascripts/graphs/graphs_bundle.js @@ -1 +1,4 @@ -require('./stat_graph_contributors'); +import ContributorsStatGraph from './stat_graph_contributors'; + +// export to global scope +window.ContributorsStatGraph = ContributorsStatGraph; diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js index a11ee5967e7..c6be4c9e8fe 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js @@ -1,116 +1,111 @@ -/* 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 */ +/* 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, no-shadow */ +import d3 from 'd3'; import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph'; import ContributorsStatGraphUtil from './stat_graph_contributors_util'; -/* global d3 */ +export default (function() { + function ContributorsStatGraph() {} -window.d3 = require('d3'); + ContributorsStatGraph.prototype.init = function(log) { + var author_commits, total_commits; + this.parsed_log = ContributorsStatGraphUtil.parse_log(log); + this.set_current_field("commits"); + total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); + author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field); + this.add_master_graph(total_commits); + this.add_authors_graph(author_commits); + return this.change_date_header(); + }; -(function() { - this.ContributorsStatGraph = (function() { - function ContributorsStatGraph() {} + ContributorsStatGraph.prototype.add_master_graph = function(total_data) { + this.master_graph = new ContributorsMasterGraph(total_data); + return this.master_graph.draw(); + }; - ContributorsStatGraph.prototype.init = function(log) { - var author_commits, total_commits; - this.parsed_log = ContributorsStatGraphUtil.parse_log(log); - this.set_current_field("commits"); - total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); - author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field); - this.add_master_graph(total_commits); - this.add_authors_graph(author_commits); - return this.change_date_header(); - }; + ContributorsStatGraph.prototype.add_authors_graph = function(author_data) { + var limited_author_data; + this.authors = []; + limited_author_data = author_data.slice(0, 100); + return _.each(limited_author_data, (function(_this) { + return function(d) { + var author_graph, author_header; + author_header = _this.create_author_header(d); + $(".contributors-list").append(author_header); + _this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates); + return author_graph.draw(); + }; + })(this)); + }; - ContributorsStatGraph.prototype.add_master_graph = function(total_data) { - this.master_graph = new ContributorsMasterGraph(total_data); - return this.master_graph.draw(); - }; + ContributorsStatGraph.prototype.format_author_commit_info = function(author) { + var commits; + commits = $('', { + "class": 'graph-author-commits-count' + }); + commits.text(author.commits + " commits"); + return $('').append(commits); + }; - ContributorsStatGraph.prototype.add_authors_graph = function(author_data) { - var limited_author_data; - this.authors = []; - limited_author_data = author_data.slice(0, 100); - return _.each(limited_author_data, (function(_this) { - return function(d) { - var author_graph, author_header; - author_header = _this.create_author_header(d); - $(".contributors-list").append(author_header); - _this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates); - return author_graph.draw(); - }; - })(this)); - }; + ContributorsStatGraph.prototype.create_author_header = function(author) { + var author_commit_info, author_commit_info_span, author_email, author_name, list_item; + list_item = $('
  • ', { + "class": 'person', + style: 'display: block;' + }); + author_name = $('

    ' + author.author_name + '

    '); + author_email = $('

    ' + author.author_email + '

    '); + author_commit_info_span = $('', { + "class": 'commits' + }); + author_commit_info = this.format_author_commit_info(author); + author_commit_info_span.html(author_commit_info); + list_item.append(author_name); + list_item.append(author_email); + list_item.append(author_commit_info_span); + return list_item; + }; - ContributorsStatGraph.prototype.format_author_commit_info = function(author) { - var commits; - commits = $('', { - "class": 'graph-author-commits-count' - }); - commits.text(author.commits + " commits"); - return $('').append(commits); - }; + ContributorsStatGraph.prototype.redraw_master = function() { + var total_data; + total_data = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); + this.master_graph.set_data(total_data); + return this.master_graph.redraw(); + }; - ContributorsStatGraph.prototype.create_author_header = function(author) { - var author_commit_info, author_commit_info_span, author_email, author_name, list_item; - list_item = $('
  • ', { - "class": 'person', - style: 'display: block;' - }); - author_name = $('

    ' + author.author_name + '

    '); - author_email = $('

    ' + author.author_email + '

    '); - author_commit_info_span = $('', { - "class": 'commits' - }); - author_commit_info = this.format_author_commit_info(author); - author_commit_info_span.html(author_commit_info); - list_item.append(author_name); - list_item.append(author_email); - list_item.append(author_commit_info_span); - return list_item; - }; + ContributorsStatGraph.prototype.redraw_authors = function() { + var author_commits, x_domain; + $("ol").html(""); + x_domain = ContributorsGraph.prototype.x_domain; + author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain); + return _.each(author_commits, (function(_this) { + return function(d) { + _this.redraw_author_commit_info(d); + $(_this.authors[d.author_name].list_item).appendTo("ol"); + _this.authors[d.author_name].set_data(d.dates); + return _this.authors[d.author_name].redraw(); + }; + })(this)); + }; - ContributorsStatGraph.prototype.redraw_master = function() { - var total_data; - total_data = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); - this.master_graph.set_data(total_data); - return this.master_graph.redraw(); - }; + ContributorsStatGraph.prototype.set_current_field = function(field) { + return this.field = field; + }; - ContributorsStatGraph.prototype.redraw_authors = function() { - var author_commits, x_domain; - $("ol").html(""); - x_domain = ContributorsGraph.prototype.x_domain; - author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain); - return _.each(author_commits, (function(_this) { - return function(d) { - _this.redraw_author_commit_info(d); - $(_this.authors[d.author_name].list_item).appendTo("ol"); - _this.authors[d.author_name].set_data(d.dates); - return _this.authors[d.author_name].redraw(); - }; - })(this)); - }; + ContributorsStatGraph.prototype.change_date_header = function() { + var print, print_date_format, x_domain; + x_domain = ContributorsGraph.prototype.x_domain; + print_date_format = d3.time.format("%B %e %Y"); + print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]); + return $("#date_header").text(print); + }; - ContributorsStatGraph.prototype.set_current_field = function(field) { - return this.field = field; - }; + ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) { + var author_commit_info, author_list_item; + author_list_item = $(this.authors[author.author_name].list_item); + author_commit_info = this.format_author_commit_info(author); + return author_list_item.find("span").html(author_commit_info); + }; - ContributorsStatGraph.prototype.change_date_header = function() { - var print, print_date_format, x_domain; - x_domain = ContributorsGraph.prototype.x_domain; - print_date_format = d3.time.format("%B %e %Y"); - print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]); - return $("#date_header").text(print); - }; - - ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) { - var author_commit_info, author_list_item; - author_list_item = $(this.authors[author.author_name].list_item); - author_commit_info = this.format_author_commit_info(author); - return author_list_item.find("span").html(author_commit_info); - }; - - return ContributorsStatGraph; - })(); -}).call(window); + return ContributorsStatGraph; +})(); -- cgit v1.2.3 From 631d3c6baa5501b7aeb871d07e9894ab4ab917b6 Mon Sep 17 00:00:00 2001 From: Lukas Raska Date: Fri, 24 Feb 2017 07:50:45 +0100 Subject: Use correct GitLab Prometheus exporter name in docs --- doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md b/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md index 86ef9d167e2..edb9c911aac 100644 --- a/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md +++ b/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md @@ -13,7 +13,7 @@ To enable the GitLab monitor exporter: 1. Add or find and uncomment the following line, making sure it's set to `true`: ```ruby - gitlab_monitor_exporter['enable'] = true + gitlab_monitor['enable'] = true ``` 1. Save the file and [reconfigure GitLab][reconfigure] for the changes to -- cgit v1.2.3 From 510cd9315eaa292a3c81c105080414e0b053b034 Mon Sep 17 00:00:00 2001 From: Lukas Raska Date: Fri, 24 Feb 2017 08:34:48 +0100 Subject: Use persistent name identifier instead of transient in SAML2 documentation --- doc/integration/saml.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/integration/saml.md b/doc/integration/saml.md index 7a809eddac0..2277aa827b7 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -74,7 +74,7 @@ in your SAML IdP: idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', idp_sso_target_url: 'https://login.example.com/idp', issuer: 'https://gitlab.example.com', - name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent' }, label: 'Company Login' # optional label for SAML login button, defaults to "Saml" } @@ -91,7 +91,7 @@ in your SAML IdP: idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', idp_sso_target_url: 'https://login.example.com/idp', issuer: 'https://gitlab.example.com', - name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent' }, label: 'Company Login' # optional label for SAML login button, defaults to "Saml" } @@ -172,7 +172,7 @@ tell GitLab which groups are external via the `external_groups:` element: idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', idp_sso_target_url: 'https://login.example.com/idp', issuer: 'https://gitlab.example.com', - name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent' } } ``` @@ -227,7 +227,7 @@ args: { idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', idp_sso_target_url: 'https://login.example.com/idp', issuer: 'https://gitlab.example.com', - name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', attribute_statements: { email: ['EmailAddress'] } } ``` @@ -245,7 +245,7 @@ args: { idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', idp_sso_target_url: 'https://login.example.com/idp', issuer: 'https://gitlab.example.com', - name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', attribute_statements: { email: ['EmailAddress'] }, allowed_clock_drift: 1 # for one second clock drift } -- cgit v1.2.3 From ac531c0ee5121faed524157fc1a2e0d4bd13297d Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 24 Feb 2017 17:15:25 +0800 Subject: Test against default to '0', it should not set --- spec/requests/ci/api/builds_spec.rb | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index c7284be09b7..9948d1a9ea0 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -668,14 +668,28 @@ describe Ci::API::Builds do end context 'with application default' do - let(:default_artifacts_expire_in) { '5 days' } - - it 'sets to application default' do - build.reload - expect(response).to have_http_status(201) - expect(json_response['artifacts_expire_at']).not_to be_empty - expect(build.artifacts_expire_at). - to be_within(5.minutes).of(5.days.from_now) + context 'default to 5 days' do + let(:default_artifacts_expire_in) { '5 days' } + + it 'sets to application default' do + build.reload + expect(response).to have_http_status(201) + expect(json_response['artifacts_expire_at']) + .not_to be_empty + expect(build.artifacts_expire_at) + .to be_within(5.minutes).of(5.days.from_now) + end + end + + context 'default to 0' do + let(:default_artifacts_expire_in) { '0' } + + it 'does not set expire_in' do + build.reload + expect(response).to have_http_status(201) + expect(json_response['artifacts_expire_at']).to be_nil + expect(build.artifacts_expire_at).to be_nil + end end end end -- cgit v1.2.3 From 728b0a5fe073f1f2f088f9d2c6ce9a4a050e1fcf Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 24 Feb 2017 17:28:24 +0800 Subject: Introduce DurationValidator, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9219#note_24032923 --- app/models/application_setting.rb | 9 +-------- app/validators/duration_validator.rb | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 app/validators/duration_validator.rb diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 99759ec4cae..dc36c754438 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -80,8 +80,7 @@ class ApplicationSetting < ActiveRecord::Base presence: true, numericality: { only_integer: true, greater_than: 0 } - validates :default_artifacts_expire_in, presence: true - validate :check_default_artifacts_expire_in + validates :default_artifacts_expire_in, presence: true, duration: true validates :container_registry_token_expire_delay, presence: true, @@ -305,10 +304,4 @@ class ApplicationSetting < ActiveRecord::Base errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless invalid.empty? end - - def check_default_artifacts_expire_in - ChronicDuration.parse(default_artifacts_expire_in) - rescue ChronicDuration::DurationParseError - errors.add(:default_artifacts_expire_in, "is not a correct duration") - end end diff --git a/app/validators/duration_validator.rb b/app/validators/duration_validator.rb new file mode 100644 index 00000000000..10ff44031c6 --- /dev/null +++ b/app/validators/duration_validator.rb @@ -0,0 +1,17 @@ +# DurationValidator +# +# Validate the format conforms with ChronicDuration +# +# Example: +# +# class ApplicationSetting < ActiveRecord::Base +# validates :default_artifacts_expire_in, presence: true, duration: true +# end +# +class DurationValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + ChronicDuration.parse(value) + rescue ChronicDuration::DurationParseError + record.errors.add(attribute, "is not a correct duration") + end +end -- cgit v1.2.3 From 161500366561297626ca6df76fb68beb846d5249 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Fri, 24 Feb 2017 09:25:53 +0100 Subject: API: Use parameter to get owned groups instead of dedicated endpoint --- lib/api/groups.rb | 16 ++++------------ spec/requests/api/groups_spec.rb | 14 ++------------ 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 9f29c4466ab..9cffd6180ae 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -36,12 +36,15 @@ module API optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list' optional :all_available, type: Boolean, desc: 'Show all group that you have access to' optional :search, type: String, desc: 'Search for a specific group' + optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user' 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 + groups = if params[:owned] + current_user.owned_groups + elsif current_user.admin Group.all elsif params[:all_available] GroupsFinder.new.execute(current_user) @@ -56,17 +59,6 @@ module API present_groups groups, statistics: params[:statistics] && current_user.is_admin? end - desc 'Get list of owned groups for authenticated user' do - success Entities::Group - end - params do - use :pagination - use :statistics_params - end - get '/owned' do - present_groups current_user.owned_groups, statistics: params[:statistics] - end - desc 'Create a group. Available only for users who can create groups.' do success Entities::Group end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 9c3a92bedbd..f54a5e77866 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -150,20 +150,10 @@ describe API::Groups, api: true do expect(response_groups).to eq([group1.name, group3.name]) end end - end - - describe 'GET /groups/owned' do - context 'when unauthenticated' do - it 'returns authentication error' do - get api('/groups/owned') - - expect(response).to have_http_status(401) - end - end - context 'when authenticated as group owner' do + context 'when using owned in the request' do it 'returns an array of groups the user owns' do - get api('/groups/owned', user2) + get api('/groups', user2), owned: true expect(response).to have_http_status(200) expect(response).to include_pagination_headers -- cgit v1.2.3 From 2ac84e36f92a14f70e4dd18fa872bea3d997f87a Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Fri, 24 Feb 2017 09:29:53 +0100 Subject: Backport groups API to V3 --- lib/api/api.rb | 1 + lib/api/v3/groups.rb | 38 +++++++++++++++++++++++++++++++++++++ spec/requests/api/v3/groups_spec.rb | 35 ++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 lib/api/v3/groups.rb create mode 100644 spec/requests/api/v3/groups_spec.rb diff --git a/lib/api/api.rb b/lib/api/api.rb index 1803387bb8c..dc732012a33 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -10,6 +10,7 @@ module API mount ::API::V3::Commits mount ::API::V3::DeployKeys mount ::API::V3::Files + mount ::API::V3::Groups mount ::API::V3::Issues mount ::API::V3::Labels mount ::API::V3::Members diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb new file mode 100644 index 00000000000..c826bc4fe0b --- /dev/null +++ b/lib/api/v3/groups.rb @@ -0,0 +1,38 @@ +module API + module V3 + class Groups < Grape::API + include PaginationParams + + before { authenticate! } + + helpers do + params :statistics_params do + optional :statistics, type: Boolean, default: false, desc: 'Include project statistics' + end + + def present_groups(groups, options = {}) + options = options.reverse_merge( + with: ::API::Entities::Group, + current_user: current_user, + ) + + groups = groups.with_statistics if options[:statistics] + present paginate(groups), options + end + end + + resource :groups do + desc 'Get list of owned groups for authenticated user' do + success ::API::Entities::Group + end + params do + use :pagination + use :statistics_params + end + get '/owned' do + present_groups current_user.owned_groups, statistics: params[:statistics] + end + end + end + end +end diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb new file mode 100644 index 00000000000..8b29ad03737 --- /dev/null +++ b/spec/requests/api/v3/groups_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe API::V3::Groups, api: true do + include ApiHelpers + include UploadHelpers + + let(:user2) { create(:user) } + let!(:group2) { create(:group, :private) } + let!(:project2) { create(:empty_project, namespace: group2) } + + before do + group2.add_owner(user2) + end + + describe 'GET /groups/owned' do + context 'when unauthenticated' do + it 'returns authentication error' do + get v3_api('/groups/owned') + + expect(response).to have_http_status(401) + end + end + + context 'when authenticated as group owner' do + it 'returns an array of groups the user owns' do + get v3_api('/groups/owned', user2) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(group2.name) + end + end + end +end -- cgit v1.2.3 From a82e3d30b2ef625dae0d2f25324460402f960f09 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Fri, 24 Feb 2017 09:37:38 +0100 Subject: Update documentation --- changelogs/unreleased/api-remove-owned-groups.yml | 4 ++++ doc/api/groups.md | 15 +-------------- doc/api/v3_to_v4.md | 1 + 3 files changed, 6 insertions(+), 14 deletions(-) create mode 100644 changelogs/unreleased/api-remove-owned-groups.yml diff --git a/changelogs/unreleased/api-remove-owned-groups.yml b/changelogs/unreleased/api-remove-owned-groups.yml new file mode 100644 index 00000000000..cf0301b7fe0 --- /dev/null +++ b/changelogs/unreleased/api-remove-owned-groups.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Remove /groups/owned endpoint' +merge_request: 9505 +author: Robert Schilling diff --git a/doc/api/groups.md b/doc/api/groups.md index 4a39dbc5555..39adb5be502 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -14,6 +14,7 @@ Parameters: | `order_by` | string | no | Order groups by `name` or `path`. Default is `name` | | `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` | | `statistics` | boolean | no | Include group statistics (admins only) | +| `owned` | boolean | no | Limit by groups owned by the current user | ``` GET /groups @@ -40,20 +41,6 @@ GET /groups You can search for groups by name or path, see below. -## List owned groups - -Get a list of groups which are owned by the authenticated user. - -``` -GET /groups/owned -``` - -Parameters: - -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `statistics` | boolean | no | Include group statistics | - ## List a group's projects Get a list of projects in this group. diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 9a48d63c117..b0feb2a0d16 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -41,3 +41,4 @@ changes are in V4: - Renamed `branch_name` to `branch` on DELETE `id/repository/branches/:branch` response [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936) - Remove `public` param from create and edit actions of projects [!8736](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8736) - Notes do not return deprecated field `upvote` and `downvote` [!9384](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9384) +- Remove `GET /groups/owned`. Use `GET /groups?owned=true` instead [!9505](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9505) -- cgit v1.2.3 From bff14cedf320d593520ee3de9cfd753399dd2de9 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Fri, 24 Feb 2017 11:24:40 +0100 Subject: Simplyfy variables validation in triggers API --- lib/api/triggers.rb | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index 87a717ba751..ea0ad852633 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -21,14 +21,9 @@ module API unauthorized! unless trigger.project == project # validate variables - variables = params[:variables] - if variables - unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) } - render_api_error!('variables needs to be a map of key-valued strings', 400) - end - - # convert variables from Mash to Hash - variables = variables.to_h + variables = params[:variables].to_h + unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) } + render_api_error!('variables needs to be a map of key-valued strings', 400) end # create request and trigger builds -- cgit v1.2.3 From f2ebb63a8ecca260a31fe55c2eead76ff8da5686 Mon Sep 17 00:00:00 2001 From: Pedro Moreira da Silva Date: Fri, 10 Feb 2017 15:26:28 +0000 Subject: Add clearer placeholders and channel definition. --- app/models/project_services/mattermost_service.rb | 14 +++++++------- app/models/project_services/slack_service.rb | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb index 4ebc5318da1..c13538e9fea 100644 --- a/app/models/project_services/mattermost_service.rb +++ b/app/models/project_services/mattermost_service.rb @@ -15,10 +15,10 @@ class MattermostService < ChatNotificationService 'This service sends notifications about projects events to Mattermost channels.
    To set up this service:
      -
    1. Enable incoming webhooks in your Mattermost installation.
    2. -
    3. Add an incoming webhook in your Mattermost team. The default channel can be overridden for each event.
    4. -
    5. Paste the webhook URL into the field bellow.
    6. -
    7. Select events below to enable notifications. The channel and username are optional.
    8. +
    9. Enable incoming webhooks in your Mattermost installation.
    10. +
    11. Add an incoming webhook in your Mattermost team. The default channel can be overridden for each event.
    12. +
    13. Paste the webhook URL into the field below.
    14. +
    15. Select events below to enable notifications. The Channel handle and Username fields are optional.
    ' end @@ -28,14 +28,14 @@ class MattermostService < ChatNotificationService def default_fields [ - { type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' }, - { type: 'text', name: 'username', placeholder: 'username' }, + { type: 'text', name: 'webhook', placeholder: 'e.g. http://mattermost_host/hooks/…' }, + { type: 'text', name: 'username', placeholder: 'e.g. GitLab' }, { type: 'checkbox', name: 'notify_only_broken_builds' }, { type: 'checkbox', name: 'notify_only_broken_pipelines' }, ] end def default_channel_placeholder - "town-square" + "Channel handle (e.g. town-square)" end end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index f77d2d7c60b..da7496573ef 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -13,11 +13,11 @@ class SlackService < ChatNotificationService def help 'This service sends notifications about projects events to Slack channels.
    - To setup this service: + To set up this service:
      -
    1. Add an incoming webhook in your Slack team. The default channel can be overridden for each event.
    2. -
    3. Paste the Webhook URL into the field below.
    4. -
    5. Select events below to enable notifications. The channel and username are optional.
    6. +
    7. Add an incoming webhook in your Slack team. The default channel can be overridden for each event.
    8. +
    9. Paste the Webhook URL into the field below.
    10. +
    11. Select events below to enable notifications. The Channel name and Username fields are optional.
    ' end @@ -27,14 +27,14 @@ class SlackService < ChatNotificationService def default_fields [ - { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' }, - { type: 'text', name: 'username', placeholder: 'username' }, + { type: 'text', name: 'webhook', placeholder: 'e.g. https://hooks.slack.com/services/…' }, + { type: 'text', name: 'username', placeholder: 'e.g. GitLab' }, { type: 'checkbox', name: 'notify_only_broken_builds' }, { type: 'checkbox', name: 'notify_only_broken_pipelines' }, ] end def default_channel_placeholder - "#general" + "Channel name (e.g. general)" end end -- cgit v1.2.3 From 14976f3db8636737e66ed2122011314cfdeb6fa9 Mon Sep 17 00:00:00 2001 From: Pedro Moreira da Silva Date: Fri, 10 Feb 2017 15:49:52 +0000 Subject: Update docs on Mattermost and Slack notifications channels --- .../integrations/img/mattermost_configuration.png | Bin 73502 -> 249592 bytes .../integrations/img/slack_configuration.png | Bin 29825 -> 229050 bytes doc/user/project/integrations/mattermost.md | 11 ++++++----- doc/user/project/integrations/slack.md | 10 ++++++---- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/doc/user/project/integrations/img/mattermost_configuration.png b/doc/user/project/integrations/img/mattermost_configuration.png index 3c5ff5ee317..f52acf4ef3b 100644 Binary files a/doc/user/project/integrations/img/mattermost_configuration.png and b/doc/user/project/integrations/img/mattermost_configuration.png differ diff --git a/doc/user/project/integrations/img/slack_configuration.png b/doc/user/project/integrations/img/slack_configuration.png index fc8e58e686b..527824fc3eb 100644 Binary files a/doc/user/project/integrations/img/slack_configuration.png and b/doc/user/project/integrations/img/slack_configuration.png differ diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md index 09ba9994d3a..cfb0931273d 100644 --- a/doc/user/project/integrations/mattermost.md +++ b/doc/user/project/integrations/mattermost.md @@ -24,23 +24,24 @@ There, you will see a checkbox with the following events that can be triggered: - Push - Issue +- Confidential issue - Merge request - Note - Tag push - Build +- Pipeline - 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. +Below each of these event checkboxes, you have an input field to enter +which Mattermost channel you want to send that event message. Enter your preferred channel handle (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... | +| **Webhook** | The incoming webhook URL 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. | - +| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. | ![Mattermost configuration](img/mattermost_configuration.png) diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md index 57a9492044b..f27f9a726fc 100644 --- a/doc/user/project/integrations/slack.md +++ b/doc/user/project/integrations/slack.md @@ -21,23 +21,25 @@ There, you will see a checkbox with the following events that can be triggered: - Push - Issue +- Confidential issue - Merge request - Note - Tag push - Build +- Pipeline - Wiki page -Bellow each of these event checkboxes, you will have an input field to insert -which Slack channel you want to send that event message, with `#general` -being the default. Enter your preferred channel **without** the hash sign (`#`). +Below each of these event checkboxes, you have an input field to enter +which Slack channel you want to send that event message. Enter your preferred channel name **without** the hash sign (`#`). At the end, fill in your Slack details: | Field | Description | | ----- | ----------- | | **Webhook** | The [incoming webhook URL][slackhook] which you have to setup on Slack. | -| **Username** | Optional username which can be on messages sent to slack. Fill this in if you want to change the username of the bot. | +| **Username** | Optional username which can be on messages sent to Slack. 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. | +| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. | After you are all done, click **Save changes** for the changes to take effect. -- cgit v1.2.3 From 1854c3f768d944b385be9a865deb5d869a84b7a6 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Thu, 10 Nov 2016 12:33:48 +0530 Subject: Add CE CHANGELOG for #12726. --- changelogs/unreleased/12726-preserve-issues-after-deleting-users.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/12726-preserve-issues-after-deleting-users.yml diff --git a/changelogs/unreleased/12726-preserve-issues-after-deleting-users.yml b/changelogs/unreleased/12726-preserve-issues-after-deleting-users.yml new file mode 100644 index 00000000000..4a1a199673c --- /dev/null +++ b/changelogs/unreleased/12726-preserve-issues-after-deleting-users.yml @@ -0,0 +1,4 @@ +--- +title: Deleting a user doesn't delete issues they've created/are assigned to +merge_request: 7393 +author: -- cgit v1.2.3 From 29540d6f35da643b4f1894dc5ffd4c0a22f7cd3e Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Wed, 21 Dec 2016 15:20:09 +0530 Subject: Don't send notifications to ghost users. We already skip sending notifications to blocked users. Simply add ghost users to this list. --- app/services/notification_service.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 3734e3c4253..97db117aa99 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -466,6 +466,7 @@ class NotificationService users = users.to_a.compact.uniq users = users.reject(&:blocked?) + users = users.reject(&:ghost?) users.reject do |user| global_notification_setting = user.global_notification_setting -- cgit v1.2.3 From ff19bbd3b40621ae94632b9aa68fd12645b6ed41 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 11 Nov 2016 11:57:43 +0530 Subject: Deleting a user shouldn't delete associated issues. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "Associated" issues are issues the user has created + issues that the user is assigned to. - Issues that a user owns are transferred to a "Ghost User" (just a regular user with `state = 'ghost'` that is created when `User.ghost` is called). - Issues that a user is assigned to are moved to the "Unassigned" state. - Fix a spec failure in `profile_spec` — a spec was asserting that when a user is deleted, `User.count` decreases by 1. After this change, deleting a user creates (potentially) a ghost user, causing `User.count` not to change. The spec has been updated to look for the relevant user in the assertion. --- app/models/user.rb | 32 ++++++++++++++++++++++++- app/services/users/destroy_service.rb | 14 +++++++++++ spec/features/profile_spec.rb | 4 ++-- spec/models/user_spec.rb | 39 ++++++++++++++++++++++++++++++- spec/services/users/destroy_spec.rb | 44 +++++++++++++++++++++++++++++++++++ 5 files changed, 129 insertions(+), 4 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index fada0e567f0..23beaa6c7c8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -81,7 +81,6 @@ class User < ActiveRecord::Base has_many :authorized_projects, through: :project_authorizations, source: :project has_many :snippets, dependent: :destroy, foreign_key: :author_id - has_many :issues, dependent: :destroy, foreign_key: :author_id has_many :notes, dependent: :destroy, foreign_key: :author_id has_many :merge_requests, dependent: :destroy, foreign_key: :author_id has_many :events, dependent: :destroy, foreign_key: :author_id @@ -99,6 +98,11 @@ class User < ActiveRecord::Base has_many :assigned_issues, dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue" has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" + # Issues that a user owns are expected to be moved to the "ghost" user before + # the user is destroyed. If the user owns any issues during deletion, this + # should be treated as an exceptional condition. + has_many :issues, dependent: :restrict_with_exception, foreign_key: :author_id + # # Validations # @@ -181,6 +185,8 @@ class User < ActiveRecord::Base "administrator if you think this is an error." end end + + state :ghost end mount_uploader :avatar, AvatarUploader @@ -337,6 +343,30 @@ class User < ActiveRecord::Base (?#{Gitlab::Regex::NAMESPACE_REF_REGEX_STR}) }x end + + # Return (create if necessary) the ghost user. The ghost user + # owns records previously belonging to deleted users. + def ghost + ghost_user = User.with_state(:ghost).first + + ghost_user || + begin + users = Enumerator.new do |y| + n = nil + loop do + user = User.new( + username: "ghost#{n}", password: Devise.friendly_token, + email: "ghost#{n}@example.com", name: "Ghost User", state: :ghost + ) + + y.yield(user) + n = n ? n.next : 0 + end + end + + users.lazy.select { |user| user.valid? }.first.tap(&:save!) + end + end end # diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb index bc0653cb634..d88b81c04e2 100644 --- a/app/services/users/destroy_service.rb +++ b/app/services/users/destroy_service.rb @@ -26,6 +26,8 @@ module Users ::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute end + move_issues_to_ghost_user(user) + # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing namespace = user.namespace user_data = user.destroy @@ -33,5 +35,17 @@ module Users user_data end + + private + + def move_issues_to_ghost_user(user) + ghost_user = User.ghost + + Issue.transaction do + user.issues.each { |issue| issue.update!(author: ghost_user) } + end + + user.reload + end end end diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb index 7a562b5e03d..406d7cf791c 100644 --- a/spec/features/profile_spec.rb +++ b/spec/features/profile_spec.rb @@ -4,7 +4,7 @@ describe 'Profile account page', feature: true do let(:user) { create(:user) } before do - login_as :user + login_as(user) end describe 'when signup is enabled' do @@ -16,7 +16,7 @@ describe 'Profile account page', feature: true do it { expect(page).to have_content('Remove account') } it 'deletes the account' do - expect { click_link 'Delete account' }.to change { User.count }.by(-1) + expect { click_link 'Delete account' }.to change { User.where(id: user.id).count }.by(-1) expect(current_path).to eq(new_user_session_path) end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index b69fd24c586..00e7927bf90 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -22,7 +22,7 @@ describe User, models: true do it { is_expected.to have_many(:deploy_keys).dependent(:destroy) } it { is_expected.to have_many(:events).dependent(:destroy) } it { is_expected.to have_many(:recent_events).class_name('Event') } - it { is_expected.to have_many(:issues).dependent(:destroy) } + it { is_expected.to have_many(:issues).dependent(:restrict_with_exception) } it { is_expected.to have_many(:notes).dependent(:destroy) } it { is_expected.to have_many(:assigned_issues).dependent(:nullify) } it { is_expected.to have_many(:merge_requests).dependent(:destroy) } @@ -1490,4 +1490,41 @@ describe User, models: true do expect(user.admin).to be true end end + + describe '.ghost' do + it "creates a ghost user if one isn't already present" do + ghost = User.ghost + + expect(ghost).to be_ghost + expect(ghost).to be_persisted + end + + it "does not create a second ghost user if one is already present" do + expect do + User.ghost + User.ghost + end.to change { User.count }.by(1) + expect(User.ghost).to eq(User.ghost) + end + + context "when a regular user exists with the username 'ghost'" do + it "creates a ghost user with a non-conflicting username" do + create(:user, username: 'ghost') + ghost = User.ghost + + expect(ghost).to be_persisted + expect(ghost.username).to eq('ghost0') + end + end + + context "when a regular user exists with the email 'ghost@example.com'" do + it "creates a ghost user with a non-conflicting email" do + create(:user, email: 'ghost@example.com') + ghost = User.ghost + + expect(ghost).to be_persisted + expect(ghost.email).to eq('ghost0@example.com') + end + end + end end diff --git a/spec/services/users/destroy_spec.rb b/spec/services/users/destroy_spec.rb index c0bf27c698c..4f0834064e5 100644 --- a/spec/services/users/destroy_spec.rb +++ b/spec/services/users/destroy_spec.rb @@ -24,6 +24,50 @@ describe Users::DestroyService, services: true do end end + context "a deleted user's issues" do + let(:project) { create :project } + + before do + project.add_developer(user) + end + + context "for an issue the user has created" do + let!(:issue) { create(:issue, project: project, author: user) } + + before do + service.execute(user) + end + + it 'does not delete the issue' do + expect(Issue.find_by_id(issue.id)).to be_present + end + + it 'migrates the issue so that the "Ghost User" is the issue owner' do + migrated_issue = Issue.find_by_id(issue.id) + + expect(migrated_issue.author).to eq(User.ghost) + end + end + + context "for an issue the user was assigned to" do + let!(:issue) { create(:issue, project: project, assignee: user) } + + before do + service.execute(user) + end + + it 'does not delete issues the user is assigned to' do + expect(Issue.find_by_id(issue.id)).to be_present + end + + it 'migrates the issue so that it is "Unassigned"' do + migrated_issue = Issue.find_by_id(issue.id) + + expect(migrated_issue.assignee).to be_nil + end + end + end + context "solo owned groups present" do let(:solo_owned) { create(:group) } let(:member) { create(:group_member) } -- cgit v1.2.3 From ca16c3734b7b89f71bdc9e1c18152aa1599c4f89 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 3 Feb 2017 18:25:50 +0530 Subject: Extract code from `Namespace#clean_path` for ghost user generation. 1. Create a `Uniquify` class, which generalizes the process of generating unique strings, by accepting a function that defines what "uniqueness" means in a given context. 2. WIP: Make sure tests for `Namespace` pass, add more if necessary. 3. WIP: Add tests for `Uniquify` --- app/models/concerns/uniquify.rb | 25 +++++++++++++++++++++++++ app/models/namespace.rb | 10 ++-------- app/models/user.rb | 27 +++++++++++++-------------- spec/models/user_spec.rb | 4 ++-- 4 files changed, 42 insertions(+), 24 deletions(-) create mode 100644 app/models/concerns/uniquify.rb diff --git a/app/models/concerns/uniquify.rb b/app/models/concerns/uniquify.rb new file mode 100644 index 00000000000..1485ab6ae99 --- /dev/null +++ b/app/models/concerns/uniquify.rb @@ -0,0 +1,25 @@ +class Uniquify + # Return a version of the given 'base' string that is unique + # by appending a counter to it. Uniqueness is determined by + # repeated calls to `exists_fn`. + # + # If `base` is a function/proc, we expect that calling it with a + # candidate counter returns a string to test/return. + def string(base, exists_fn) + @counter = nil + + if base.respond_to?(:call) + increment_counter! while exists_fn[base.call(@counter)] + base.call(@counter) + else + increment_counter! while exists_fn["#{base}#{@counter}"] + "#{base}#{@counter}" + end + end + + private + + def increment_counter! + @counter = @counter ? @counter.next : 1 + end +end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index bd0336c984a..8cc3c1473e2 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -98,14 +98,8 @@ class Namespace < ActiveRecord::Base # Work around that by setting their username to "blank", followed by a counter. path = "blank" if path.blank? - counter = 0 - base = path - while Namespace.find_by_path_or_name(path) - counter += 1 - path = "#{base}#{counter}" - end - - path + uniquify = Uniquify.new + uniquify.string(path, -> (s) { Namespace.find_by_path_or_name(s) }) end end diff --git a/app/models/user.rb b/app/models/user.rb index 23beaa6c7c8..13529c9997a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -351,20 +351,19 @@ class User < ActiveRecord::Base ghost_user || begin - users = Enumerator.new do |y| - n = nil - loop do - user = User.new( - username: "ghost#{n}", password: Devise.friendly_token, - email: "ghost#{n}@example.com", name: "Ghost User", state: :ghost - ) - - y.yield(user) - n = n ? n.next : 0 - end - end - - users.lazy.select { |user| user.valid? }.first.tap(&:save!) + uniquify = Uniquify.new + + username = uniquify.string("ghost", -> (s) { User.find_by_username(s) }) + + email = uniquify.string( + -> (n) { "ghost#{n}@example.com" }, + -> (s) { User.find_by_email(s) } + ) + + User.create( + username: username, password: Devise.friendly_token, + email: email, name: "Ghost User", state: :ghost + ) end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 00e7927bf90..861d338ef95 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1513,7 +1513,7 @@ describe User, models: true do ghost = User.ghost expect(ghost).to be_persisted - expect(ghost.username).to eq('ghost0') + expect(ghost.username).to eq('ghost1') end end @@ -1523,7 +1523,7 @@ describe User, models: true do ghost = User.ghost expect(ghost).to be_persisted - expect(ghost.email).to eq('ghost0@example.com') + expect(ghost.email).to eq('ghost1@example.com') end end end -- cgit v1.2.3 From 53c34c7436112d7cac9c3887ada1d5ae630a206c Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 6 Feb 2017 17:18:27 +0530 Subject: Implement review comments from @DouweM and @nick.thomas. 1. Use an advisory lock to guarantee the absence of concurrency in `User.ghost`, to prevent data races from creating more than one ghost, or preventing the creation of ghost users by causing validation errors. 2. Use `update_all` instead of updating issues one-by-one. --- app/models/user.rb | 13 ++++++ app/services/users/destroy_service.rb | 4 +- lib/gitlab/database/advisory_locking.rb | 70 +++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 lib/gitlab/database/advisory_locking.rb diff --git a/app/models/user.rb b/app/models/user.rb index 13529c9997a..e4adcd6cde9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -351,6 +351,17 @@ class User < ActiveRecord::Base ghost_user || begin + # Since we only want a single ghost user in an instance, we use an + # advisory lock to ensure than this block is never run concurrently. + advisory_lock = Gitlab::Database::AdvisoryLocking.new(:ghost_user) + advisory_lock.lock + + # Recheck if a ghost user is already present (one might have been) + # added between the time we last checked (first line of this method) + # and the time we acquired the lock. + ghost_user = User.with_state(:ghost).first + return ghost_user if ghost_user.present? + uniquify = Uniquify.new username = uniquify.string("ghost", -> (s) { User.find_by_username(s) }) @@ -364,6 +375,8 @@ class User < ActiveRecord::Base username: username, password: Devise.friendly_token, email: email, name: "Ghost User", state: :ghost ) + ensure + advisory_lock.unlock end end end diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb index d88b81c04e2..bf1e49734cc 100644 --- a/app/services/users/destroy_service.rb +++ b/app/services/users/destroy_service.rb @@ -37,12 +37,12 @@ module Users end private - + def move_issues_to_ghost_user(user) ghost_user = User.ghost Issue.transaction do - user.issues.each { |issue| issue.update!(author: ghost_user) } + user.issues.update_all(author_id: ghost_user.id) end user.reload diff --git a/lib/gitlab/database/advisory_locking.rb b/lib/gitlab/database/advisory_locking.rb new file mode 100644 index 00000000000..950898288be --- /dev/null +++ b/lib/gitlab/database/advisory_locking.rb @@ -0,0 +1,70 @@ +# An advisory lock is an application-level database lock which isn't tied +# to a specific table or row. +# +# Postgres names its advisory locks with integers, while MySQL uses strings. +# We support both here by using a `LOCK_TYPES` map of symbols to integers. +# The symbol (stringified) is used for MySQL, and the corresponding integer +# is used for Postgres. +module Gitlab + module Database + class AdvisoryLocking + LOCK_TYPES = { + ghost_user: 1 + } + + def initialize(lock_type) + @lock_type = lock_type + end + + def lock + ensure_valid_lock_type! + + query = + if Gitlab::Database.postgresql? + Arel::SelectManager.new(ActiveRecord::Base).project( + Arel::Nodes::NamedFunction.new("pg_advisory_lock", [LOCK_TYPES[@lock_type]]) + ) + elsif Gitlab::Database.mysql? + Arel::SelectManager.new(ActiveRecord::Base).project( + Arel::Nodes::NamedFunction.new("get_lock", [Arel.sql("'#{@lock_type}'"), -1]) + ) + end + + run_query(query) + end + + def unlock + ensure_valid_lock_type! + + query = + if Gitlab::Database.postgresql? + Arel::SelectManager.new(ActiveRecord::Base).project( + Arel::Nodes::NamedFunction.new("pg_advisory_unlock", [LOCK_TYPES[@lock_type]]) + ) + elsif Gitlab::Database.mysql? + Arel::SelectManager.new(ActiveRecord::Base).project( + Arel::Nodes::NamedFunction.new("release_lock", [Arel.sql("'#{@lock_type}'")]) + ) + end + + run_query(query) + end + + private + + def ensure_valid_lock_type! + unless valid_lock_type? + raise RuntimeError, "Trying to use an advisory lock with an invalid lock type, #{@lock_type}." + end + end + + def valid_lock_type? + LOCK_TYPES.keys.include?(@lock_type) + end + + def run_query(arel_query) + ActiveRecord::Base.connection.execute(arel_query.to_sql) + end + end + end +end -- cgit v1.2.3 From 8e684809765fa866a125c176327825ebc565f5b3 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 6 Feb 2017 17:37:05 +0530 Subject: Use a `ghost` boolean to track ghost users. Rather than using a separate `ghost` state. This lets us have the benefits of both ghost and blocked users (ghost: true, state: blocked) without having to rewrite a number of queries to include cases for `state: ghost`. --- app/models/user.rb | 15 ++++++++++----- app/services/notification_service.rb | 1 - db/migrate/20170206115204_add_column_ghost_to_users.rb | 15 +++++++++++++++ db/schema.rb | 1 + spec/factories/users.rb | 4 ++++ spec/models/user_spec.rb | 14 ++++++++++++++ 6 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 db/migrate/20170206115204_add_column_ghost_to_users.rb diff --git a/app/models/user.rb b/app/models/user.rb index e4adcd6cde9..f71d7e90d33 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -124,6 +124,7 @@ class User < ActiveRecord::Base validate :unique_email, if: ->(user) { user.email_changed? } validate :owns_notification_email, if: ->(user) { user.notification_email_changed? } validate :owns_public_email, if: ->(user) { user.public_email_changed? } + validate :ghost_users_must_be_blocked validates :avatar, file_size: { maximum: 200.kilobytes.to_i } before_validation :generate_password, on: :create @@ -185,8 +186,6 @@ class User < ActiveRecord::Base "administrator if you think this is an error." end end - - state :ghost end mount_uploader :avatar, AvatarUploader @@ -347,7 +346,7 @@ class User < ActiveRecord::Base # Return (create if necessary) the ghost user. The ghost user # owns records previously belonging to deleted users. def ghost - ghost_user = User.with_state(:ghost).first + ghost_user = User.find_by_ghost(true) ghost_user || begin @@ -359,7 +358,7 @@ class User < ActiveRecord::Base # Recheck if a ghost user is already present (one might have been) # added between the time we last checked (first line of this method) # and the time we acquired the lock. - ghost_user = User.with_state(:ghost).first + ghost_user = User.find_by_ghost(true) return ghost_user if ghost_user.present? uniquify = Uniquify.new @@ -373,7 +372,7 @@ class User < ActiveRecord::Base User.create( username: username, password: Devise.friendly_token, - email: email, name: "Ghost User", state: :ghost + email: email, name: "Ghost User", state: :blocked, ghost: true ) ensure advisory_lock.unlock @@ -477,6 +476,12 @@ class User < ActiveRecord::Base errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email) end + def ghost_users_must_be_blocked + if ghost? && !blocked? + errors.add(:ghost, 'cannot be enabled for a user who is not blocked.') + end + end + def update_emails_with_primary_email primary_email_record = emails.find_by(email: email) if primary_email_record diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 97db117aa99..3734e3c4253 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -466,7 +466,6 @@ class NotificationService users = users.to_a.compact.uniq users = users.reject(&:blocked?) - users = users.reject(&:ghost?) users.reject do |user| global_notification_setting = user.global_notification_setting diff --git a/db/migrate/20170206115204_add_column_ghost_to_users.rb b/db/migrate/20170206115204_add_column_ghost_to_users.rb new file mode 100644 index 00000000000..57b66ca91a8 --- /dev/null +++ b/db/migrate/20170206115204_add_column_ghost_to_users.rb @@ -0,0 +1,15 @@ +class AddColumnGhostToUsers < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default :users, :ghost, :boolean, default: false, allow_null: false + end + + def down + remove_column :users, :ghost + end +end diff --git a/db/schema.rb b/db/schema.rb index 82bccda580a..c50d6fc4ad6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1282,6 +1282,7 @@ ActiveRecord::Schema.define(version: 20170216141440) do t.string "incoming_email_token" t.boolean "authorized_projects_populated" t.boolean "notified_of_own_activity", default: false, null: false + t.boolean "ghost", default: false, null: false end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 1732b1a0081..58602c54c7a 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -26,6 +26,10 @@ FactoryGirl.define do two_factor_via_otp end + trait :ghost do + ghost true + end + trait :two_factor_via_otp do before(:create) do |user| user.otp_required_for_login = true diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 861d338ef95..9e2b1c9290f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -208,6 +208,20 @@ describe User, models: true do end end end + + describe 'ghost users' do + it 'does not allow a non-blocked ghost user' do + user = build(:user, :ghost, state: :active) + + expect(user).to be_invalid + end + + it 'allows a blocked ghost user' do + user = build(:user, :ghost, state: :blocked) + + expect(user).to be_valid + end + end end describe "scopes" do -- cgit v1.2.3 From 8f01644ff4d25285475cbf053b140292ac50f225 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 7 Feb 2017 00:32:35 +0530 Subject: Implement review comments from @rymai and @yorickpeterse 1. Refactoring and specs in the `Uniquify` class. 2. Don't use the `AdvisoryLocking` class. Similar functionality is provided (backed by Redis) in the `ExclusiveLease` class. --- app/models/concerns/uniquify.rb | 18 +++++---- app/models/user.rb | 14 +++++-- lib/gitlab/database/advisory_locking.rb | 70 --------------------------------- spec/models/concerns/uniquify_spec.rb | 40 +++++++++++++++++++ 4 files changed, 61 insertions(+), 81 deletions(-) delete mode 100644 lib/gitlab/database/advisory_locking.rb create mode 100644 spec/models/concerns/uniquify_spec.rb diff --git a/app/models/concerns/uniquify.rb b/app/models/concerns/uniquify.rb index 1485ab6ae99..6734472bc6d 100644 --- a/app/models/concerns/uniquify.rb +++ b/app/models/concerns/uniquify.rb @@ -6,19 +6,23 @@ class Uniquify # If `base` is a function/proc, we expect that calling it with a # candidate counter returns a string to test/return. def string(base, exists_fn) + @base = base @counter = nil - if base.respond_to?(:call) - increment_counter! while exists_fn[base.call(@counter)] - base.call(@counter) - else - increment_counter! while exists_fn["#{base}#{@counter}"] - "#{base}#{@counter}" - end + increment_counter! while exists_fn[base_string] + base_string end private + def base_string + if @base.respond_to?(:call) + @base.call(@counter) + else + "#{@base}#{@counter}" + end + end + def increment_counter! @counter = @counter ? @counter.next : 1 end diff --git a/app/models/user.rb b/app/models/user.rb index f71d7e90d33..7207cdc4581 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -351,9 +351,15 @@ class User < ActiveRecord::Base ghost_user || begin # Since we only want a single ghost user in an instance, we use an - # advisory lock to ensure than this block is never run concurrently. - advisory_lock = Gitlab::Database::AdvisoryLocking.new(:ghost_user) - advisory_lock.lock + # exclusive lease to ensure than this block is never run concurrently. + lease_key = "ghost_user_creation" + lease = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.minute.to_i) + + until uuid = lease.try_obtain + # Keep trying until we obtain the lease. To prevent hammering Redis too + # much we'll wait for a bit between retries. + sleep(1) + end # Recheck if a ghost user is already present (one might have been) # added between the time we last checked (first line of this method) @@ -375,7 +381,7 @@ class User < ActiveRecord::Base email: email, name: "Ghost User", state: :blocked, ghost: true ) ensure - advisory_lock.unlock + Gitlab::ExclusiveLease.cancel(lease_key, uuid) end end end diff --git a/lib/gitlab/database/advisory_locking.rb b/lib/gitlab/database/advisory_locking.rb deleted file mode 100644 index 950898288be..00000000000 --- a/lib/gitlab/database/advisory_locking.rb +++ /dev/null @@ -1,70 +0,0 @@ -# An advisory lock is an application-level database lock which isn't tied -# to a specific table or row. -# -# Postgres names its advisory locks with integers, while MySQL uses strings. -# We support both here by using a `LOCK_TYPES` map of symbols to integers. -# The symbol (stringified) is used for MySQL, and the corresponding integer -# is used for Postgres. -module Gitlab - module Database - class AdvisoryLocking - LOCK_TYPES = { - ghost_user: 1 - } - - def initialize(lock_type) - @lock_type = lock_type - end - - def lock - ensure_valid_lock_type! - - query = - if Gitlab::Database.postgresql? - Arel::SelectManager.new(ActiveRecord::Base).project( - Arel::Nodes::NamedFunction.new("pg_advisory_lock", [LOCK_TYPES[@lock_type]]) - ) - elsif Gitlab::Database.mysql? - Arel::SelectManager.new(ActiveRecord::Base).project( - Arel::Nodes::NamedFunction.new("get_lock", [Arel.sql("'#{@lock_type}'"), -1]) - ) - end - - run_query(query) - end - - def unlock - ensure_valid_lock_type! - - query = - if Gitlab::Database.postgresql? - Arel::SelectManager.new(ActiveRecord::Base).project( - Arel::Nodes::NamedFunction.new("pg_advisory_unlock", [LOCK_TYPES[@lock_type]]) - ) - elsif Gitlab::Database.mysql? - Arel::SelectManager.new(ActiveRecord::Base).project( - Arel::Nodes::NamedFunction.new("release_lock", [Arel.sql("'#{@lock_type}'")]) - ) - end - - run_query(query) - end - - private - - def ensure_valid_lock_type! - unless valid_lock_type? - raise RuntimeError, "Trying to use an advisory lock with an invalid lock type, #{@lock_type}." - end - end - - def valid_lock_type? - LOCK_TYPES.keys.include?(@lock_type) - end - - def run_query(arel_query) - ActiveRecord::Base.connection.execute(arel_query.to_sql) - end - end - end -end diff --git a/spec/models/concerns/uniquify_spec.rb b/spec/models/concerns/uniquify_spec.rb new file mode 100644 index 00000000000..4e06a1f4c31 --- /dev/null +++ b/spec/models/concerns/uniquify_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Uniquify, models: true do + describe "#string" do + it 'returns the given string if it does not exist' do + uniquify = Uniquify.new + + result = uniquify.string('test_string', -> (s) { false }) + + expect(result).to eq('test_string') + end + + it 'returns the given string with a counter attached if the string exists' do + uniquify = Uniquify.new + + result = uniquify.string('test_string', -> (s) { true if s == 'test_string' }) + + expect(result).to eq('test_string1') + end + + it 'increments the counter for each candidate string that also exists' do + uniquify = Uniquify.new + + result = uniquify.string('test_string', -> (s) { true if s == 'test_string' || s == 'test_string1' }) + + expect(result).to eq('test_string2') + end + + it 'allows passing in a base function that defines the location of the counter' do + uniquify = Uniquify.new + + result = uniquify.string( + -> (counter) { "test_#{counter}_string" }, + -> (s) { true if s == 'test__string' } + ) + + expect(result).to eq('test_1_string') + end + end +end -- cgit v1.2.3 From 3bd2a98f64a12a2e23a6b9ce77427a9c2033a6bf Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 7 Feb 2017 12:13:24 +0530 Subject: Remove the default value for the `users.ghost` database column. The default (false) is not strictly required, and this lets us avoid a potentially expensive migration --- db/migrate/20170206115204_add_column_ghost_to_users.rb | 6 +----- db/schema.rb | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/db/migrate/20170206115204_add_column_ghost_to_users.rb b/db/migrate/20170206115204_add_column_ghost_to_users.rb index 57b66ca91a8..cc1eeda1160 100644 --- a/db/migrate/20170206115204_add_column_ghost_to_users.rb +++ b/db/migrate/20170206115204_add_column_ghost_to_users.rb @@ -1,12 +1,8 @@ class AddColumnGhostToUsers < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - DOWNTIME = false - disable_ddl_transaction! - def up - add_column_with_default :users, :ghost, :boolean, default: false, allow_null: false + add_column :users, :ghost, :boolean end def down diff --git a/db/schema.rb b/db/schema.rb index c50d6fc4ad6..1d94368f66e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1278,11 +1278,11 @@ ActiveRecord::Schema.define(version: 20170216141440) 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" t.boolean "notified_of_own_activity", default: false, null: false - t.boolean "ghost", default: false, null: false + t.boolean "ghost" end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree -- cgit v1.2.3 From f2ed82fa8486875660b80dd061827ac8b86d00b6 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Thu, 16 Feb 2017 12:35:10 +0530 Subject: Implement final review comments from @DouweM and @rymai - Have `Uniquify` take a block instead of a Proc/function. This is more idiomatic than passing around a function in Ruby. - Block a user before moving their issues to the ghost user. This avoids a data race where an issue is created after the issues are migrated to the ghost user, and before the destroy takes place. - No need to migrate issues (to the ghost user) in a transaction, because we're using `update_all` - Other minor changes --- app/models/concerns/uniquify.rb | 9 +++-- app/models/namespace.rb | 2 +- app/models/user.rb | 72 +++++++++++++++++------------------ app/services/users/destroy_service.rb | 13 +++++-- spec/models/concerns/uniquify_spec.rb | 23 ++++------- spec/services/users/destroy_spec.rb | 4 ++ 6 files changed, 62 insertions(+), 61 deletions(-) diff --git a/app/models/concerns/uniquify.rb b/app/models/concerns/uniquify.rb index 6734472bc6d..a7fe5951b6e 100644 --- a/app/models/concerns/uniquify.rb +++ b/app/models/concerns/uniquify.rb @@ -1,15 +1,15 @@ class Uniquify # Return a version of the given 'base' string that is unique # by appending a counter to it. Uniqueness is determined by - # repeated calls to `exists_fn`. + # repeated calls to the passed block. # # If `base` is a function/proc, we expect that calling it with a # candidate counter returns a string to test/return. - def string(base, exists_fn) + def string(base) @base = base @counter = nil - increment_counter! while exists_fn[base_string] + increment_counter! while yield(base_string) base_string end @@ -24,6 +24,7 @@ class Uniquify end def increment_counter! - @counter = @counter ? @counter.next : 1 + @counter ||= 0 + @counter += 1 end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 8cc3c1473e2..3137dd32f93 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -99,7 +99,7 @@ class Namespace < ActiveRecord::Base path = "blank" if path.blank? uniquify = Uniquify.new - uniquify.string(path, -> (s) { Namespace.find_by_path_or_name(s) }) + uniquify.string(path) { |s| Namespace.find_by_path_or_name(s) } end end diff --git a/app/models/user.rb b/app/models/user.rb index 7207cdc4581..4bb9dc0c59f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -346,43 +346,7 @@ class User < ActiveRecord::Base # Return (create if necessary) the ghost user. The ghost user # owns records previously belonging to deleted users. def ghost - ghost_user = User.find_by_ghost(true) - - ghost_user || - begin - # Since we only want a single ghost user in an instance, we use an - # exclusive lease to ensure than this block is never run concurrently. - lease_key = "ghost_user_creation" - lease = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.minute.to_i) - - until uuid = lease.try_obtain - # Keep trying until we obtain the lease. To prevent hammering Redis too - # much we'll wait for a bit between retries. - sleep(1) - end - - # Recheck if a ghost user is already present (one might have been) - # added between the time we last checked (first line of this method) - # and the time we acquired the lock. - ghost_user = User.find_by_ghost(true) - return ghost_user if ghost_user.present? - - uniquify = Uniquify.new - - username = uniquify.string("ghost", -> (s) { User.find_by_username(s) }) - - email = uniquify.string( - -> (n) { "ghost#{n}@example.com" }, - -> (s) { User.find_by_email(s) } - ) - - User.create( - username: username, password: Devise.friendly_token, - email: email, name: "Ghost User", state: :blocked, ghost: true - ) - ensure - Gitlab::ExclusiveLease.cancel(lease_key, uuid) - end + User.find_by_ghost(true) || create_ghost_user end end @@ -1052,4 +1016,38 @@ class User < ActiveRecord::Base super end end + + def self.create_ghost_user + # Since we only want a single ghost user in an instance, we use an + # exclusive lease to ensure than this block is never run concurrently. + lease_key = "ghost_user_creation" + lease = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.minute.to_i) + + until uuid = lease.try_obtain + # Keep trying until we obtain the lease. To prevent hammering Redis too + # much we'll wait for a bit between retries. + sleep(1) + end + + # Recheck if a ghost user is already present. One might have been + # added between the time we last checked (first line of this method) + # and the time we acquired the lock. + ghost_user = User.find_by_ghost(true) + return ghost_user if ghost_user.present? + + uniquify = Uniquify.new + + username = uniquify.string("ghost") { |s| User.find_by_username(s) } + + email = uniquify.string(-> (n) { "ghost#{n}@example.com" }) do |s| + User.find_by_email(s) + end + + User.create( + username: username, password: Devise.friendly_token, + email: email, name: "Ghost User", state: :blocked, ghost: true + ) + ensure + Gitlab::ExclusiveLease.cancel(lease_key, uuid) + end end diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb index bf1e49734cc..523279944ae 100644 --- a/app/services/users/destroy_service.rb +++ b/app/services/users/destroy_service.rb @@ -37,13 +37,18 @@ module Users end private - + def move_issues_to_ghost_user(user) + # Block the user before moving issues to prevent a data race. + # If the user creates an issue after `move_issues_to_ghost_user` + # runs and before the user is destroyed, the destroy will fail with + # an exception. We block the user so that issues can't be created + # after `move_issues_to_ghost_user` runs and before the destroy happens. + user.block + ghost_user = User.ghost - Issue.transaction do - user.issues.update_all(author_id: ghost_user.id) - end + user.issues.update_all(author_id: ghost_user.id) user.reload end diff --git a/spec/models/concerns/uniquify_spec.rb b/spec/models/concerns/uniquify_spec.rb index 4e06a1f4c31..83187d732e4 100644 --- a/spec/models/concerns/uniquify_spec.rb +++ b/spec/models/concerns/uniquify_spec.rb @@ -1,38 +1,31 @@ require 'spec_helper' describe Uniquify, models: true do + let(:uniquify) { described_class.new } + describe "#string" do it 'returns the given string if it does not exist' do - uniquify = Uniquify.new - - result = uniquify.string('test_string', -> (s) { false }) + result = uniquify.string('test_string') { |s| false } expect(result).to eq('test_string') end it 'returns the given string with a counter attached if the string exists' do - uniquify = Uniquify.new - - result = uniquify.string('test_string', -> (s) { true if s == 'test_string' }) + result = uniquify.string('test_string') { |s| s == 'test_string' } expect(result).to eq('test_string1') end it 'increments the counter for each candidate string that also exists' do - uniquify = Uniquify.new - - result = uniquify.string('test_string', -> (s) { true if s == 'test_string' || s == 'test_string1' }) + result = uniquify.string('test_string') { |s| s == 'test_string' || s == 'test_string1' } expect(result).to eq('test_string2') end it 'allows passing in a base function that defines the location of the counter' do - uniquify = Uniquify.new - - result = uniquify.string( - -> (counter) { "test_#{counter}_string" }, - -> (s) { true if s == 'test__string' } - ) + result = uniquify.string(-> (counter) { "test_#{counter}_string" }) do |s| + s == 'test__string' + end expect(result).to eq('test_1_string') end diff --git a/spec/services/users/destroy_spec.rb b/spec/services/users/destroy_spec.rb index 4f0834064e5..922e82445d0 100644 --- a/spec/services/users/destroy_spec.rb +++ b/spec/services/users/destroy_spec.rb @@ -47,6 +47,10 @@ describe Users::DestroyService, services: true do expect(migrated_issue.author).to eq(User.ghost) end + + it 'blocks the user before migrating issues to the "Ghost User' do + expect(user).to be_blocked + end end context "for an issue the user was assigned to" do -- cgit v1.2.3 From 6fdb17cbbe5dc70d18f50e9d131ab70407976a71 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 17 Feb 2017 20:28:12 +0530 Subject: Don't allow deleting a ghost user. - Add a `destroy_user` ability. This didn't exist before, and was implicit in other abilities (only admins could access the admin area, so only they could destroy all users; a user can only access their own account page, and so can destroy only themselves). - Grant this ability to admins, and when the current user is trying to destroy themselves. Disallow destroying ghost users in all cases. - Modify the `Users::DestroyService` to check this ability. Also check it in views to decide whether or not to show the "Delete User" button. - Add a short summary of the Ghost User to the bio. --- app/models/user.rb | 4 +++- app/policies/user_policy.rb | 8 +++++++ app/services/users/destroy_service.rb | 2 +- app/views/admin/users/_user.html.haml | 2 +- app/views/admin/users/show.html.haml | 5 +++- app/views/profiles/accounts/show.html.haml | 5 +++- spec/factories/users.rb | 1 + spec/policies/user_policy_spec.rb | 37 ++++++++++++++++++++++++++++++ 8 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 spec/policies/user_policy_spec.rb diff --git a/app/models/user.rb b/app/models/user.rb index 4bb9dc0c59f..13f6f2c3d77 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1043,8 +1043,10 @@ class User < ActiveRecord::Base User.find_by_email(s) end + bio = 'This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.' + User.create( - username: username, password: Devise.friendly_token, + username: username, password: Devise.friendly_token, bio: bio, email: email, name: "Ghost User", state: :blocked, ghost: true ) ensure diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index 03a2499e263..229846e368c 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -3,6 +3,14 @@ class UserPolicy < BasePolicy def rules can! :read_user if @user || !restricted_public_level? + + if @user + if @user.admin? || @subject == @user + can! :destroy_user + end + + cannot! :destroy_user if @subject.ghost? + end end def restricted_public_level? diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb index 523279944ae..833da5bc5d1 100644 --- a/app/services/users/destroy_service.rb +++ b/app/services/users/destroy_service.rb @@ -7,7 +7,7 @@ module Users end def execute(user, options = {}) - unless current_user.admin? || current_user == user + unless Ability.allowed?(current_user, :destroy_user, user) raise Gitlab::Access::AccessDeniedError, "#{current_user} tried to destroy user #{user}!" end diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml index 3b5c713ac2d..a756cb7243a 100644 --- a/app/views/admin/users/_user.html.haml +++ b/app/views/admin/users/_user.html.haml @@ -34,7 +34,7 @@ - if user.access_locked? %li = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' } - - if user.can_be_removed? + - if user.can_be_removed? && can?(current_user, :destroy_user, @user) %li.divider %li = link_to 'Delete User', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Consider cancelling this deletion and blocking the user instead. Are you sure?" }, diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 76b1291fe10..840d843f069 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -173,7 +173,7 @@ .panel-heading Remove user .panel-body - - if @user.can_be_removed? + - if @user.can_be_removed? && can?(current_user, :destroy_user, @user) %p Deleting a user has the following effects: %ul %li All user content like authored issues, snippets, comments will be removed @@ -189,3 +189,6 @@ %strong= @user.solo_owned_groups.map(&:name).join(', ') %p You must transfer ownership or delete these groups before you can delete this user. + - else + %p + You don't have access to delete this user. diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index a4f4079d556..02fb47ec981 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -115,7 +115,7 @@ %h4.prepend-top-0.danger-title Remove account .col-lg-9 - - if @user.can_be_removed? + - if @user.can_be_removed? && can?(current_user, :destroy_user, @user) %p Deleting an account has the following effects: %ul @@ -131,4 +131,7 @@ %strong= @user.solo_owned_groups.map(&:name).join(', ') %p You must transfer ownership or delete these groups before you can delete your account. + - else + %p + You don't have access to delete this user. .append-bottom-default diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 58602c54c7a..249dabbaae8 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -28,6 +28,7 @@ FactoryGirl.define do trait :ghost do ghost true + after(:build) { |user, _| user.block! } end trait :two_factor_via_otp do diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb new file mode 100644 index 00000000000..d5761390d39 --- /dev/null +++ b/spec/policies/user_policy_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe UserPolicy, models: true do + let(:current_user) { create(:user) } + let(:user) { create(:user) } + + subject { described_class.abilities(current_user, user).to_set } + + describe "reading a user's information" do + it { is_expected.to include(:read_user) } + end + + describe "destroying a user" do + context "when a regular user tries to destroy another regular user" do + it { is_expected.not_to include(:destroy_user) } + end + + context "when a regular user tries to destroy themselves" do + let(:current_user) { user } + + it { is_expected.to include(:destroy_user) } + end + + context "when an admin user tries to destroy a regular user" do + let(:current_user) { create(:user, :admin) } + + it { is_expected.to include(:destroy_user) } + end + + context "when an admin user tries to destroy a ghost user" do + let(:current_user) { create(:user, :admin) } + let(:user) { create(:user, :ghost) } + + it { is_expected.not_to include(:destroy_user) } + end + end +end -- cgit v1.2.3 From 53aa043724a0395a2f95f41b8f3fb4c308691874 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 21 Feb 2017 12:50:05 +0530 Subject: Fix specs for the ghost user feature. --- spec/models/abuse_report_spec.rb | 2 +- spec/models/user_spec.rb | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb index c4486a32082..4e71597521d 100644 --- a/spec/models/abuse_report_spec.rb +++ b/spec/models/abuse_report_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' RSpec.describe AbuseReport, type: :model do subject { create(:abuse_report) } - let(:user) { create(:user) } + let(:user) { create(:admin) } it { expect(subject).to be_valid } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 9e2b1c9290f..6356f8b6c92 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -211,13 +211,15 @@ describe User, models: true do describe 'ghost users' do it 'does not allow a non-blocked ghost user' do - user = build(:user, :ghost, state: :active) + user = build(:user, :ghost) + user.state = 'active' expect(user).to be_invalid end it 'allows a blocked ghost user' do - user = build(:user, :ghost, state: :blocked) + user = build(:user, :ghost) + user.state = 'blocked' expect(user).to be_valid end -- cgit v1.2.3 From 5852e0e0605e90949aec817293f45fabf5b116ac Mon Sep 17 00:00:00 2001 From: Maxime Besson Date: Fri, 24 Feb 2017 12:21:41 +0100 Subject: Suggest a more secure way of handling SSH host keys in docker builds --- doc/ci/ssh_keys/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md index 49e7ac38b26..688a69d77ba 100644 --- a/doc/ci/ssh_keys/README.md +++ b/doc/ci/ssh_keys/README.md @@ -38,6 +38,15 @@ following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY` and in the **Value** field paste the content of your _private_ key that you created earlier. +It is also good practice to check the server's own public key to make sure you +are not being targeted by a man-in-the-middle attack. To do this, add another +variable named `SSH_SERVER_HOSTKEYS`. To find out the hostkeys of your server, run +the `ssh-keyscan YOUR_SERVER` command from a trusted network (ideally, from the +server itself), and paste its output into the `SSH_SERVER_HOSTKEY` variable. If +you need to connect to multiple servers, concatenate all the server public keys +that you collected into the **Value** of the variable. There must be one key per +line. + Next you need to modify your `.gitlab-ci.yml` with a `before_script` action. Add it to the top: @@ -59,6 +68,11 @@ before_script: # you will overwrite your user's SSH config. - mkdir -p ~/.ssh - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config' + # In order to properly check the server's host key, assuming you created the + # SSH_SERVER_HOSTKEYS variable previously, uncomment the following two lines + # instead. + # - mkdir -p ~/.ssh + # - '[[ -f /.dockerenv ]] && echo "$SSH_SERVER_HOSTKEYS" > ~/.ssh/known_hosts' ``` As a final step, add the _public_ key from the one you created earlier to the -- cgit v1.2.3 From 0be25bffd8c6fdab956bca269ba5a40c3ced682d Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 24 Feb 2017 12:46:03 +0100 Subject: Fix broken links in CI admin area docs [ci skip] --- doc/user/admin_area/settings/continuous_integration.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md index 64df7ee48bd..eb6f915f3f4 100644 --- a/doc/user/admin_area/settings/continuous_integration.md +++ b/doc/user/admin_area/settings/continuous_integration.md @@ -16,8 +16,6 @@ that this setting is set for each job. 1. Hit **Save** for the changes to take effect. -[art-yml]: ../../../administration/build_artifacts - ## Default artifacts expiration The default expiration time of the [job artifacts][art-yml] can be set in @@ -36,5 +34,5 @@ expiration. 1. Hit **Save** for the changes to take effect. -[art-yml]: ../../../administration/job_artifacts -[duration-syntax]: ../../../ci/yaml/README#artifactsexpire_in +[art-yml]: ../../../administration/job_artifacts.md +[duration-syntax]: ../../../ci/yaml/README.md#artifactsexpire_in -- cgit v1.2.3 From 9f0e23d6093d67d368485e2ac7e4df0059935770 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Fri, 24 Feb 2017 13:25:42 +0100 Subject: Fix tests --- app/assets/javascripts/behaviors/toggler_behavior.js | 2 +- app/services/groups/base_service.rb | 6 ++++++ app/services/groups/create_service.rb | 6 ------ app/services/groups/update_service.rb | 17 +++++++++++++---- app/views/groups/_create_chat_team.html.haml | 2 +- db/migrate/20170120131253_create_chat_teams.rb | 5 +++-- spec/services/groups/create_service_spec.rb | 6 +++++- 7 files changed, 29 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index 2d98fc5a545..0726c6c9636 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -21,7 +21,7 @@ // %a.js-toggle-button // %div.js-toggle-content // - $('body').on('click', '.js-toggle-button', function(e) { + $('body').on('click', '.js-toggle-button', function() { toggleContainer($(this).closest('.js-toggle-container')); }); diff --git a/app/services/groups/base_service.rb b/app/services/groups/base_service.rb index a8fa098246a..eb2abcd3723 100644 --- a/app/services/groups/base_service.rb +++ b/app/services/groups/base_service.rb @@ -5,5 +5,11 @@ module Groups def initialize(group, user, params = {}) @group, @current_user, @params = group, user, params.dup end + + private + + def create_chat_team? + @chat_team == "true" && Gitlab.config.mattermost.enabled + end end end diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index 3028025fc6e..4ed2cb7c8af 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -32,11 +32,5 @@ module Groups @group.add_owner(current_user) @group end - - private - - def create_chat_team? - @chat_team && Gitlab.config.mattermost.enabled - end end end diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb index aff42ad598c..e8ad6d41ed4 100644 --- a/app/services/groups/update_service.rb +++ b/app/services/groups/update_service.rb @@ -1,10 +1,7 @@ module Groups class UpdateService < Groups::BaseService def execute - if params.delete(:create_chat_team) == '1' - chat_name = params[:chat_team_name] - options = chat_name ? { name: chat_name } : {} - end + @chat_team = params.delete(:create_chat_team) # check that user is allowed to set specified visibility_level new_visibility = params[:visibility_level] @@ -19,6 +16,12 @@ module Groups group.assign_attributes(params) + if create_chat_team? + Mattermost::CreateTeamService.new(group, current_user).execute + + return group if group.errors.any? + end + begin group.save rescue Gitlab::UpdatePathError => e @@ -27,5 +30,11 @@ module Groups false end end + + private + + def create_chat_team? + super && group.chat_team.nil? + end end end diff --git a/app/views/groups/_create_chat_team.html.haml b/app/views/groups/_create_chat_team.html.haml index 8b908e233dc..b9bf9fe39c2 100644 --- a/app/views/groups/_create_chat_team.html.haml +++ b/app/views/groups/_create_chat_team.html.haml @@ -13,4 +13,4 @@ Team URL: = Settings.mattermost.host %span> / - %span{ "data-bind-out" => "create_chat_team"} + %span{ "data-bind-out" => "create_chat_team" } diff --git a/db/migrate/20170120131253_create_chat_teams.rb b/db/migrate/20170120131253_create_chat_teams.rb index 6476c239152..2d9341d235f 100644 --- a/db/migrate/20170120131253_create_chat_teams.rb +++ b/db/migrate/20170120131253_create_chat_teams.rb @@ -1,7 +1,8 @@ class CreateChatTeams < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers - DOWNTIME = false + DOWNTIME = true + DOWNTIME_REASON = "Adding a foreign key" def change create_table :chat_teams do |t| @@ -12,6 +13,6 @@ class CreateChatTeams < ActiveRecord::Migration t.timestamps null: false end - add_foreign_key :chat_teams, :namespaces, on_delete: :cascade + add_concurrent_foreign_key :chat_teams, :namespaces, on_delete: :cascade end end diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index d8181f73110..b11483fb407 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -40,9 +40,13 @@ describe Groups::CreateService, '#execute', services: true do end describe 'creating a mattermost team' do - let!(:params) { group_params.merge(create_chat_team: true) } + let!(:params) { group_params.merge(create_chat_team: "true") } let!(:service) { described_class.new(user, params) } + before do + Settings.mattermost['enabled'] = true + end + it 'triggers the service' do expect_any_instance_of(Mattermost::CreateTeamService).to receive(:execute) -- cgit v1.2.3 From dfbccdfc1b119ff801f810607b724e4572e37bb1 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 24 Feb 2017 08:13:57 -0600 Subject: Fix spec --- spec/lib/gitlab/import_export/import_export_spec.rb | 2 +- spec/support/markdown_feature.rb | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/spec/lib/gitlab/import_export/import_export_spec.rb b/spec/lib/gitlab/import_export/import_export_spec.rb index 20743811dab..f3fd0d82875 100644 --- a/spec/lib/gitlab/import_export/import_export_spec.rb +++ b/spec/lib/gitlab/import_export/import_export_spec.rb @@ -10,7 +10,7 @@ describe Gitlab::ImportExport, services: true do end it 'contains the namespace path' do - expect(described_class.export_filename(project: project)).to include(project.namespace.full_path) + expect(described_class.export_filename(project: project)).to include(project.namespace.full_path.tr('/', '_')) end it 'does not go over a certain length' do diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb index 5980d14c8ae..dea0015f105 100644 --- a/spec/support/markdown_feature.rb +++ b/spec/support/markdown_feature.rb @@ -79,9 +79,8 @@ class MarkdownFeature def xproject @xproject ||= begin - group = create(:group, name: 'cross-reference') - group2 = create(:group, parent: group, name: 'nested-group') - create(:project, namespace: group2) do |project| + group = create(:group, :nested) + create(:project, namespace: group) do |project| project.team << [user, :developer] end end -- cgit v1.2.3 From 150c3637521653a9e1200483376e4661ea2a46b3 Mon Sep 17 00:00:00 2001 From: Jan Christophersen Date: Fri, 17 Feb 2017 18:18:06 +0100 Subject: Add the username of the current user to the HTTP(S) clone URL --- app/helpers/button_helper.rb | 2 +- app/helpers/projects_helper.rb | 2 +- app/models/project.rb | 10 ++++++++-- .../unreleased/1937-https-clone-url-username.yml | 4 ++++ features/steps/explore/projects.rb | 2 +- .../admin_disables_git_access_protocol_spec.rb | 2 +- ...veloper_views_empty_project_instructions_spec.rb | 12 +++++++++--- spec/models/project_spec.rb | 21 +++++++++++++++++++++ 8 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 changelogs/unreleased/1937-https-clone-url-username.yml diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index 4c7c16d694c..195094730aa 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -34,7 +34,7 @@ module ButtonHelper content_tag (append_link ? :a : :span), protocol, class: klass, - href: (project.http_url_to_repo if append_link), + href: (project.http_url_to_repo(current_user) if append_link), data: { html: true, placement: placement, diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 735a355c25a..4befeacc135 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -241,7 +241,7 @@ module ProjectsHelper when 'ssh' project.ssh_url_to_repo else - project.http_url_to_repo + project.http_url_to_repo(current_user) end end diff --git a/app/models/project.rb b/app/models/project.rb index 814fd0c0f4f..6175f8dce7f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -873,8 +873,14 @@ class Project < ActiveRecord::Base url_to_repo end - def http_url_to_repo - "#{web_url}.git" + def http_url_to_repo(user = nil) + url = web_url + + if user + url.sub!(%r{\Ahttps?://}) { |protocol| "#{protocol}#{user.username}@" } + end + + "#{url}.git" end # Check if current branch name is marked as protected in the system diff --git a/changelogs/unreleased/1937-https-clone-url-username.yml b/changelogs/unreleased/1937-https-clone-url-username.yml new file mode 100644 index 00000000000..fa89d94e0f3 --- /dev/null +++ b/changelogs/unreleased/1937-https-clone-url-username.yml @@ -0,0 +1,4 @@ +--- +title: Add the Username to the HTTP(S) clone URL of a Repository +merge_request: 9347 +author: Jan Christophersen diff --git a/features/steps/explore/projects.rb b/features/steps/explore/projects.rb index 2b4a5ab0864..7dc33ab5683 100644 --- a/features/steps/explore/projects.rb +++ b/features/steps/explore/projects.rb @@ -49,7 +49,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps step 'I should see an http link to the repository' do project = Project.find_by(name: 'Community') - expect(page).to have_field('project_clone', with: project.http_url_to_repo) + expect(page).to have_field('project_clone', with: project.http_url_to_repo(@user)) end step 'I should see an ssh link to the repository' do diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb index e8e080ce3e2..273cacd82cd 100644 --- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb +++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb @@ -32,7 +32,7 @@ feature 'Admin disables Git access protocol', feature: true do scenario 'shows only HTTP url' do visit_project - expect(page).to have_content("git clone #{project.http_url_to_repo}") + expect(page).to have_content("git clone #{project.http_url_to_repo(admin)}") expect(page).not_to have_selector('#clone-dropdown') end end diff --git a/spec/features/projects/developer_views_empty_project_instructions_spec.rb b/spec/features/projects/developer_views_empty_project_instructions_spec.rb index 0c51fe72ca4..2352329d58c 100644 --- a/spec/features/projects/developer_views_empty_project_instructions_spec.rb +++ b/spec/features/projects/developer_views_empty_project_instructions_spec.rb @@ -56,8 +56,14 @@ feature 'Developer views empty project instructions', feature: true do end def expect_instructions_for(protocol) - msg = :"#{protocol.downcase}_url_to_repo" - - expect(page).to have_content("git clone #{project.send(msg)}") + url = + case protocol + when 'ssh' + project.ssh_url_to_repo + when 'http' + project.http_url_to_repo(developer) + end + + expect(page).to have_content("git clone #{url}") end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index b0087a9e15d..80146bb44e0 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1896,4 +1896,25 @@ describe Project, models: true do end end end + + describe '#http_url_to_repo' do + let(:project) { create :empty_project } + + context 'when no user is given' do + it 'returns the url to the repo without a username' do + url = project.http_url_to_repo + + expect(url).to eq(project.http_url_to_repo) + expect(url).not_to include('@') + end + end + + context 'when user is given' do + it 'returns the url to the repo with the username' do + user = build_stubbed(:user) + + expect(project.http_url_to_repo(user)).to match(%r{https?:\/\/#{user.username}@}) + end + end + end end -- cgit v1.2.3 From 0625af3bcb75b3186a3286eee662a7ff8442f130 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 13 Feb 2017 15:09:36 -0600 Subject: Consistently create, update, and delete files, taking CRLF settings into account --- app/models/repository.rb | 128 ++++------------ app/services/files/multi_service.rb | 14 +- db/fixtures/development/17_cycle_analytics.rb | 10 +- lib/api/commits.rb | 7 - lib/gitlab/git/blob.rb | 157 ------------------- lib/gitlab/git/index.rb | 124 +++++++++++++++ lib/gitlab/git/repository.rb | 51 ------- spec/lib/gitlab/git/blob_spec.rb | 185 ----------------------- spec/lib/gitlab/git/index_spec.rb | 210 ++++++++++++++++++++++++++ spec/lib/gitlab/git/repository_spec.rb | 75 --------- spec/models/repository_spec.rb | 2 +- spec/support/cycle_analytics_helpers.rb | 9 +- 12 files changed, 376 insertions(+), 596 deletions(-) create mode 100644 lib/gitlab/git/index.rb create mode 100644 spec/lib/gitlab/git/index_spec.rb diff --git a/app/models/repository.rb b/app/models/repository.rb index cd2568ad445..b747b71ae68 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -752,24 +752,28 @@ class Repository message:, branch_name:, author_email: nil, author_name: nil, start_branch_name: nil, start_project: project) - check_tree_entry_for_dir(branch_name, path) - if start_branch_name - start_project.repository. - check_tree_entry_for_dir(start_branch_name, path) + entry = tree_entry_at(start_branch_name || branch_name, path) + if entry + if entry[:type] == :blob + raise Gitlab::Git::Repository::InvalidBlobName.new( + "Directory already exists as a file") + else + raise Gitlab::Git::Repository::InvalidBlobName.new( + "Directory already exists") + end end - commit_file( - user, - "#{path}/.gitkeep", - '', + multi_action( + user: user, message: message, branch_name: branch_name, - update: false, author_email: author_email, author_name: author_name, start_branch_name: start_branch_name, - start_project: start_project) + start_project: start_project, + actions: [{ action: :create_dir, + file_path: path }]) end # rubocop:enable Metrics/ParameterLists @@ -779,18 +783,8 @@ class Repository message:, branch_name:, update: true, author_email: nil, author_name: nil, start_branch_name: nil, start_project: project) - unless update - error_message = "Filename already exists; update not allowed" - if tree_entry_at(branch_name, path) - raise Gitlab::Git::Repository::InvalidBlobName.new(error_message) - end - - if start_branch_name && - start_project.repository.tree_entry_at(start_branch_name, path) - raise Gitlab::Git::Repository::InvalidBlobName.new(error_message) - end - end + action = update ? :update : :create multi_action( user: user, @@ -800,7 +794,7 @@ class Repository author_name: author_name, start_branch_name: start_branch_name, start_project: start_project, - actions: [{ action: :create, + actions: [{ action: action, file_path: path, content: content }]) end @@ -839,6 +833,7 @@ class Repository message:, branch_name:, author_email: nil, author_name: nil, start_branch_name: nil, start_project: project) + multi_action( user: user, message: message, @@ -861,21 +856,22 @@ class Repository branch_name, start_branch_name: start_branch_name, start_project: start_project) do |start_commit| - index = rugged.index - parents = if start_commit - index.read_tree(start_commit.raw_commit.tree) - [start_commit.sha] - else - [] - end + index = Gitlab::Git::Index.new(raw_repository) - actions.each do |act| - git_action(index, act) + if start_commit + index.read_tree(start_commit.raw_commit.tree) + parents = [start_commit.sha] + else + parents = [] + end + + actions.each do |options| + index.__send__(options.delete(:action), options) end options = { - tree: index.write_tree(rugged), + tree: index.write_tree, message: message, parents: parents } @@ -1174,22 +1170,6 @@ class Repository raw_repository.send(:tree_entry, commit(branch_name), path) end - def check_tree_entry_for_dir(branch_name, path) - return unless branch_exists?(branch_name) - - entry = tree_entry_at(branch_name, path) - - return unless entry - - if entry[:type] == :blob - raise Gitlab::Git::Repository::InvalidBlobName.new( - "Directory already exists as a file") - else - raise Gitlab::Git::Repository::InvalidBlobName.new( - "Directory already exists") - end - end - private def blob_data_at(sha, path) @@ -1200,58 +1180,6 @@ class Repository blob.data end - def git_action(index, action) - path = normalize_path(action[:file_path]) - - if action[:action] == :move - previous_path = normalize_path(action[:previous_path]) - end - - case action[:action] - when :create, :update, :move - mode = - case action[:action] - when :update - index.get(path)[:mode] - when :move - index.get(previous_path)[:mode] - end - mode ||= 0o100644 - - index.remove(previous_path) if action[:action] == :move - - content = if action[:encoding] == 'base64' - Base64.decode64(action[:content]) - else - action[:content] - end - - detect = CharlockHolmes::EncodingDetector.new.detect(content) if content - - unless detect && detect[:type] == :binary - # When writing to the repo directly as we are doing here, - # the `core.autocrlf` config isn't taken into account. - content.gsub!("\r\n", "\n") if self.autocrlf - end - - oid = rugged.write(content, :blob) - - index.add(path: path, oid: oid, mode: mode) - when :delete - index.remove(path) - end - end - - def normalize_path(path) - pathname = Gitlab::Git::PathHelper.normalize_path(path) - - if pathname.each_filename.include?('..') - raise Gitlab::Git::Repository::InvalidBlobName.new('Invalid path') - end - - pathname.to_s - end - def refs_directory_exists? return false unless path_with_namespace diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb index af6da5b9d56..809fa32eca5 100644 --- a/app/services/files/multi_service.rb +++ b/app/services/files/multi_service.rb @@ -2,6 +2,8 @@ module Files class MultiService < Files::BaseService class FileChangedError < StandardError; end + ACTIONS = %w[create update delete move].freeze + def commit repository.multi_action( user: current_user, @@ -19,15 +21,23 @@ module Files def validate super - params[:actions].each_with_index do |action, index| unless action[:file_path].present? raise_error("You must specify a file_path.") end + action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/') + action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/') + regex_check(action[:file_path]) regex_check(action[:previous_path]) if action[:previous_path] + if ACTIONS.include?(action[:action].to_s) + action[:action] = action[:action].to_sym + else + raise_error("Unknown action type `#{action[:action]}`.") + end + if project.empty_repo? && action[:action] != :create raise_error("No files to #{action[:action]}.") end @@ -43,8 +53,6 @@ module Files validate_delete(action) when :move validate_move(action, index) - else - raise_error("Unknown action type `#{action[:action]}`.") end end end diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb index 747901dd634..647ed75b6d1 100644 --- a/db/fixtures/development/17_cycle_analytics.rb +++ b/db/fixtures/development/17_cycle_analytics.rb @@ -155,17 +155,9 @@ class Gitlab::Seeder::CycleAnalytics issue.project.repository.add_branch(@user, branch_name, 'master') - options = { - committer: issue.project.repository.user_to_committer(@user), - author: issue.project.repository.user_to_committer(@user), - commit: { message: "Commit for ##{issue.iid}", branch: branch_name, update_ref: true }, - file: { content: "content", path: filename, update: false } - } - - commit_sha = Gitlab::Git::Blob.commit(issue.project.repository, options) + commit_sha = issue.project.repository.commit_file(@user, filename, "content", options, message: "Commit for ##{issue.iid}", branch_name: branch_name) issue.project.repository.commit(commit_sha) - GitPushService.new(issue.project, @user, oldrev: issue.project.repository.commit("master").sha, diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 9ce396d4660..fd03e92264d 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -52,13 +52,6 @@ module API attrs = declared_params.merge(start_branch: declared_params[:branch], target_branch: declared_params[:branch]) - attrs[:actions].map! do |action| - action[:action] = action[:action].to_sym - action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/') - action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/') - action - end - result = ::Files::MultiService.new(user_project, current_user, attrs).execute if result[:status] == :success diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index b742d9e1e4b..e56eb0d3beb 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -93,163 +93,6 @@ module Gitlab commit_id: sha, ) end - - # Commit file in repository and return commit sha - # - # options should contain next structure: - # file: { - # content: 'Lorem ipsum...', - # path: 'documents/story.txt', - # update: true - # }, - # author: { - # email: 'user@example.com', - # name: 'Test User', - # time: Time.now - # }, - # committer: { - # email: 'user@example.com', - # name: 'Test User', - # time: Time.now - # }, - # commit: { - # message: 'Wow such commit', - # branch: 'master', - # update_ref: false - # } - # - # rubocop:disable Metrics/AbcSize - # rubocop:disable Metrics/CyclomaticComplexity - # rubocop:disable Metrics/PerceivedComplexity - def commit(repository, options, action = :add) - file = options[:file] - update = file[:update].nil? ? true : file[:update] - author = options[:author] - committer = options[:committer] - commit = options[:commit] - repo = repository.rugged - ref = commit[:branch] - update_ref = commit[:update_ref].nil? ? true : commit[:update_ref] - parents = [] - mode = 0o100644 - - unless ref.start_with?('refs/') - ref = 'refs/heads/' + ref - end - - path_name = Gitlab::Git::PathHelper.normalize_path(file[:path]) - # Abort if any invalid characters remain (e.g. ../foo) - raise Gitlab::Git::Repository::InvalidBlobName.new("Invalid path") if path_name.each_filename.to_a.include?('..') - - filename = path_name.to_s - index = repo.index - - unless repo.empty? - rugged_ref = repo.references[ref] - raise Gitlab::Git::Repository::InvalidRef.new("Invalid branch name") unless rugged_ref - last_commit = rugged_ref.target - index.read_tree(last_commit.tree) - parents = [last_commit] - end - - if action == :remove - index.remove(filename) - else - file_entry = index.get(filename) - - if action == :rename - old_path_name = Gitlab::Git::PathHelper.normalize_path(file[:previous_path]) - old_filename = old_path_name.to_s - file_entry = index.get(old_filename) - index.remove(old_filename) unless file_entry.blank? - end - - if file_entry - raise Gitlab::Git::Repository::InvalidBlobName.new("Filename already exists; update not allowed") unless update - - # Preserve the current file mode if one is available - mode = file_entry[:mode] if file_entry[:mode] - end - - content = file[:content] - detect = CharlockHolmes::EncodingDetector.new.detect(content) if content - - unless detect && detect[:type] == :binary - # When writing to the repo directly as we are doing here, - # the `core.autocrlf` config isn't taken into account. - content.gsub!("\r\n", "\n") if repository.autocrlf - end - - oid = repo.write(content, :blob) - index.add(path: filename, oid: oid, mode: mode) - end - - opts = {} - opts[:tree] = index.write_tree(repo) - opts[:author] = author - opts[:committer] = committer - opts[:message] = commit[:message] - opts[:parents] = parents - opts[:update_ref] = ref if update_ref - - Rugged::Commit.create(repo, opts) - end - # rubocop:enable Metrics/AbcSize - # rubocop:enable Metrics/CyclomaticComplexity - # rubocop:enable Metrics/PerceivedComplexity - - # Remove file from repository and return commit sha - # - # options should contain next structure: - # file: { - # path: 'documents/story.txt' - # }, - # author: { - # email: 'user@example.com', - # name: 'Test User', - # time: Time.now - # }, - # committer: { - # email: 'user@example.com', - # name: 'Test User', - # time: Time.now - # }, - # commit: { - # message: 'Remove FILENAME', - # branch: 'master' - # } - # - def remove(repository, options) - commit(repository, options, :remove) - end - - # Rename file from repository and return commit sha - # - # options should contain next structure: - # file: { - # previous_path: 'documents/old_story.txt' - # path: 'documents/story.txt' - # content: 'Lorem ipsum...', - # update: true - # }, - # author: { - # email: 'user@example.com', - # name: 'Test User', - # time: Time.now - # }, - # committer: { - # email: 'user@example.com', - # name: 'Test User', - # time: Time.now - # }, - # commit: { - # message: 'Rename FILENAME', - # branch: 'master' - # } - # - def rename(repository, options) - commit(repository, options, :rename) - end end def initialize(options) diff --git a/lib/gitlab/git/index.rb b/lib/gitlab/git/index.rb new file mode 100644 index 00000000000..3d0000879f8 --- /dev/null +++ b/lib/gitlab/git/index.rb @@ -0,0 +1,124 @@ +module Gitlab + module Git + class Index + DEFAULT_MODE = 0o100644 + + attr_reader :repository, :raw_index + + def initialize(repository) + @repository = repository + @raw_index = repository.rugged.index + end + + def read_tree(tree) + raw_index.read_tree(tree) + end + + def write_tree + raw_index.write_tree(repository.rugged) + end + + def get(*args) + raw_index.get(*args) + end + + def create(options) + normalize_options!(options) + + file_entry = raw_index.get(options[:file_path]) + if file_entry + raise Gitlab::Git::Repository::InvalidBlobName.new("Filename already exists") + end + + add_blob(options) + end + + def create_dir(options) + normalize_options!(options) + + file_entry = raw_index.get(options[:file_path]) + if file_entry + raise Gitlab::Git::Repository::InvalidBlobName.new("Directory already exists as a file") + end + + options = options.dup + options[:file_path] += '/.gitkeep' + options[:content] = '' + + add_blob(options) + end + + def update(options) + normalize_options!(options) + + file_entry = raw_index.get(options[:file_path]) + unless file_entry + raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist") + end + + add_blob(options, mode: file_entry[:mode]) + end + + def move(options) + normalize_options!(options) + + file_entry = raw_index.get(options[:previous_path]) + unless file_entry + raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist") + end + + raw_index.remove(options[:previous_path]) + + add_blob(options, mode: file_entry[:mode]) + end + + def delete(options) + normalize_options!(options) + + file_entry = raw_index.get(options[:file_path]) + unless file_entry + raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist") + end + + raw_index.remove(options[:file_path]) + end + + private + + def normalize_options!(options) + options[:file_path] = normalize_path(options[:file_path]) if options[:file_path] + options[:previous_path] = normalize_path(options[:previous_path]) if options[:previous_path] + end + + def normalize_path(path) + pathname = Gitlab::Git::PathHelper.normalize_path(path) + + if pathname.each_filename.include?('..') + raise Gitlab::Git::Repository::InvalidBlobName.new('Invalid path') + end + + pathname.to_s + end + + def add_blob(options, mode: nil) + content = options[:content] + return unless content + + content = Base64.decode64(content) if options[:encoding] == 'base64' + + detect = CharlockHolmes::EncodingDetector.new.detect(content) + unless detect && detect[:type] == :binary + # When writing to the repo directly as we are doing here, + # the `core.autocrlf` config isn't taken into account. + content.gsub!("\r\n", "\n") if repository.autocrlf + end + + oid = repository.rugged.write(content, :blob) + + raw_index.add(path: options[:file_path], oid: oid, mode: mode || DEFAULT_MODE) + rescue Rugged::IndexError => e + raise Gitlab::Git::Repository::InvalidBlobName.new(e.message) + end + end + end +end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 4b6ad8037ce..b8354d7bf04 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -837,57 +837,6 @@ module Gitlab rugged.config['core.autocrlf'] = AUTOCRLF_VALUES.invert[value] end - # Create a new directory with a .gitkeep file. Creates - # all required nested directories (i.e. mkdir -p behavior) - # - # options should contain next structure: - # author: { - # email: 'user@example.com', - # name: 'Test User', - # time: Time.now - # }, - # committer: { - # email: 'user@example.com', - # name: 'Test User', - # time: Time.now - # }, - # commit: { - # message: 'Wow such commit', - # branch: 'master', - # update_ref: false - # } - def mkdir(path, options = {}) - # Check if this directory exists; if it does, then don't bother - # adding .gitkeep file. - ref = options[:commit][:branch] - path = Gitlab::Git::PathHelper.normalize_path(path).to_s - rugged_ref = rugged.ref(ref) - - raise InvalidRef.new("Invalid ref") if rugged_ref.nil? - - target_commit = rugged_ref.target - - raise InvalidRef.new("Invalid target commit") if target_commit.nil? - - entry = tree_entry(target_commit, path) - - if entry - if entry[:type] == :blob - raise InvalidBlobName.new("Directory already exists as a file") - else - raise InvalidBlobName.new("Directory already exists") - end - end - - options[:file] = { - content: '', - path: "#{path}/.gitkeep", - update: true - } - - Gitlab::Git::Blob.commit(self, options) - end - # Returns result like "git ls-files" , recursive and full file path # # Ex. diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index 0c321f0343c..8049e2c120d 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -222,191 +222,6 @@ describe Gitlab::Git::Blob, seed_helper: true do end end - describe :commit do - let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } - - let(:commit_options) do - { - file: { - content: 'Lorem ipsum...', - path: 'documents/story.txt' - }, - author: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - committer: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - commit: { - message: 'Wow such commit', - branch: 'fix-mode' - } - } - end - - let(:commit_sha) { Gitlab::Git::Blob.commit(repository, commit_options) } - let(:commit) { repository.lookup(commit_sha) } - - it 'should add file with commit' do - # Commit message valid - expect(commit.message).to eq('Wow such commit') - - tree = commit.tree.to_a.find { |tree| tree[:name] == 'documents' } - - # Directory was created - expect(tree[:type]).to eq(:tree) - - # File was created - expect(repository.lookup(tree[:oid]).first[:name]).to eq('story.txt') - end - - describe "ref updating" do - it 'creates a commit but does not udate a ref' do - commit_opts = commit_options.tap{ |opts| opts[:commit][:update_ref] = false} - commit_sha = Gitlab::Git::Blob.commit(repository, commit_opts) - commit = repository.lookup(commit_sha) - - # Commit message valid - expect(commit.message).to eq('Wow such commit') - - # Does not update any related ref - expect(repository.lookup("fix-mode").oid).not_to eq(commit.oid) - expect(repository.lookup("HEAD").oid).not_to eq(commit.oid) - end - end - - describe 'reject updates' do - it 'should reject updates' do - commit_options[:file][:update] = false - commit_options[:file][:path] = 'files/executables/ls' - - expect{ commit_sha }.to raise_error('Filename already exists; update not allowed') - end - end - - describe 'file modes' do - it 'should preserve file modes with commit' do - commit_options[:file][:path] = 'files/executables/ls' - - entry = Gitlab::Git::Blob.find_entry_by_path(repository, commit.tree.oid, commit_options[:file][:path]) - expect(entry[:filemode]).to eq(0100755) - end - end - end - - describe :rename do - let(:repository) { Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH) } - let(:rename_options) do - { - file: { - path: 'NEWCONTRIBUTING.md', - previous_path: 'CONTRIBUTING.md', - content: 'Lorem ipsum...', - update: true - }, - author: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - committer: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - commit: { - message: 'Rename readme', - branch: 'master' - } - } - end - - let(:rename_options2) do - { - file: { - content: 'Lorem ipsum...', - path: 'bin/new_executable', - previous_path: 'bin/executable', - }, - author: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - committer: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - commit: { - message: 'Updates toberenamed.txt', - branch: 'master', - update_ref: false - } - } - end - - it 'maintains file permissions when renaming' do - mode = 0o100755 - - Gitlab::Git::Blob.rename(repository, rename_options2) - - expect(repository.rugged.index.get(rename_options2[:file][:path])[:mode]).to eq(mode) - end - - it 'renames the file with commit and not change file permissions' do - ref = rename_options[:commit][:branch] - - expect(repository.rugged.index.get('CONTRIBUTING.md')).not_to be_nil - expect { Gitlab::Git::Blob.rename(repository, rename_options) }.to change { repository.commit_count(ref) }.by(1) - - expect(repository.rugged.index.get('CONTRIBUTING.md')).to be_nil - expect(repository.rugged.index.get('NEWCONTRIBUTING.md')).not_to be_nil - end - end - - describe :remove do - let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } - - let(:commit_options) do - { - file: { - path: 'README.md' - }, - author: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - committer: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - commit: { - message: 'Remove readme', - branch: 'feature' - } - } - end - - let(:commit_sha) { Gitlab::Git::Blob.remove(repository, commit_options) } - let(:commit) { repository.lookup(commit_sha) } - let(:blob) { Gitlab::Git::Blob.find(repository, commit_sha, "README.md") } - - it 'should remove file with commit' do - # Commit message valid - expect(commit.message).to eq('Remove readme') - - # File was removed - expect(blob).to be_nil - end - end - describe :lfs_pointers do context 'file a valid lfs pointer' do let(:blob) do diff --git a/spec/lib/gitlab/git/index_spec.rb b/spec/lib/gitlab/git/index_spec.rb new file mode 100644 index 00000000000..91ffbd138ce --- /dev/null +++ b/spec/lib/gitlab/git/index_spec.rb @@ -0,0 +1,210 @@ +require 'spec_helper' + +describe Gitlab::Git::Index, seed_helper: true do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + let(:index) { described_class.new(repository) } + + before do + index.read_tree(repository.lookup('master').tree) + end + + describe '#create' do + let(:options) do + { + content: 'Lorem ipsum...', + file_path: 'documents/story.txt' + } + end + + context 'when no file at that path exists' do + it 'creates the file in the index' do + index.create(options) + + entry = index.get(options[:file_path]) + + expect(entry).not_to be_nil + expect(repository.lookup(entry[:oid]).content).to eq(options[:content]) + end + end + + context 'when a file at that path exists' do + before do + options[:file_path] = 'files/executables/ls' + end + + it 'raises an error' do + expect { index.create(options) }.to raise_error('Filename already exists') + end + end + + context 'when content is in base64' do + before do + options[:content] = Base64.encode64(options[:content]) + options[:encoding] = 'base64' + end + + it 'decodes base64' do + index.create(options) + + entry = index.get(options[:file_path]) + expect(repository.lookup(entry[:oid]).content).to eq(Base64.decode64(options[:content])) + end + end + + context 'when content contains CRLF' do + before do + repository.autocrlf = :input + options[:content] = "Hello,\r\nWorld" + end + + it 'converts to LF' do + index.create(options) + + entry = index.get(options[:file_path]) + expect(repository.lookup(entry[:oid]).content).to eq("Hello,\nWorld") + end + end + end + + describe '#create_dir' do + let(:options) do + { + file_path: 'newdir' + } + end + + context 'when no file or dir at that path exists' do + it 'creates the dir in the index' do + index.create_dir(options) + + entry = index.get(options[:file_path] + '/.gitkeep') + + expect(entry).not_to be_nil + end + end + + context 'when a file at that path exists' do + before do + options[:file_path] = 'files/executables/ls' + end + + it 'raises an error' do + expect { index.create_dir(options) }.to raise_error('Directory already exists as a file') + end + end + end + + describe '#update' do + let(:options) do + { + content: 'Lorem ipsum...', + file_path: 'README.md' + } + end + + context 'when no file at that path exists' do + before do + options[:file_path] = 'documents/story.txt' + end + + it 'raises an error' do + expect { index.update(options) }.to raise_error("File doesn't exist") + end + end + + context 'when a file at that path exists' do + it 'updates the file in the index' do + index.update(options) + + entry = index.get(options[:file_path]) + + expect(repository.lookup(entry[:oid]).content).to eq(options[:content]) + end + + it 'preserves file mode' do + options[:file_path] = 'files/executables/ls' + + index.update(options) + + entry = index.get(options[:file_path]) + + expect(entry[:mode]).to eq(0100755) + end + end + end + + describe '#move' do + let(:options) do + { + content: 'Lorem ipsum...', + previous_path: 'README.md', + file_path: 'NEWREADME.md' + } + end + + context 'when no file at that path exists' do + it 'raises an error' do + options[:previous_path] = 'documents/story.txt' + + expect { index.move(options) }.to raise_error("File doesn't exist") + end + end + + context 'when a file at that path exists' do + it 'removes the old file in the index' do + index.move(options) + + entry = index.get(options[:previous_path]) + + expect(entry).to be_nil + end + + it 'creates the new file in the index' do + index.move(options) + + entry = index.get(options[:file_path]) + + expect(entry).not_to be_nil + expect(repository.lookup(entry[:oid]).content).to eq(options[:content]) + end + + it 'preserves file mode' do + options[:previous_path] = 'files/executables/ls' + + index.move(options) + + entry = index.get(options[:file_path]) + + expect(entry[:mode]).to eq(0100755) + end + end + end + + describe '#delete' do + let(:options) do + { + file_path: 'README.md' + } + end + + context 'when no file at that path exists' do + before do + options[:file_path] = 'documents/story.txt' + end + + it 'raises an error' do + expect { index.delete(options) }.to raise_error("File doesn't exist") + end + end + + context 'when a file at that path exists' do + it 'removes the file in the index' do + index.delete(options) + + entry = index.get(options[:file_path]) + + expect(entry).to be_nil + end + end + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 2a915bf426f..766f16bb824 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -844,81 +844,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe '#mkdir' do - let(:commit_options) do - { - author: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - committer: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - commit: { - message: 'Test message', - branch: 'refs/heads/fix', - } - } - end - - def generate_diff_for_path(path) - "diff --git a/#{path}/.gitkeep b/#{path}/.gitkeep -new file mode 100644 -index 0000000..e69de29 ---- /dev/null -+++ b/#{path}/.gitkeep\n" - end - - shared_examples 'mkdir diff check' do |path, expected_path| - it 'creates a directory' do - result = repository.mkdir(path, commit_options) - expect(result).not_to eq(nil) - - # Verify another mkdir doesn't create a directory that already exists - expect{ repository.mkdir(path, commit_options) }.to raise_error('Directory already exists') - end - end - - describe 'creates a directory in root directory' do - it_should_behave_like 'mkdir diff check', 'new_dir', 'new_dir' - end - - describe 'creates a directory in subdirectory' do - it_should_behave_like 'mkdir diff check', 'files/ruby/test', 'files/ruby/test' - end - - describe 'creates a directory in subdirectory with a slash' do - it_should_behave_like 'mkdir diff check', '/files/ruby/test2', 'files/ruby/test2' - end - - describe 'creates a directory in subdirectory with multiple slashes' do - it_should_behave_like 'mkdir diff check', '//files/ruby/test3', 'files/ruby/test3' - end - - describe 'handles relative paths' do - it_should_behave_like 'mkdir diff check', 'files/ruby/../test_relative', 'files/test_relative' - end - - describe 'creates nested directories' do - it_should_behave_like 'mkdir diff check', 'files/missing/test', 'files/missing/test' - end - - it 'does not attempt to create a directory with invalid relative path' do - expect{ repository.mkdir('../files/missing/test', commit_options) }.to raise_error('Invalid path') - end - - it 'does not attempt to overwrite a file' do - expect{ repository.mkdir('README.md', commit_options) }.to raise_error('Directory already exists as a file') - end - - it 'does not attempt to overwrite a directory' do - expect{ repository.mkdir('files', commit_options) }.to raise_error('Directory already exists') - end - end - describe "#ls_files" do let(:master_file_paths) { repository.ls_files("master") } let(:not_existed_branch) { repository.ls_files("not_existed_branch") } diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 43aac31d8b7..070f1102e94 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -355,7 +355,7 @@ describe Repository, models: true do repository.commit_file(user, 'hello.txt', "Hello,\r\nWorld", message: 'Add hello world', branch_name: 'master', - update: true) + update: false) blob = repository.blob_at('master', 'hello.txt') diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb index 6ed55289ed9..26f3ed5300b 100644 --- a/spec/support/cycle_analytics_helpers.rb +++ b/spec/support/cycle_analytics_helpers.rb @@ -9,14 +9,7 @@ module CycleAnalyticsHelpers commit_shas = Array.new(count) do |index| filename = random_git_name - options = { - committer: project.repository.user_to_committer(user), - author: project.repository.user_to_committer(user), - commit: { message: message, branch: branch_name, update_ref: true }, - file: { content: "content", path: filename, update: false } - } - - commit_sha = Gitlab::Git::Blob.commit(project.repository, options) + commit_sha = project.repository.commit_file(user, filename, "content", message: message, branch_name: branch_name) project.repository.commit(commit_sha) commit_sha -- cgit v1.2.3 From faa2e2df8fb95655ebfb8523689295e223005d9d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 15 Feb 2017 17:28:29 -0600 Subject: Rename commit_file, commit_dir and remove_file and update specs --- app/models/repository.rb | 14 ++-- app/services/files/create_dir_service.rb | 2 +- app/services/files/create_service.rb | 3 +- app/services/files/destroy_service.rb | 2 +- db/fixtures/development/17_cycle_analytics.rb | 2 +- .../projects/templates_controller_spec.rb | 4 +- spec/factories/projects.rb | 15 ++-- .../project_owner_creates_license_file_spec.rb | 2 +- spec/features/projects/issuable_templates_spec.rb | 25 +++--- spec/lib/gitlab/git_access_spec.rb | 5 +- spec/models/cycle_analytics/production_spec.rb | 5 +- spec/models/cycle_analytics/staging_spec.rb | 5 +- spec/models/project_spec.rb | 2 +- spec/models/repository_spec.rb | 90 +++++++++++----------- .../merge_requests/resolve_service_spec.rb | 5 +- spec/support/cycle_analytics_helpers.rb | 7 +- 16 files changed, 86 insertions(+), 102 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index b747b71ae68..73256df0591 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -747,7 +747,7 @@ class Repository end # rubocop:disable Metrics/ParameterLists - def commit_dir( + def create_dir( user, path, message:, branch_name:, author_email: nil, author_name: nil, @@ -778,14 +778,12 @@ class Repository # rubocop:enable Metrics/ParameterLists # rubocop:disable Metrics/ParameterLists - def commit_file( + def create_file( user, path, content, - message:, branch_name:, update: true, + message:, branch_name:, author_email: nil, author_name: nil, start_branch_name: nil, start_project: project) - action = update ? :update : :create - multi_action( user: user, message: message, @@ -794,7 +792,7 @@ class Repository author_name: author_name, start_branch_name: start_branch_name, start_project: start_project, - actions: [{ action: action, + actions: [{ action: :create, file_path: path, content: content }]) end @@ -803,7 +801,7 @@ class Repository # rubocop:disable Metrics/ParameterLists def update_file( user, path, content, - message:, branch_name:, previous_path:, + message:, branch_name:, previous_path: nil, author_email: nil, author_name: nil, start_branch_name: nil, start_project: project) action = if previous_path && previous_path != path @@ -828,7 +826,7 @@ class Repository # rubocop:enable Metrics/ParameterLists # rubocop:disable Metrics/ParameterLists - def remove_file( + def delete_file( user, path, message:, branch_name:, author_email: nil, author_name: nil, diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb index 858de5f0538..083ffdc634c 100644 --- a/app/services/files/create_dir_service.rb +++ b/app/services/files/create_dir_service.rb @@ -1,7 +1,7 @@ module Files class CreateDirService < Files::BaseService def commit - repository.commit_dir( + repository.create_dir( current_user, @file_path, message: @commit_message, diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index 88dd7bbaedb..63be33409e2 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -1,13 +1,12 @@ module Files class CreateService < Files::BaseService def commit - repository.commit_file( + repository.create_file( current_user, @file_path, @file_content, message: @commit_message, branch_name: @target_branch, - update: false, author_email: @author_email, author_name: @author_name, start_project: @start_project, diff --git a/app/services/files/destroy_service.rb b/app/services/files/destroy_service.rb index c3be806a42d..e294659bc98 100644 --- a/app/services/files/destroy_service.rb +++ b/app/services/files/destroy_service.rb @@ -1,7 +1,7 @@ module Files class DestroyService < Files::BaseService def commit - repository.remove_file( + repository.delete_file( current_user, @file_path, message: @commit_message, diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb index 647ed75b6d1..aea0a72b633 100644 --- a/db/fixtures/development/17_cycle_analytics.rb +++ b/db/fixtures/development/17_cycle_analytics.rb @@ -155,7 +155,7 @@ class Gitlab::Seeder::CycleAnalytics issue.project.repository.add_branch(@user, branch_name, 'master') - commit_sha = issue.project.repository.commit_file(@user, filename, "content", options, message: "Commit for ##{issue.iid}", branch_name: branch_name) + commit_sha = issue.project.repository.create_file(@user, filename, "content", options, message: "Commit for ##{issue.iid}", branch_name: branch_name) issue.project.repository.commit(commit_sha) GitPushService.new(issue.project, diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb index 066a1831852..70e7f9ca96e 100644 --- a/spec/controllers/projects/templates_controller_spec.rb +++ b/spec/controllers/projects/templates_controller_spec.rb @@ -14,8 +14,8 @@ describe Projects::TemplatesController do before do project.add_user(user, Gitlab::Access::MASTER) - project.repository.commit_file(user, file_path_1, 'something valid', - message: 'test 3', branch_name: 'master', update: false) + project.repository.create_file(user, file_path_1, 'something valid', + message: 'test 3', branch_name: 'master') end describe '#show' do diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index c80b09e9b9d..586efdefdb3 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -138,27 +138,24 @@ FactoryGirl.define do project.add_user(args[:user], args[:access]) - project.repository.commit_file( + project.repository.create_file( args[:user], ".gitlab/#{args[:path]}/bug.md", 'something valid', message: 'test 3', - branch_name: 'master', - update: false) - project.repository.commit_file( + branch_name: 'master') + project.repository.create_file( args[:user], ".gitlab/#{args[:path]}/template_test.md", 'template_test', message: 'test 1', - branch_name: 'master', - update: false) - project.repository.commit_file( + branch_name: 'master') + project.repository.create_file( args[:user], ".gitlab/#{args[:path]}/feature_proposal.md", 'feature_proposal', message: 'test 2', - branch_name: 'master', - update: false) + branch_name: 'master') end end end diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb index f8ef4577a26..ccadc936567 100644 --- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb +++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb @@ -6,7 +6,7 @@ feature 'project owner creates a license file', feature: true, js: true do let(:project_master) { create(:user) } let(:project) { create(:project) } background do - project.repository.remove_file(project_master, 'LICENSE', + project.repository.delete_file(project_master, 'LICENSE', message: 'Remove LICENSE', branch_name: 'master') project.team << [project_master, :master] login_as(project_master) diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index e90a033b8c4..62d0aedda48 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -18,20 +18,18 @@ feature 'issuable templates', feature: true, js: true do let(:description_addition) { ' appending to description' } background do - project.repository.commit_file( + project.repository.create_file( user, '.gitlab/issue_templates/bug.md', template_content, message: 'added issue template', - branch_name: 'master', - update: false) - project.repository.commit_file( + branch_name: 'master') + project.repository.create_file( user, '.gitlab/issue_templates/test.md', longtemplate_content, message: 'added issue template', - branch_name: 'master', - update: false) + branch_name: 'master') visit edit_namespace_project_issue_path project.namespace, project, issue fill_in :'issue[title]', with: 'test issue title' end @@ -79,13 +77,12 @@ feature 'issuable templates', feature: true, js: true do let(:issue) { create(:issue, author: user, assignee: user, project: project) } background do - project.repository.commit_file( + project.repository.create_file( user, '.gitlab/issue_templates/bug.md', template_content, message: 'added issue template', - branch_name: 'master', - update: false) + branch_name: 'master') visit edit_namespace_project_issue_path project.namespace, project, issue fill_in :'issue[title]', with: 'test issue title' fill_in :'issue[description]', with: prior_description @@ -104,13 +101,12 @@ feature 'issuable templates', feature: true, js: true do let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) } background do - project.repository.commit_file( + project.repository.create_file( user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, message: 'added merge request template', - branch_name: 'master', - update: false) + branch_name: 'master') visit edit_namespace_project_merge_request_path project.namespace, project, merge_request fill_in :'merge_request[title]', with: 'test merge request title' end @@ -135,13 +131,12 @@ feature 'issuable templates', feature: true, js: true do fork_project.team << [fork_user, :master] create(:forked_project_link, forked_to_project: fork_project, forked_from_project: project) login_as fork_user - project.repository.commit_file( + project.repository.create_file( fork_user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, message: 'added merge request template', - branch_name: 'master', - update: false) + branch_name: 'master') visit edit_namespace_project_merge_request_path project.namespace, project, merge_request fill_in :'merge_request[title]', with: 'test merge request title' end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index d37890de9ae..48f7754bed8 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -209,13 +209,12 @@ describe Gitlab::GitAccess, lib: true do stub_git_hooks project.repository.add_branch(user, unprotected_branch, 'feature') target_branch = project.repository.lookup('feature') - source_branch = project.repository.commit_file( + source_branch = project.repository.create_file( user, FFaker::InternetSE.login_user_name, FFaker::HipsterIpsum.paragraph, message: FFaker::HipsterIpsum.sentence, - branch_name: unprotected_branch, - update: false) + branch_name: unprotected_branch) rugged = project.repository.rugged author = { email: "email@example.com", time: Time.now, name: "Example Git User" } diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb index b9fe492fe2c..e6a826a9418 100644 --- a/spec/models/cycle_analytics/production_spec.rb +++ b/spec/models/cycle_analytics/production_spec.rb @@ -21,13 +21,12 @@ describe 'CycleAnalytics#production', feature: true do ["production deploy happens after merge request is merged (along with other changes)", lambda do |context, data| # Make other changes on master - sha = context.project.repository.commit_file( + sha = context.project.repository.create_file( context.user, context.random_git_name, 'content', message: 'commit message', - branch_name: 'master', - update: false) + branch_name: 'master') context.project.repository.commit(sha) context.deploy_master diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb index 78ec518ac86..3a02ed81adb 100644 --- a/spec/models/cycle_analytics/staging_spec.rb +++ b/spec/models/cycle_analytics/staging_spec.rb @@ -26,13 +26,12 @@ describe 'CycleAnalytics#staging', feature: true do ["production deploy happens after merge request is merged (along with other changes)", lambda do |context, data| # Make other changes on master - sha = context.project.repository.commit_file( + sha = context.project.repository.create_file( context.user, context.random_git_name, 'content', message: 'commit message', - branch_name: 'master', - update: false) + branch_name: 'master') context.project.repository.commit(sha) context.deploy_master diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 8a27dfff203..5232c531635 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1765,7 +1765,7 @@ describe Project, models: true do end before do - project.repository.commit_file(User.last, '.gitlab/route-map.yml', route_map, message: 'Add .gitlab/route-map.yml', branch_name: 'master', update: false) + project.repository.create_file(User.last, '.gitlab/route-map.yml', route_map, message: 'Add .gitlab/route-map.yml', branch_name: 'master') end context 'when there is a .gitlab/route-map.yml at the commit' do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 070f1102e94..75d961e0bac 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -294,7 +294,7 @@ describe Repository, models: true do describe "#commit_dir" do it "commits a change that creates a new directory" do expect do - repository.commit_dir(user, 'newdir', + repository.create_dir(user, 'newdir', message: 'Create newdir', branch_name: 'master') end.to change { repository.commits('master').count }.by(1) @@ -307,7 +307,7 @@ describe Repository, models: true do it "creates a fork and commit to the forked project" do expect do - repository.commit_dir(user, 'newdir', + repository.create_dir(user, 'newdir', message: 'Create newdir', branch_name: 'patch', start_branch_name: 'master', start_project: forked_project) end.to change { repository.commits('master').count }.by(0) @@ -323,7 +323,7 @@ describe Repository, models: true do context "when an author is specified" do it "uses the given email/name to set the commit's author" do expect do - repository.commit_dir(user, 'newdir', + repository.create_dir(user, 'newdir', message: 'Add newdir', branch_name: 'master', author_email: author_email, author_name: author_name) @@ -337,25 +337,23 @@ describe Repository, models: true do end end - describe "#commit_file" do - it 'commits change to a file successfully' do + describe "#create_file" do + it 'commits new file successfully' do expect do - repository.commit_file(user, 'CHANGELOG', 'Changelog!', - message: 'Updates file content', - branch_name: 'master', - update: true) + repository.create_file(user, 'NEWCHANGELOG', 'Changelog!', + message: 'Create changelog', + branch_name: 'master') end.to change { repository.commits('master').count }.by(1) - blob = repository.blob_at('master', 'CHANGELOG') + blob = repository.blob_at('master', 'NEWCHANGELOG') expect(blob.data).to eq('Changelog!') end it 'respects the autocrlf setting' do - repository.commit_file(user, 'hello.txt', "Hello,\r\nWorld", + repository.create_file(user, 'hello.txt', "Hello,\r\nWorld", message: 'Add hello world', - branch_name: 'master', - update: false) + branch_name: 'master') blob = repository.blob_at('master', 'hello.txt') @@ -365,10 +363,9 @@ describe Repository, models: true do context "when an author is specified" do it "uses the given email/name to set the commit's author" do expect do - repository.commit_file(user, 'README', 'README!', + repository.create_file(user, 'NEWREADME', 'README!', message: 'Add README', branch_name: 'master', - update: true, author_email: author_email, author_name: author_name) end.to change { repository.commits('master').count }.by(1) @@ -382,6 +379,18 @@ describe Repository, models: true do end describe "#update_file" do + it 'updates file successfully' do + expect do + repository.update_file(user, 'CHANGELOG', 'Changelog!', + message: 'Update changelog', + branch_name: 'master') + end.to change { repository.commits('master').count }.by(1) + + blob = repository.blob_at('master', 'CHANGELOG') + + expect(blob.data).to eq('Changelog!') + end + it 'updates filename successfully' do expect do repository.update_file(user, 'NEWLICENSE', 'Copyright!', @@ -398,8 +407,6 @@ describe Repository, models: true do context "when an author is specified" do it "uses the given email/name to set the commit's author" do - repository.commit_file(user, 'README', 'README!', - message: 'Add README', branch_name: 'master', update: true) expect do repository.update_file(user, 'README', 'Updated README!', @@ -420,11 +427,8 @@ describe Repository, models: true do describe "#remove_file" do it 'removes file successfully' do - repository.commit_file(user, 'README', 'README!', - message: 'Add README', branch_name: 'master', update: true) - expect do - repository.remove_file(user, 'README', + repository.delete_file(user, 'README', message: 'Remove README', branch_name: 'master') end.to change { repository.commits('master').count }.by(1) @@ -433,11 +437,8 @@ describe Repository, models: true do context "when an author is specified" do it "uses the given email/name to set the commit's author" do - repository.commit_file(user, 'README', 'README!', - message: 'Add README', branch_name: 'master', update: true) - expect do - repository.remove_file(user, 'README', + repository.delete_file(user, 'README', message: 'Remove README', branch_name: 'master', author_email: author_email, author_name: author_name) end.to change { repository.commits('master').count }.by(1) @@ -587,14 +588,14 @@ describe Repository, models: true do describe "#license_blob", caching: true do before do - repository.remove_file( + repository.delete_file( user, 'LICENSE', message: 'Remove LICENSE', branch_name: 'master') end it 'handles when HEAD points to non-existent ref' do - repository.commit_file( + repository.create_file( user, 'LICENSE', 'Copyright!', - message: 'Add LICENSE', branch_name: 'master', update: false) + message: 'Add LICENSE', branch_name: 'master') allow(repository).to receive(:file_on_head). and_raise(Rugged::ReferenceError) @@ -603,27 +604,27 @@ describe Repository, models: true do end it 'looks in the root_ref only' do - repository.remove_file(user, 'LICENSE', + repository.delete_file(user, 'LICENSE', message: 'Remove LICENSE', branch_name: 'markdown') - repository.commit_file(user, 'LICENSE', + repository.create_file(user, 'LICENSE', Licensee::License.new('mit').content, - message: 'Add LICENSE', branch_name: 'markdown', update: false) + message: 'Add LICENSE', branch_name: 'markdown') expect(repository.license_blob).to be_nil end it 'detects license file with no recognizable open-source license content' do - repository.commit_file(user, 'LICENSE', 'Copyright!', - message: 'Add LICENSE', branch_name: 'master', update: false) + repository.create_file(user, 'LICENSE', 'Copyright!', + message: 'Add LICENSE', branch_name: 'master') expect(repository.license_blob.name).to eq('LICENSE') end %w[LICENSE LICENCE LiCensE LICENSE.md LICENSE.foo COPYING COPYING.md].each do |filename| it "detects '#{filename}'" do - repository.commit_file(user, filename, + repository.create_file(user, filename, Licensee::License.new('mit').content, - message: "Add #{filename}", branch_name: 'master', update: false) + message: "Add #{filename}", branch_name: 'master') expect(repository.license_blob.name).to eq(filename) end @@ -632,7 +633,7 @@ describe Repository, models: true do describe '#license_key', caching: true do before do - repository.remove_file(user, 'LICENSE', + repository.delete_file(user, 'LICENSE', message: 'Remove LICENSE', branch_name: 'master') end @@ -647,16 +648,16 @@ describe Repository, models: true do end it 'detects license file with no recognizable open-source license content' do - repository.commit_file(user, 'LICENSE', 'Copyright!', - message: 'Add LICENSE', branch_name: 'master', update: false) + repository.create_file(user, 'LICENSE', 'Copyright!', + message: 'Add LICENSE', branch_name: 'master') expect(repository.license_key).to be_nil end it 'returns the license key' do - repository.commit_file(user, 'LICENSE', + repository.create_file(user, 'LICENSE', Licensee::License.new('mit').content, - message: 'Add LICENSE', branch_name: 'master', update: false) + message: 'Add LICENSE', branch_name: 'master') expect(repository.license_key).to eq('mit') end @@ -913,10 +914,9 @@ describe Repository, models: true do expect(empty_repository).to receive(:expire_emptiness_caches) expect(empty_repository).to receive(:expire_branches_cache) - empty_repository.commit_file(user, 'CHANGELOG', 'Changelog!', + empty_repository.create_file(user, 'CHANGELOG', 'Changelog!', message: 'Updates file content', - branch_name: 'master', - update: false) + branch_name: 'master') end end end @@ -1796,7 +1796,7 @@ describe Repository, models: true do describe '#gitlab_ci_yml_for' do before do - repository.commit_file(User.last, '.gitlab-ci.yml', 'CONTENT', message: 'Add .gitlab-ci.yml', branch_name: 'master', update: false) + repository.create_file(User.last, '.gitlab-ci.yml', 'CONTENT', message: 'Add .gitlab-ci.yml', branch_name: 'master') end context 'when there is a .gitlab-ci.yml at the commit' do @@ -1814,7 +1814,7 @@ describe Repository, models: true do describe '#route_map_for' do before do - repository.commit_file(User.last, '.gitlab/route-map.yml', 'CONTENT', message: 'Add .gitlab/route-map.yml', branch_name: 'master', update: false) + repository.create_file(User.last, '.gitlab/route-map.yml', 'CONTENT', message: 'Add .gitlab/route-map.yml', branch_name: 'master') end context 'when there is a .gitlab/route-map.yml at the commit' do diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb index c3b468ac47f..d33535d22af 100644 --- a/spec/services/merge_requests/resolve_service_spec.rb +++ b/spec/services/merge_requests/resolve_service_spec.rb @@ -66,13 +66,12 @@ describe MergeRequests::ResolveService do context 'when the source project is a fork and does not contain the HEAD of the target branch' do let!(:target_head) do - project.repository.commit_file( + project.repository.create_file( user, 'new-file-in-target', '', message: 'Add new file in target', - branch_name: 'conflict-start', - update: false) + branch_name: 'conflict-start') end before do diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb index 26f3ed5300b..c864a705ca4 100644 --- a/spec/support/cycle_analytics_helpers.rb +++ b/spec/support/cycle_analytics_helpers.rb @@ -9,7 +9,7 @@ module CycleAnalyticsHelpers commit_shas = Array.new(count) do |index| filename = random_git_name - commit_sha = project.repository.commit_file(user, filename, "content", message: message, branch_name: branch_name) + commit_sha = project.repository.create_file(user, filename, "content", message: message, branch_name: branch_name) project.repository.commit(commit_sha) commit_sha @@ -28,13 +28,12 @@ module CycleAnalyticsHelpers project.repository.add_branch(user, source_branch, 'master') end - sha = project.repository.commit_file( + sha = project.repository.create_file( user, random_git_name, 'content', message: 'commit message', - branch_name: source_branch, - update: false) + branch_name: source_branch) project.repository.commit(sha) opts = { -- cgit v1.2.3 From c927bc3ffa11c4b6033eb8d65f33c511f3207552 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 16 Feb 2017 09:41:54 -0600 Subject: Fix specs --- spec/models/repository_spec.rb | 1 - spec/requests/api/files_spec.rb | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 75d961e0bac..58943fd7252 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -407,7 +407,6 @@ describe Repository, models: true do context "when an author is specified" do it "uses the given email/name to set the commit's author" do - expect do repository.update_file(user, 'README', 'Updated README!', branch_name: 'master', diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index a8ce0430401..29d67b5259e 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -127,7 +127,7 @@ describe API::Files, api: true do end it "returns a 400 if editor fails to create file" do - allow_any_instance_of(Repository).to receive(:commit_file). + allow_any_instance_of(Repository).to receive(:create_file). and_return(false) post api("/projects/#{project.id}/repository/files", user), valid_params @@ -215,7 +215,7 @@ describe API::Files, api: true do end it "returns a 400 if fails to create file" do - allow_any_instance_of(Repository).to receive(:remove_file).and_return(false) + allow_any_instance_of(Repository).to receive(:delete_file).and_return(false) delete api("/projects/#{project.id}/repository/files", user), valid_params -- cgit v1.2.3 From 5bbd09ca8ad15ad0033353c8e7b9f0bfda96f44d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 23 Feb 2017 13:00:16 -0600 Subject: Fix spec --- spec/requests/api/v3/files_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/requests/api/v3/files_spec.rb b/spec/requests/api/v3/files_spec.rb index 4af05605ec6..52fd908af7d 100644 --- a/spec/requests/api/v3/files_spec.rb +++ b/spec/requests/api/v3/files_spec.rb @@ -127,7 +127,7 @@ describe API::V3::Files, api: true do end it "returns a 400 if editor fails to create file" do - allow_any_instance_of(Repository).to receive(:commit_file). + allow_any_instance_of(Repository).to receive(:create_file). and_return(false) post v3_api("/projects/#{project.id}/repository/files", user), valid_params @@ -215,7 +215,7 @@ describe API::V3::Files, api: true do end it "returns a 400 if fails to create file" do - allow_any_instance_of(Repository).to receive(:remove_file).and_return(false) + allow_any_instance_of(Repository).to receive(:delete_file).and_return(false) delete v3_api("/projects/#{project.id}/repository/files", user), valid_params -- cgit v1.2.3 From 564b0eb017df77a166ee124a36c02972799ec911 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 23 Feb 2017 14:27:07 -0600 Subject: Fix new offenses --- lib/gitlab/git/index.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/gitlab/git/index.rb b/lib/gitlab/git/index.rb index 3d0000879f8..69a7a513281 100644 --- a/lib/gitlab/git/index.rb +++ b/lib/gitlab/git/index.rb @@ -10,9 +10,7 @@ module Gitlab @raw_index = repository.rugged.index end - def read_tree(tree) - raw_index.read_tree(tree) - end + delegate :read_tree, to: :raw_index def write_tree raw_index.write_tree(repository.rugged) -- cgit v1.2.3 From 9e39b317a3865688e9fb204fb8680d3e91958fec Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 23 Feb 2017 15:14:52 -0600 Subject: Update API v3 in line with v4 --- lib/api/v3/commits.rb | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb index 126cc95fc3d..506204b3517 100644 --- a/lib/api/v3/commits.rb +++ b/lib/api/v3/commits.rb @@ -55,13 +55,6 @@ module API branch = attrs.delete(:branch_name) attrs.merge!(branch: branch, start_branch: branch, target_branch: branch) - attrs[:actions].map! do |action| - action[:action] = action[:action].to_sym - action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/') - action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/') - action - end - result = ::Files::MultiService.new(user_project, current_user, attrs).execute if result[:status] == :success -- cgit v1.2.3 From a530e9da3580e05a171b3ba0a3f4408afc000b10 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 24 Feb 2017 09:32:10 -0600 Subject: Address review --- app/models/repository.rb | 4 ++-- app/services/files/multi_service.rb | 12 ++++++------ lib/gitlab/git/index.rb | 36 +++++++++++++++++++++--------------- spec/lib/gitlab/git/index_spec.rb | 10 ++++++++++ spec/models/repository_spec.rb | 4 ++-- 5 files changed, 41 insertions(+), 25 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 73256df0591..c3fceddb9ca 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -753,7 +753,7 @@ class Repository author_email: nil, author_name: nil, start_branch_name: nil, start_project: project) - entry = tree_entry_at(start_branch_name || branch_name, path) + entry = start_project.repository.tree_entry_at(start_branch_name || branch_name, path) if entry if entry[:type] == :blob raise Gitlab::Git::Repository::InvalidBlobName.new( @@ -865,7 +865,7 @@ class Repository end actions.each do |options| - index.__send__(options.delete(:action), options) + index.public_send(options.delete(:action), options) end options = { diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb index 809fa32eca5..2c89f6e7f92 100644 --- a/app/services/files/multi_service.rb +++ b/app/services/files/multi_service.rb @@ -22,6 +22,12 @@ module Files def validate super params[:actions].each_with_index do |action, index| + if ACTIONS.include?(action[:action].to_s) + action[:action] = action[:action].to_sym + else + raise_error("Unknown action type `#{action[:action]}`.") + end + unless action[:file_path].present? raise_error("You must specify a file_path.") end @@ -32,12 +38,6 @@ module Files regex_check(action[:file_path]) regex_check(action[:previous_path]) if action[:previous_path] - if ACTIONS.include?(action[:action].to_s) - action[:action] = action[:action].to_sym - else - raise_error("Unknown action type `#{action[:action]}`.") - end - if project.empty_repo? && action[:action] != :create raise_error("No files to #{action[:action]}.") end diff --git a/lib/gitlab/git/index.rb b/lib/gitlab/git/index.rb index 69a7a513281..546696e4bd1 100644 --- a/lib/gitlab/git/index.rb +++ b/lib/gitlab/git/index.rb @@ -10,20 +10,20 @@ module Gitlab @raw_index = repository.rugged.index end - delegate :read_tree, to: :raw_index + delegate :read_tree, :get, to: :raw_index def write_tree raw_index.write_tree(repository.rugged) end - def get(*args) - raw_index.get(*args) + def dir_exists?(path) + raw_index.find { |entry| entry[:path].start_with?("#{path}/") } end def create(options) - normalize_options!(options) + options = normalize_options(options) - file_entry = raw_index.get(options[:file_path]) + file_entry = get(options[:file_path]) if file_entry raise Gitlab::Git::Repository::InvalidBlobName.new("Filename already exists") end @@ -32,13 +32,17 @@ module Gitlab end def create_dir(options) - normalize_options!(options) + options = normalize_options(options) - file_entry = raw_index.get(options[:file_path]) + file_entry = get(options[:file_path]) if file_entry raise Gitlab::Git::Repository::InvalidBlobName.new("Directory already exists as a file") end + if dir_exists?(options[:file_path]) + raise Gitlab::Git::Repository::InvalidBlobName.new("Directory already exists") + end + options = options.dup options[:file_path] += '/.gitkeep' options[:content] = '' @@ -47,9 +51,9 @@ module Gitlab end def update(options) - normalize_options!(options) + options = normalize_options(options) - file_entry = raw_index.get(options[:file_path]) + file_entry = get(options[:file_path]) unless file_entry raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist") end @@ -58,9 +62,9 @@ module Gitlab end def move(options) - normalize_options!(options) + options = normalize_options(options) - file_entry = raw_index.get(options[:previous_path]) + file_entry = get(options[:previous_path]) unless file_entry raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist") end @@ -71,9 +75,9 @@ module Gitlab end def delete(options) - normalize_options!(options) + options = normalize_options(options) - file_entry = raw_index.get(options[:file_path]) + file_entry = get(options[:file_path]) unless file_entry raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist") end @@ -83,13 +87,15 @@ module Gitlab private - def normalize_options!(options) + def normalize_options(options) + options = options.dup options[:file_path] = normalize_path(options[:file_path]) if options[:file_path] options[:previous_path] = normalize_path(options[:previous_path]) if options[:previous_path] + options end def normalize_path(path) - pathname = Gitlab::Git::PathHelper.normalize_path(path) + pathname = Gitlab::Git::PathHelper.normalize_path(path.dup) if pathname.each_filename.include?('..') raise Gitlab::Git::Repository::InvalidBlobName.new('Invalid path') diff --git a/spec/lib/gitlab/git/index_spec.rb b/spec/lib/gitlab/git/index_spec.rb index 91ffbd138ce..d0c7ca60ddc 100644 --- a/spec/lib/gitlab/git/index_spec.rb +++ b/spec/lib/gitlab/git/index_spec.rb @@ -92,6 +92,16 @@ describe Gitlab::Git::Index, seed_helper: true do expect { index.create_dir(options) }.to raise_error('Directory already exists as a file') end end + + context 'when a directory at that path exists' do + before do + options[:file_path] = 'files/executables' + end + + it 'raises an error' do + expect { index.create_dir(options) }.to raise_error('Directory already exists') + end + end end describe '#update' do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 58943fd7252..ae203fada12 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -291,7 +291,7 @@ describe Repository, models: true do end end - describe "#commit_dir" do + describe "#create_dir" do it "commits a change that creates a new directory" do expect do repository.create_dir(user, 'newdir', @@ -424,7 +424,7 @@ describe Repository, models: true do end end - describe "#remove_file" do + describe "#delete_file" do it 'removes file successfully' do expect do repository.delete_file(user, 'README', -- cgit v1.2.3 From c72e71552209aae964e5aa931d435f7e1164e5d1 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 24 Feb 2017 09:38:44 -0600 Subject: Raise error when no content is provided --- app/services/files/create_service.rb | 4 ++++ app/services/files/multi_service.rb | 22 +++++++++++++++------- app/services/files/update_service.rb | 4 ++++ lib/gitlab/git/index.rb | 2 -- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index 63be33409e2..ec45a60eda1 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -16,6 +16,10 @@ module Files def validate super + if @file_content.empty? + raise_error("You must provide content.") + end + if @file_path =~ Gitlab::Regex.directory_traversal_regex raise_error( 'Your changes could not be committed, because the file name ' + diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb index 2c89f6e7f92..93f77f29aff 100644 --- a/app/services/files/multi_service.rb +++ b/app/services/files/multi_service.rb @@ -27,7 +27,7 @@ module Files else raise_error("Unknown action type `#{action[:action]}`.") end - + unless action[:file_path].present? raise_error("You must specify a file_path.") end @@ -100,6 +100,20 @@ module Files if repository.blob_at_branch(params[:branch], action[:file_path]) raise_error("Your changes could not be committed because a file with the name `#{action[:file_path]}` already exists.") end + + if action[:content].empty? + raise_error("You must provide content.") + end + end + + def validate_update(action) + if action[:content].empty? + raise_error("You must provide content.") + end + + if file_has_changed? + raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.") + end end def validate_delete(action) @@ -122,11 +136,5 @@ module Files params[:actions][index][:content] = blob.data end end - - def validate_update(action) - if file_has_changed? - raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.") - end - end end end diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index a71fe61a4b6..462de06b6ff 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -18,6 +18,10 @@ module Files def validate super + if @file_content.empty? + raise_error("You must provide content.") + end + if file_has_changed? raise FileChangedError.new("You are attempting to update a file that has changed since you started editing it.") end diff --git a/lib/gitlab/git/index.rb b/lib/gitlab/git/index.rb index 546696e4bd1..af1744c9c46 100644 --- a/lib/gitlab/git/index.rb +++ b/lib/gitlab/git/index.rb @@ -106,8 +106,6 @@ module Gitlab def add_blob(options, mode: nil) content = options[:content] - return unless content - content = Base64.decode64(content) if options[:encoding] == 'base64' detect = CharlockHolmes::EncodingDetector.new.detect(content) -- cgit v1.2.3 From 3d880d9350e4c9e590a83f84b6a5645873800df8 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 24 Feb 2017 09:52:59 -0600 Subject: We don't need these checks anymore --- app/models/repository.rb | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index c3fceddb9ca..3bf89ee52a3 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -753,17 +753,6 @@ class Repository author_email: nil, author_name: nil, start_branch_name: nil, start_project: project) - entry = start_project.repository.tree_entry_at(start_branch_name || branch_name, path) - if entry - if entry[:type] == :blob - raise Gitlab::Git::Repository::InvalidBlobName.new( - "Directory already exists as a file") - else - raise Gitlab::Git::Repository::InvalidBlobName.new( - "Directory already exists") - end - end - multi_action( user: user, message: message, @@ -1160,14 +1149,6 @@ class Repository blob_data_at(sha, '.gitlab-ci.yml') end - protected - - def tree_entry_at(branch_name, path) - branch_exists?(branch_name) && - # tree_entry is private - raw_repository.send(:tree_entry, commit(branch_name), path) - end - private def blob_data_at(sha, path) -- cgit v1.2.3 From 3ecca3b0394cb75f866e005714f2a6a238bb840d Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 24 Feb 2017 16:28:30 +0000 Subject: Adds backoff algo from EE to CE --- .../javascripts/lib/utils/common_utils.js.es6 | 52 ++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index 45a1d90a9d9..dbf40ec7fcf 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -296,5 +296,57 @@ * @returns {Boolean} */ w.gl.utils.convertPermissionToBoolean = permission => permission === 'true'; + + /** + * Back Off exponential algorithm + * backOff :: (Function, Number) -> Promise + * + * @param {Function} fn function to be called + * @param {Number} timeout + * @return {Promise} + * @example + * ``` + * backOff(function (next, stop) { + * // Let's perform this function repeatedly for 60s or for the timeout provided. + * + * ourFunction() + * .then(function (result) { + * // continue if result is not what we need + * next(); + * + * // when result is what we need let's stop with the repetions and jump out of the cycle + * stop(result); + * }) + * .catch(function (error) { + * // if there is an error, we need to stop this with an error. + * stop(error); + * }) + * }, 60000) + * .then(function (result) {}) + * .catch(function (error) { + * // deal with errors passed to stop() + * }) + * ``` + */ + w.gl.utils.backOff = (fn, timeout = 60000) => { + let nextInterval = 2000; + + const startTime = (+new Date()); + + return new Promise((resolve, reject) => { + const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg)); + + const next = () => { + if (new Date().getTime() - startTime < timeout) { + setTimeout(fn.bind(null, next, stop), nextInterval); + nextInterval *= 2; + } else { + reject(new Error('BACKOFF_TIMEOUT')); + } + }; + + fn(next, stop); + }); + }; })(window); }).call(window); -- cgit v1.2.3 From 3a1fff9c62c8ebbd5ecd7ede2c7960cd048a7c48 Mon Sep 17 00:00:00 2001 From: Pedro Moreira da Silva Date: Thu, 26 Jan 2017 17:54:50 +0000 Subject: Format milestone header as issues and MRs * Enclose the identifier in strong * Use the % character as in GFM * Remove spaces around en dash for date range --- app/helpers/milestones_helper.rb | 2 +- app/views/projects/milestones/show.html.haml | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 729928ce1dd..7011e670cee 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -97,7 +97,7 @@ module MilestonesHelper def milestone_date_range(milestone) if milestone.start_date && milestone.due_date - "#{milestone.start_date.to_s(:medium)} - #{milestone.due_date.to_s(:medium)}" + "#{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)}" diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 06a31698ee6..a216d59bc74 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -19,10 +19,9 @@ Open .header-text-content %span.identifier - Milestone ##{@milestone.iid} + %strong + Milestone %#{@milestone.iid} - if @milestone.due_date || @milestone.start_date - %span.creator - · = milestone_date_range(@milestone) .milestone-buttons - if can?(current_user, :admin_milestone, @project) -- cgit v1.2.3 From 2ca2e06269d40703281c7af45f9e1e6c3da2012d Mon Sep 17 00:00:00 2001 From: Pedro Moreira da Silva Date: Fri, 24 Feb 2017 17:07:29 +0000 Subject: Fix specs --- spec/features/milestone_spec.rb | 2 +- spec/helpers/milestones_helper_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb index a2e40546588..c3297de709a 100644 --- a/spec/features/milestone_spec.rb +++ b/spec/features/milestone_spec.rb @@ -24,7 +24,7 @@ feature 'Milestone', feature: true do 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') + 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 14a95479339..68b20a1e4fc 100644 --- a/spec/helpers/milestones_helper_spec.rb +++ b/spec/helpers/milestones_helper_spec.rb @@ -17,7 +17,7 @@ describe MilestonesHelper do 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}") } + it { expect(result_for(start_date: yesterday, due_date: tomorrow)).to eq("#{yesterday_formatted}–#{tomorrow_formatted}") } end describe '#milestone_counts' do -- cgit v1.2.3 From 2da8bc3de9f8b63bd80a081c7e2880adee3edb71 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 24 Feb 2017 15:56:41 +0000 Subject: Only create unmergeable todos once Previously, we created an unmergeable todo when a merge request: 1. Had merge when pipeline succeeds set. 2. Became unmergeable. However, when merge when pipeline succeeds fails due to unmergeability, the flag isn't actually removed. And a merge request can become unmergeable multiple times, as every time the target branch is updated we need to re-check the mergeable status. This means that if the todo was marked done, and the MR was checked again, a new todo would be created for the same event. Instead of checking this, we should create the todo from the service responsible for merging when the pipeline succeeds. That way the todo is guaranteed to only be created when we care about it. --- app/models/merge_request.rb | 4 ---- .../merge_when_pipeline_succeeds_service.rb | 6 +++++- .../only-create-unmergeable-todo-once.yml | 4 ++++ spec/models/merge_request_spec.rb | 6 ------ .../merge_when_pipeline_succeeds_service_spec.rb | 25 ++++++++++++++++++++++ 5 files changed, 34 insertions(+), 11 deletions(-) create mode 100644 changelogs/unreleased/only-create-unmergeable-todo-once.yml diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 7eb875f1ef5..d6e7ed87555 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -91,10 +91,6 @@ class MergeRequest < ActiveRecord::Base around_transition do |merge_request, transition, block| Gitlab::Timeless.timeless(merge_request, &block) end - - after_transition unchecked: :cannot_be_merged do |merge_request, transition| - TodoService.new.merge_request_became_unmergeable(merge_request) - end end validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?] diff --git a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb index 5616edf8b4a..5081dd5a0c4 100644 --- a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb +++ b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb @@ -24,7 +24,11 @@ module MergeRequests pipeline_merge_requests(pipeline) do |merge_request| next unless merge_request.merge_when_build_succeeds? - next unless merge_request.mergeable? + + unless merge_request.mergeable? + todo_service.merge_request_became_unmergeable(merge_request) + next + end MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params) end diff --git a/changelogs/unreleased/only-create-unmergeable-todo-once.yml b/changelogs/unreleased/only-create-unmergeable-todo-once.yml new file mode 100644 index 00000000000..e675ed945ad --- /dev/null +++ b/changelogs/unreleased/only-create-unmergeable-todo-once.yml @@ -0,0 +1,4 @@ +--- +title: Only create unmergeable todos once when MR fails to merge +merge_request: +author: diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index fa1b0396bcf..9331dc41a5e 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -833,12 +833,6 @@ describe MergeRequest, models: true do it 'becomes unmergeable' do expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged') end - - it 'creates Todo on unmergeability' do - expect_any_instance_of(TodoService).to receive(:merge_request_became_unmergeable).with(subject) - - subject.check_if_can_be_merged - end end context 'when it has conflicts' do 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 f92978a33a3..0ff6e8fda16 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 @@ -111,6 +111,31 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do service.trigger(unrelated_pipeline) end end + + context 'when the merge request is not mergeable' do + let(:mr_conflict) do + create(:merge_request, merge_when_build_succeeds: true, merge_user: user, + source_branch: 'master', target_branch: 'feature-conflict', + source_project: project, target_project: project) + end + + let(:conflict_pipeline) do + create(:ci_pipeline, project: project, ref: mr_conflict.source_branch, + sha: mr_conflict.diff_head_sha, status: 'success') + end + + it 'does not merge the merge request' do + expect(MergeWorker).not_to receive(:perform_async) + + service.trigger(conflict_pipeline) + end + + it 'creates todos for unmergeability' do + expect_any_instance_of(TodoService).to receive(:merge_request_became_unmergeable).with(mr_conflict) + + service.trigger(conflict_pipeline) + end + end end describe "#cancel" do -- cgit v1.2.3 From be18e70cd13eed870add512846534852da375814 Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Fri, 24 Feb 2017 11:31:11 -0600 Subject: Corrected indentation on the template string Also removed eslint disabled rules --- app/assets/javascripts/user_callout.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js index 300d825ec9a..74b869502a4 100644 --- a/app/assets/javascripts/user_callout.js +++ b/app/assets/javascripts/user_callout.js @@ -1,4 +1,3 @@ -/* eslint-disable class-methods-use-this */ /* global Cookies */ const userCalloutElementName = '.user-callout'; @@ -16,15 +15,15 @@ const USER_CALLOUT_TEMPLATE = `
    -
    -

    - Customize your experience -

    -

    - Change syntax themes, default project pages, and more in preferences. -

    - Check it out -
    +
    +

    + Customize your experience +

    +

    + Change syntax themes, default project pages, and more in preferences. +

    + Check it out +
  • `; -- cgit v1.2.3 From 38a7a5f4bc80265ba84bd49e951becfa54745518 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 24 Feb 2017 11:43:58 -0600 Subject: Add newline --- app/services/files/multi_service.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb index 93f77f29aff..ba99af00e96 100644 --- a/app/services/files/multi_service.rb +++ b/app/services/files/multi_service.rb @@ -21,6 +21,7 @@ module Files def validate super + params[:actions].each_with_index do |action, index| if ACTIONS.include?(action[:action].to_s) action[:action] = action[:action].to_sym -- cgit v1.2.3 From 0d000d351ca587ff7a6d4a14ad3cfa693238eec0 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Mon, 16 Jan 2017 16:43:03 -0200 Subject: Fix issuable stale object error handler for js when updating tasklists --- app/assets/javascripts/task_list.js | 12 +++++ app/controllers/concerns/issuable_actions.rb | 17 +++++++ app/controllers/projects/issues_controller.rb | 3 +- .../projects/merge_requests_controller.rb | 23 +++++---- changelogs/unreleased/issue_24815.yml | 4 ++ .../controllers/projects/issues_controller_spec.rb | 12 +++-- .../projects/merge_requests_controller_spec.rb | 2 + spec/support/update_invalid_issuable.rb | 57 ++++++++++++++++++++++ 8 files changed, 111 insertions(+), 19 deletions(-) create mode 100644 changelogs/unreleased/issue_24815.yml create mode 100644 spec/support/update_invalid_issuable.rb diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js index dfe24d1fb33..b1402c0a880 100644 --- a/app/assets/javascripts/task_list.js +++ b/app/assets/javascripts/task_list.js @@ -1,3 +1,4 @@ +/* global Flash */ require('vendor/task_list'); class TaskList { @@ -6,6 +7,16 @@ class TaskList { this.dataType = options.dataType; this.fieldName = options.fieldName; this.onSuccess = options.onSuccess || (() => {}); + this.onError = function showFlash(response) { + let errorMessages = ''; + + if (response.responseJSON) { + errorMessages = response.responseJSON.errors.join(' '); + } + + return new Flash(errorMessages || 'Update failed', 'alert'); + }; + this.init(); } @@ -32,6 +43,7 @@ class TaskList { url: $target.data('update-url') || $('form.js-issuable-update').attr('action'), data: patchData, success: this.onSuccess, + error: this.onError, }); } } diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 0821974aa93..3ccf2a9ce33 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -26,6 +26,23 @@ module IssuableActions private + def render_conflict_response + respond_to do |format| + format.html do + @conflict = true + render :edit + end + + format.json do + render json: { + errors: [ + "Someone edited this #{issuable.human_class_name} at the same time you did. Please refresh your browser and make sure your changes will not unintentionally remove theirs." + ] + }, status: 409 + end + end + end + def labels @labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index ca5e81100da..1151555b8fa 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -134,8 +134,7 @@ class Projects::IssuesController < Projects::ApplicationController end rescue ActiveRecord::StaleObjectError - @conflict = true - render :edit + render_conflict_response end def referenced_merge_requests diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 365c49a20d4..dda7a92550a 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -296,22 +296,21 @@ class Projects::MergeRequestsController < Projects::ApplicationController def update @merge_request = MergeRequests::UpdateService.new(project, current_user, merge_request_params).execute(@merge_request) - if @merge_request.valid? - respond_to do |format| - format.html do - redirect_to([@merge_request.target_project.namespace.becomes(Namespace), - @merge_request.target_project, @merge_request]) - end - format.json do - render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short]) + respond_to do |format| + format.html do + if @merge_request.valid? + redirect_to([@merge_request.target_project.namespace.becomes(Namespace), @merge_request.target_project, @merge_request]) + else + render :edit end end - else - render "edit" + + format.json do + render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short]) + end end rescue ActiveRecord::StaleObjectError - @conflict = true - render :edit + render_conflict_response end def remove_wip diff --git a/changelogs/unreleased/issue_24815.yml b/changelogs/unreleased/issue_24815.yml new file mode 100644 index 00000000000..916e47d36a9 --- /dev/null +++ b/changelogs/unreleased/issue_24815.yml @@ -0,0 +1,4 @@ +--- +title: Fix issuable stale object error handler for js when updating tasklists +merge_request: +author: diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 7871b6a9e10..147c3a60c06 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -125,14 +125,16 @@ describe Projects::IssuesController do end describe 'PUT #update' do + before do + sign_in(user) + project.team << [user, :developer] + end + + it_behaves_like 'update invalid issuable', Issue + context 'when moving issue to another private project' do let(:another_project) { create(:empty_project, :private) } - before do - sign_in(user) - project.team << [user, :developer] - end - context 'when user has access to move issue' do before { another_project.team << [user, :reporter] } diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index f84f922ba5e..b6f329afd97 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -254,6 +254,8 @@ describe Projects::MergeRequestsController do expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch } end + + it_behaves_like 'update invalid issuable', MergeRequest end end diff --git a/spec/support/update_invalid_issuable.rb b/spec/support/update_invalid_issuable.rb new file mode 100644 index 00000000000..f984ac7bfa7 --- /dev/null +++ b/spec/support/update_invalid_issuable.rb @@ -0,0 +1,57 @@ +shared_examples 'update invalid issuable' do |klass| + let(:params) do + { + namespace_id: project.namespace.path, + project_id: project.path, + id: issuable.iid + } + end + + let(:issuable) do + klass == Issue ? issue : merge_request + end + + before do + if klass == Issue + params.merge!(issue: { title: "any" }) + else + params.merge!(merge_request: { title: "any" }) + end + end + + context 'when updating causes conflicts' do + before do + allow_any_instance_of(issuable.class).to receive(:save). + and_raise(ActiveRecord::StaleObjectError.new(issuable, :save)) + end + + it 'renders edit when format is html' do + put :update, params + + expect(response).to render_template(:edit) + expect(assigns[:conflict]).to be_truthy + end + + it 'renders json error message when format is json' do + params.merge!(format: "json") + + put :update, params + + expect(response.status).to eq(409) + expect(JSON.parse(response.body)).to have_key('errors') + end + end + + context 'when updating an invalid issuable' do + before do + key = klass == Issue ? :issue : :merge_request + params[key][:title] = "" + end + + it 'renders edit when merge request is invalid' do + put :update, params + + expect(response).to render_template(:edit) + end + end +end -- cgit v1.2.3 From 009a323dc2064e0fe4f00523df4fb17e89134f6e Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 24 Feb 2017 18:20:38 +0000 Subject: Adds confirmation for cancel button --- .../vue_pipelines_index/pipeline_actions.js.es6 | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 index 54e8f977a47..c953a589456 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 @@ -1,5 +1,5 @@ /* global Vue, Flash, gl */ -/* eslint-disable no-param-reassign */ +/* eslint-disable no-param-reassign, no-alert */ ((gl) => { gl.VuePipelineActions = Vue.extend({ @@ -16,6 +16,20 @@ download(name) { return `Download ${name} artifacts`; }, + + /** + * Shows a dialog when the user clicks in the cancel button. + * We need to prevent the default behavior and stop propagation because the + * link relies on UJS. + * + * @param {Event} event + */ + confirmAction(event) { + if (!confirm('Are you sure you want to cancel this pipeline?')) { + event.preventDefault(); + event.stopPropagation(); + } + }, }, template: ` @@ -87,6 +101,7 @@ Date: Fri, 24 Feb 2017 18:35:25 +0000 Subject: Adds loader to load SVG --- config/webpack.config.js | 4 ++++ package.json | 1 + yarn.lock | 8 ++++++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/config/webpack.config.js b/config/webpack.config.js index e754f68553a..a754cdd1fd1 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -63,6 +63,10 @@ var config = { 'stage-2' ] } + }, + { + test: /\.svg$/, + use: 'raw-loader' } ] }, diff --git a/package.json b/package.json index 66aa7e9fe5d..ddbf4a3293c 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "js-cookie": "^2.1.3", "mousetrap": "^1.4.6", "pikaday": "^1.5.1", + "raw-loader": "^0.5.1", "select2": "3.5.2-browserify", "stats-webpack-plugin": "^0.4.3", "timeago.js": "^2.0.5", diff --git a/yarn.lock b/yarn.lock index 1eaa04e21c1..565fe687a36 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25,7 +25,7 @@ acorn-jsx@^3.0.0: dependencies: acorn "^3.0.4" -acorn@4.0.4, acorn@^4.0.3, acorn@^4.0.4: +acorn@4.0.4, acorn@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.4.tgz#17a8d6a7a6c4ef538b814ec9abac2779293bf30a" @@ -33,7 +33,7 @@ acorn@^3.0.4: version "3.3.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" -acorn@^4.0.11: +acorn@^4.0.11, acorn@^4.0.3: version "4.0.11" resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.11.tgz#edcda3bd937e7556410d42ed5860f67399c794c0" @@ -3566,6 +3566,10 @@ raw-body@~2.2.0: iconv-lite "0.4.15" unpipe "1.0.0" +raw-loader@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa" + rc@~1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/rc/-/rc-1.1.6.tgz#43651b76b6ae53b5c802f1151fa3fc3b059969c9" -- cgit v1.2.3 From e50375298e1f799e60f5a2e4ee3eb006d1a008a5 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 24 Feb 2017 18:47:17 +0000 Subject: Loads SVGs into JS for environments --- .../environments/components/environment.js.es6 | 8 +------- .../components/environment_actions.js.es6 | 8 ++++---- .../environments/components/environment_item.js.es6 | 17 ----------------- .../components/environment_terminal_button.js.es6 | 9 +++++---- .../components/environments_table.js.es6 | 20 +------------------- .../javascripts/vue_shared/components/commit.js.es6 | 10 +++++----- app/views/projects/environments/index.html.haml | 5 +---- 7 files changed, 17 insertions(+), 60 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index 4b700a39d44..a5a6514f4df 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -35,9 +35,6 @@ module.exports = Vue.component('environment-component', { projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath, newEnvironmentPath: environmentsData.newEnvironmentPath, helpPagePath: environmentsData.helpPagePath, - commitIconSvg: environmentsData.commitIconSvg, - playIconSvg: environmentsData.playIconSvg, - terminalIconSvg: environmentsData.terminalIconSvg, // Pagination Properties, paginationInformation: {}, @@ -176,10 +173,7 @@ module.exports = Vue.component('environment-component', { + :can-read-environment="canReadEnvironmentParsed"> [], }, + }, - playIconSvg: { - type: String, - required: false, - }, + data() { + return { playIconSvg }; }, template: ` diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index ad9d1d21a79..97a4d4111f1 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -46,21 +46,6 @@ module.exports = Vue.component('environment-item', { required: false, default: false, }, - - commitIconSvg: { - type: String, - required: false, - }, - - playIconSvg: { - type: String, - required: false, - }, - - terminalIconSvg: { - type: String, - required: false, - }, }, computed: { @@ -507,7 +492,6 @@ module.exports = Vue.component('environment-item', {
    @@ -520,7 +504,6 @@ module.exports = Vue.component('environment-item', { diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 index 481e0d15e7a..fbaf78d6e36 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 +++ b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 @@ -3,6 +3,7 @@ * Used in environments table. */ const Vue = require('vue'); +const terminalIconSvg = require('../../../../views/shared/icons/_icon_terminal.svg'); module.exports = Vue.component('terminal-button-component', { props: { @@ -10,10 +11,10 @@ module.exports = Vue.component('terminal-button-component', { type: String, default: '', }, - terminalIconSvg: { - type: String, - default: '', - }, + }, + + data() { + return { terminalIconSvg }; }, template: ` diff --git a/app/assets/javascripts/environments/components/environments_table.js.es6 b/app/assets/javascripts/environments/components/environments_table.js.es6 index fd35d77fd3d..4df4e33b7a1 100644 --- a/app/assets/javascripts/environments/components/environments_table.js.es6 +++ b/app/assets/javascripts/environments/components/environments_table.js.es6 @@ -28,21 +28,6 @@ module.exports = Vue.component('environment-table-component', { required: false, default: false, }, - - commitIconSvg: { - type: String, - required: false, - }, - - playIconSvg: { - type: String, - required: false, - }, - - terminalIconSvg: { - type: String, - required: false, - }, }, template: ` @@ -63,10 +48,7 @@ module.exports = Vue.component('environment-table-component', { + :can-read-environment="canReadEnvironment"> diff --git a/app/assets/javascripts/vue_shared/components/commit.js.es6 b/app/assets/javascripts/vue_shared/components/commit.js.es6 index ff88e236829..89415fd4f1c 100644 --- a/app/assets/javascripts/vue_shared/components/commit.js.es6 +++ b/app/assets/javascripts/vue_shared/components/commit.js.es6 @@ -1,5 +1,6 @@ /* global Vue */ window.Vue = require('vue'); +const commitIconSvg = require('../../../../views/shared/icons/_icon_commit.svg'); (() => { window.gl = window.gl || {}; @@ -69,11 +70,6 @@ window.Vue = require('vue'); required: false, default: () => ({}), }, - - commitIconSvg: { - type: String, - required: false, - }, }, computed: { @@ -116,6 +112,10 @@ window.Vue = require('vue'); }, }, + data() { + return { commitIconSvg }; + }, + template: `
    diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 1f27d41ddd9..d6366b57957 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -13,7 +13,4 @@ "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, - "commit-icon-svg" => custom_icon("icon_commit"), - "terminal-icon-svg" => custom_icon("icon_terminal"), - "play-icon-svg" => custom_icon("icon_play") } } + "css-class" => container_class } } -- cgit v1.2.3 From 7951b8469d81f58132f69ad3a1e71fbd39ef1f49 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 24 Feb 2017 23:11:50 +0530 Subject: Document U2F limitations with multiple hostnames/FQDNs. --- .../28277-document-u2f-limitations-with-multiple-urls.yml | 4 ++++ doc/user/profile/account/two_factor_authentication.md | 11 +++++++++++ 2 files changed, 15 insertions(+) create mode 100644 changelogs/unreleased/28277-document-u2f-limitations-with-multiple-urls.yml diff --git a/changelogs/unreleased/28277-document-u2f-limitations-with-multiple-urls.yml b/changelogs/unreleased/28277-document-u2f-limitations-with-multiple-urls.yml new file mode 100644 index 00000000000..6e3cd8a60d8 --- /dev/null +++ b/changelogs/unreleased/28277-document-u2f-limitations-with-multiple-urls.yml @@ -0,0 +1,4 @@ +--- +title: Document U2F limitations with multiple URLs +merge_request: 9300 +author: diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md index eaa39a0c4ea..63a3d3c472e 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -215,3 +215,14 @@ you may have cases where authorization always fails because of time differences. [Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en [FreeOTP]: https://freeotp.github.io/ [YubiKey]: https://www.yubico.com/products/yubikey-hardware/ + +- The GitLab U2F implementation does _not_ work when the GitLab instance is accessed from +multiple hostnames, or FQDNs. Each U2F registration is linked to the _current hostname_ at +the time of registration, and cannot be used for other hostnames/FQDNs. + + For example, if a user is trying to access a GitLab instance from `first.host.xyz` and `second.host.xyz`: + + - The user logs in via `first.host.xyz` and registers their U2F key. + - The user logs out and attempts to log in via `first.host.xyz` - U2F authentication suceeds. + - The user logs out and attempts to log in via `second.host.xyz` - U2F authentication fails, because + the U2F key has only been registered on `first.host.xyz`. -- cgit v1.2.3 From 0394055112fc0fe947aa13bc049f03a0dc1db0d1 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Fri, 24 Feb 2017 20:13:27 +0100 Subject: API: Return 400 for all validation erros in the mebers API --- changelogs/unreleased/unified-member-api-response.yml | 4 ++++ doc/api/v3_to_v4.md | 3 ++- lib/api/members.rb | 7 ------- spec/requests/api/members_spec.rb | 10 +++++----- 4 files changed, 11 insertions(+), 13 deletions(-) create mode 100644 changelogs/unreleased/unified-member-api-response.yml diff --git a/changelogs/unreleased/unified-member-api-response.yml b/changelogs/unreleased/unified-member-api-response.yml new file mode 100644 index 00000000000..0a60b4d46a3 --- /dev/null +++ b/changelogs/unreleased/unified-member-api-response.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Return 400 for all validation erros in the mebers API' +merge_request: 9523 +author: Robert Schilling diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 8af041be234..c178e224cc5 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -41,5 +41,6 @@ changes are in V4: - Renamed `branch_name` to `branch` on DELETE `id/repository/branches/:branch` response [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936) - Remove `public` param from create and edit actions of projects [!8736](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8736) - Notes do not return deprecated field `upvote` and `downvote` [!9384](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9384) +- Return HTTP status code `400` for all validation errors when creating or updating a member instead of sometimes `422` error. [!9523](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9523) - Remove `GET /groups/owned`. Use `GET /groups?owned=true` instead [!9505](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9505) -- Return 202 with JSON body on async removals on V4 API (DELETE `/projects/:id/repository/merged_branches` and DELETE `/projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449) \ No newline at end of file +- Return 202 with JSON body on async removals on V4 API (DELETE `/projects/:id/repository/merged_branches` and DELETE `/projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449) diff --git a/lib/api/members.rb b/lib/api/members.rb index 8360c007005..5f6913d1a27 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -55,7 +55,6 @@ module API authorize_admin_source!(source_type, source) member = source.members.find_by(user_id: params[:user_id]) - conflict!('Member already exists') if member member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at]) @@ -63,9 +62,6 @@ module API if member.persisted? && member.valid? present member.user, with: Entities::Member, member: member else - # This is to ensure back-compatibility but 400 behavior should be used - # for all validation errors in 9.0! - render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level) render_validation_error!(member) end end @@ -87,9 +83,6 @@ module API if member.update_attributes(declared_params(include_missing: false)) present member.user, with: Entities::Member, member: member else - # This is to ensure back-compatibility but 400 behavior should be used - # for all validation errors in 9.0! - render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level) render_validation_error!(member) end end diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index 31166b50033..127498ed109 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -173,11 +173,11 @@ describe API::Members, api: true do expect(response).to have_http_status(400) end - it 'returns 422 when access_level is not valid' do + it 'returns 400 when access_level is not valid' do post api("/#{source_type.pluralize}/#{source.id}/members", master), user_id: stranger.id, access_level: 1234 - expect(response).to have_http_status(422) + expect(response).to have_http_status(400) end end end @@ -230,11 +230,11 @@ describe API::Members, api: true do expect(response).to have_http_status(400) end - it 'returns 422 when access level is not valid' do + it 'returns 400 when access level is not valid' do put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master), access_level: 1234 - expect(response).to have_http_status(422) + expect(response).to have_http_status(400) end end end @@ -342,7 +342,7 @@ describe API::Members, api: true do post api("/projects/#{project.id}/members", master), user_id: stranger.id, access_level: Member::OWNER - expect(response).to have_http_status(422) + expect(response).to have_http_status(400) end.to change { project.members.count }.by(0) end end -- cgit v1.2.3 From 705f15587bfbfde33ae10f7820e66370a054ad5a Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 24 Feb 2017 14:11:10 -0600 Subject: Fix spec --- app/models/repository.rb | 98 ++++++++---------------------------- app/services/files/create_service.rb | 2 +- app/services/files/multi_service.rb | 6 +-- app/services/files/update_service.rb | 2 +- 4 files changed, 26 insertions(+), 82 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 3bf89ee52a3..1762118278a 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -746,99 +746,43 @@ class Repository @tags ||= raw_repository.tags end - # rubocop:disable Metrics/ParameterLists - def create_dir( - user, path, - message:, branch_name:, - author_email: nil, author_name: nil, - start_branch_name: nil, start_project: project) + def create_dir(user, path, **options) + options[:user] = user + options[:actions] = [{ action: :create_dir, file_path: path }] - multi_action( - user: user, - message: message, - branch_name: branch_name, - author_email: author_email, - author_name: author_name, - start_branch_name: start_branch_name, - start_project: start_project, - actions: [{ action: :create_dir, - file_path: path }]) + multi_action(**options) end - # rubocop:enable Metrics/ParameterLists - # rubocop:disable Metrics/ParameterLists - def create_file( - user, path, content, - message:, branch_name:, - author_email: nil, author_name: nil, - start_branch_name: nil, start_project: project) + def create_file(user, path, content, **options) + options[:user] = user + options[:actions] = [{ action: :create, file_path: path, content: content }] - multi_action( - user: user, - message: message, - branch_name: branch_name, - author_email: author_email, - author_name: author_name, - start_branch_name: start_branch_name, - start_project: start_project, - actions: [{ action: :create, - file_path: path, - content: content }]) + multi_action(**options) end - # rubocop:enable Metrics/ParameterLists - # rubocop:disable Metrics/ParameterLists - def update_file( - user, path, content, - message:, branch_name:, previous_path: nil, - author_email: nil, author_name: nil, - start_branch_name: nil, start_project: project) - action = if previous_path && previous_path != path - :move - else - :update - end - - multi_action( - user: user, - message: message, - branch_name: branch_name, - author_email: author_email, - author_name: author_name, - start_branch_name: start_branch_name, - start_project: start_project, - actions: [{ action: action, - file_path: path, - content: content, - previous_path: previous_path }]) + def update_file(user, path, content, **options) + previous_path = options.delete(:previous_path) + action = previous_path && previous_path != path ? :move : :update + + options[:user] = user + options[:actions] = [{ action: action, file_path: path, previous_path: previous_path, content: content }] + + multi_action(**options) end - # rubocop:enable Metrics/ParameterLists - # rubocop:disable Metrics/ParameterLists - def delete_file( - user, path, - message:, branch_name:, - author_email: nil, author_name: nil, - start_branch_name: nil, start_project: project) + def delete_file(user, path, **options) + options[:user] = user + options[:actions] = [{ action: :delete, file_path: path }] - multi_action( - user: user, - message: message, - branch_name: branch_name, - author_email: author_email, - author_name: author_name, - start_branch_name: start_branch_name, - start_project: start_project, - actions: [{ action: :delete, - file_path: path }]) + multi_action(**options) end - # rubocop:enable Metrics/ParameterLists # rubocop:disable Metrics/ParameterLists def multi_action( user:, branch_name:, message:, actions:, author_email: nil, author_name: nil, start_branch_name: nil, start_project: project) + GitOperationService.new(user, self).with_branch( branch_name, start_branch_name: start_branch_name, diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index ec45a60eda1..65b5537fb68 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -16,7 +16,7 @@ module Files def validate super - if @file_content.empty? + if @file_content.nil? raise_error("You must provide content.") end diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb index ba99af00e96..0609c6219e7 100644 --- a/app/services/files/multi_service.rb +++ b/app/services/files/multi_service.rb @@ -21,7 +21,7 @@ module Files def validate super - + params[:actions].each_with_index do |action, index| if ACTIONS.include?(action[:action].to_s) action[:action] = action[:action].to_sym @@ -102,13 +102,13 @@ module Files raise_error("Your changes could not be committed because a file with the name `#{action[:file_path]}` already exists.") end - if action[:content].empty? + if action[:content].nil? raise_error("You must provide content.") end end def validate_update(action) - if action[:content].empty? + if action[:content].nil? raise_error("You must provide content.") end diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index 462de06b6ff..54e1aaf3f67 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -18,7 +18,7 @@ module Files def validate super - if @file_content.empty? + if @file_content.nil? raise_error("You must provide content.") end -- cgit v1.2.3 From a8c62dfe5c01ed08f170c1d41a39d5167b09631b Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 23 Feb 2017 16:54:25 -0500 Subject: Minor refactoring of Uploaders - Moves a duplicate `file_storage?` definition into the common `GitlabUploader` ancestor. - Get the `uploads` base directory from a class method rather than hard-coding it where it's needed. This will be used in a subsequent MR to store Uploads in the database. - Improves the specs for uploaders. --- app/uploaders/artifact_uploader.rb | 4 --- app/uploaders/attachment_uploader.rb | 2 +- app/uploaders/avatar_uploader.rb | 2 +- app/uploaders/file_uploader.rb | 25 ++++++++-------- app/uploaders/gitlab_uploader.rb | 10 +++++++ app/uploaders/uploader_helper.rb | 6 ++-- spec/uploaders/attachment_uploader_spec.rb | 7 ++--- spec/uploaders/avatar_uploader_spec.rb | 7 ++--- spec/uploaders/file_uploader_spec.rb | 46 ++++++++---------------------- spec/uploaders/uploader_helper_spec.rb | 35 +++++++++++++++++++++++ 10 files changed, 79 insertions(+), 65 deletions(-) create mode 100644 spec/uploaders/uploader_helper_spec.rb diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb index 86f317dcd18..e84944ed411 100644 --- a/app/uploaders/artifact_uploader.rb +++ b/app/uploaders/artifact_uploader.rb @@ -27,10 +27,6 @@ class ArtifactUploader < GitlabUploader File.join(self.class.artifacts_cache_path, @build.artifacts_path) end - def file_storage? - self.class.storage == CarrierWave::Storage::File - end - def filename file.try(:filename) end diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb index cfcb877cc3e..6aa1f5a8c50 100644 --- a/app/uploaders/attachment_uploader.rb +++ b/app/uploaders/attachment_uploader.rb @@ -4,6 +4,6 @@ class AttachmentUploader < GitlabUploader storage :file def store_dir - "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" + "#{base_dir}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end end diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb index 265cea2d2c6..b4c393c6f2c 100644 --- a/app/uploaders/avatar_uploader.rb +++ b/app/uploaders/avatar_uploader.rb @@ -4,7 +4,7 @@ class AvatarUploader < GitlabUploader storage :file def store_dir - "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" + "#{base_dir}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end def exists? diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 23b7318827c..0d2edaeff3b 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -4,15 +4,12 @@ class FileUploader < GitlabUploader storage :file - attr_accessor :project, :secret + attr_accessor :project + attr_reader :secret def initialize(project, secret = nil) @project = project - @secret = secret || self.class.generate_secret - end - - def base_dir - "uploads" + @secret = secret || generate_secret end def store_dir @@ -23,10 +20,6 @@ class FileUploader < GitlabUploader File.join(base_dir, 'tmp', @project.path_with_namespace, @secret) end - def secure_url - File.join("/uploads", @secret, file.filename) - end - def to_markdown to_h[:markdown] end @@ -35,17 +28,23 @@ class FileUploader < GitlabUploader filename = image_or_video? ? self.file.basename : self.file.filename escaped_filename = filename.gsub("]", "\\]") - markdown = "[#{escaped_filename}](#{self.secure_url})" + markdown = "[#{escaped_filename}](#{secure_url})" markdown.prepend("!") if image_or_video? || dangerous? { alt: filename, - url: self.secure_url, + url: secure_url, markdown: markdown } end - def self.generate_secret + private + + def generate_secret SecureRandom.hex end + + def secure_url + File.join('/uploads', @secret, file.filename) + end end diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb index 02d7c601d6c..bd7de4ed562 100644 --- a/app/uploaders/gitlab_uploader.rb +++ b/app/uploaders/gitlab_uploader.rb @@ -1,4 +1,14 @@ class GitlabUploader < CarrierWave::Uploader::Base + def self.base_dir + 'uploads' + end + + delegate :base_dir, to: :class + + def file_storage? + self.class.storage == CarrierWave::Storage::File + end + # Reduce disk IO def move_to_cache true diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb index bee311583ea..7635c20ab3a 100644 --- a/app/uploaders/uploader_helper.rb +++ b/app/uploaders/uploader_helper.rb @@ -27,6 +27,8 @@ module UploaderHelper extension_match?(DANGEROUS_EXT) end + private + def extension_match?(extensions) return false unless file @@ -40,8 +42,4 @@ module UploaderHelper extensions.include?(extension.downcase) end - - def file_storage? - self.class.storage == CarrierWave::Storage::File - end end diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb index 6098be5cd45..ea714fb08f0 100644 --- a/spec/uploaders/attachment_uploader_spec.rb +++ b/spec/uploaders/attachment_uploader_spec.rb @@ -1,18 +1,17 @@ require 'spec_helper' describe AttachmentUploader do - let(:issue) { build(:issue) } - subject { described_class.new(issue) } + let(:uploader) { described_class.new(build_stubbed(:user)) } describe '#move_to_cache' do it 'is true' do - expect(subject.move_to_cache).to eq(true) + expect(uploader.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) + expect(uploader.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 index 76f5a4b42ed..c4d558805ab 100644 --- a/spec/uploaders/avatar_uploader_spec.rb +++ b/spec/uploaders/avatar_uploader_spec.rb @@ -1,18 +1,17 @@ require 'spec_helper' describe AvatarUploader do - let(:user) { build(:user) } - subject { described_class.new(user) } + let(:uploader) { described_class.new(build_stubbed(:user)) } describe '#move_to_cache' do it 'is false' do - expect(subject.move_to_cache).to eq(false) + expect(uploader.move_to_cache).to eq(false) end end describe '#move_to_store' do it 'is false' do - expect(subject.move_to_store).to eq(false) + expect(uploader.move_to_store).to eq(false) end end end diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb index 6a712e33c96..b0f5be55c33 100644 --- a/spec/uploaders/file_uploader_spec.rb +++ b/spec/uploaders/file_uploader_spec.rb @@ -1,57 +1,35 @@ require 'spec_helper' describe FileUploader do - let(:project) { create(:project) } + let(:uploader) { described_class.new(build_stubbed(:project)) } - before do - @previous_enable_processing = FileUploader.enable_processing - FileUploader.enable_processing = false - @uploader = FileUploader.new(project) - end - - after do - FileUploader.enable_processing = @previous_enable_processing - @uploader.remove! - end + describe 'initialize' do + it 'generates a secret if none is provided' do + expect(SecureRandom).to receive(:hex).and_return('secret') - describe '#image_or_video?' do - context 'given an image file' do - before do - @uploader.store!(fixture_file_upload(Rails.root.join('spec', 'fixtures', 'rails_sample.jpg'))) - end + uploader = described_class.new(double) - it 'detects an image based on file extension' do - expect(@uploader.image_or_video?).to be true - end + expect(uploader.secret).to eq 'secret' end - context 'given an video file' do - before do - video_file = fixture_file_upload(Rails.root.join('spec', 'fixtures', 'video_sample.mp4')) - @uploader.store!(video_file) - end - - it 'detects a video based on file extension' do - expect(@uploader.image_or_video?).to be true - end - end + it 'accepts a secret parameter' do + expect(SecureRandom).not_to receive(:hex) - it 'does not return image_or_video? for other types' do - @uploader.store!(fixture_file_upload(Rails.root.join('spec', 'fixtures', 'doc_sample.txt'))) + uploader = described_class.new(double, 'secret') - expect(@uploader.image_or_video?).to be false + expect(uploader.secret).to eq 'secret' end end describe '#move_to_cache' do it 'is true' do - expect(@uploader.move_to_cache).to eq(true) + 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) + expect(uploader.move_to_store).to eq(true) end end end diff --git a/spec/uploaders/uploader_helper_spec.rb b/spec/uploaders/uploader_helper_spec.rb new file mode 100644 index 00000000000..e9efd13b9aa --- /dev/null +++ b/spec/uploaders/uploader_helper_spec.rb @@ -0,0 +1,35 @@ +require 'rails_helper' + +describe UploaderHelper do + class ExampleUploader < CarrierWave::Uploader::Base + include UploaderHelper + + storage :file + end + + def upload_fixture(filename) + fixture_file_upload(Rails.root.join('spec', 'fixtures', filename)) + end + + describe '#image_or_video?' do + let(:uploader) { ExampleUploader.new } + + it 'returns true for an image file' do + uploader.store!(upload_fixture('dk.png')) + + expect(uploader).to be_image_or_video + end + + it 'it returns true for a video file' do + uploader.store!(upload_fixture('video_sample.mp4')) + + expect(uploader).to be_image_or_video + end + + it 'returns false for other extensions' do + uploader.store!(upload_fixture('doc_sample.txt')) + + expect(uploader).not_to be_image_or_video + end + end +end -- cgit v1.2.3 From 2858bc20378907325cb2f9487f31d335a5568677 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 24 Feb 2017 16:37:32 -0500 Subject: Add feature specs for three types of user uploads --- spec/features/issues_spec.rb | 32 ++----------------- .../uploads/user_uploads_avatar_to_group_spec.rb | 26 +++++++++++++++ .../uploads/user_uploads_avatar_to_profile_spec.rb | 24 ++++++++++++++ .../uploads/user_uploads_file_to_note_spec.rb | 22 +++++++++++++ spec/support/dropzone_helper.rb | 37 ++++++++++++++++++++++ 5 files changed, 112 insertions(+), 29 deletions(-) create mode 100644 spec/features/uploads/user_uploads_avatar_to_group_spec.rb create mode 100644 spec/features/uploads/user_uploads_avatar_to_profile_spec.rb create mode 100644 spec/features/uploads/user_uploads_file_to_note_spec.rb create mode 100644 spec/support/dropzone_helper.rb diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 1e0db4a0499..1c8267b1593 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe 'Issues', feature: true do + include DropzoneHelper include IssueHelpers include SortingHelper include WaitForAjax @@ -570,19 +571,13 @@ describe 'Issues', feature: true do end it 'uploads file when dragging into textarea' do - drop_in_dropzone test_image_file - - # Wait for the file to upload - sleep 1 + dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') expect(page.find_field("issue_description").value).to have_content 'banana_sample' end it 'adds double newline to end of attachment markdown' do - drop_in_dropzone test_image_file - - # Wait for the file to upload - sleep 1 + dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') expect(page.find_field("issue_description").value).to match /\n\n$/ end @@ -665,25 +660,4 @@ describe 'Issues', feature: true do end end end - - def drop_in_dropzone(file_path) - # Generate a fake input selector - page.execute_script <<-JS - var fakeFileInput = window.$('').attr( - {id: 'fakeFileInput', type: 'file'} - ).appendTo('body'); - JS - # Attach the file to the fake input selector with Capybara - attach_file("fakeFileInput", file_path) - # Add the file to a fileList array and trigger the fake drop event - page.execute_script <<-JS - var fileList = [$('#fakeFileInput')[0].files[0]]; - var e = jQuery.Event('drop', { dataTransfer : { files : fileList } }); - $('.div-dropzone')[0].dropzone.listeners[0].events.drop(e); - JS - end - - def test_image_file - File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') - end end diff --git a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb new file mode 100644 index 00000000000..f88a515f7fc --- /dev/null +++ b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb @@ -0,0 +1,26 @@ +require 'rails_helper' + +feature 'User uploads avatar to group', feature: true do + scenario 'they see the new avatar' do + user = create(:user) + group = create(:group) + group.add_owner(user) + login_as(user) + + visit edit_group_path(group) + attach_file( + 'group_avatar', + Rails.root.join('spec', 'fixtures', 'dk.png'), + visible: false + ) + + click_button 'Save group' + + visit group_path(group) + + expect(page).to have_selector(%Q(img[src$="/uploads/group/avatar/#{group.id}/dk.png"])) + + # Cheating here to verify something that isn't user-facing, but is important + expect(group.reload.avatar.file).to exist + end +end diff --git a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb new file mode 100644 index 00000000000..0dfd29045e5 --- /dev/null +++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +feature 'User uploads avatar to profile', feature: true do + scenario 'they see their new avatar' do + user = create(:user) + login_as(user) + + visit profile_path + attach_file( + 'user_avatar', + Rails.root.join('spec', 'fixtures', 'dk.png'), + visible: false + ) + + click_button 'Update profile settings' + + visit user_path(user) + + expect(page).to have_selector(%Q(img[src$="/uploads/user/avatar/#{user.id}/dk.png"])) + + # Cheating here to verify something that isn't user-facing, but is important + expect(user.reload.avatar.file).to exist + end +end diff --git a/spec/features/uploads/user_uploads_file_to_note_spec.rb b/spec/features/uploads/user_uploads_file_to_note_spec.rb new file mode 100644 index 00000000000..0c160dd74b4 --- /dev/null +++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +feature 'User uploads file to note', feature: true do + include DropzoneHelper + + let(:user) { create(:user) } + let(:project) { create(:empty_project, creator: user, namespace: user.namespace) } + + scenario 'they see the attached file', js: true do + issue = create(:issue, project: project, author: user) + + login_as(user) + visit namespace_project_issue_path(project.namespace, project, issue) + + dropzone_file(Rails.root.join('spec', 'fixtures', 'dk.png')) + click_button 'Comment' + wait_for_ajax + + expect(find('a.no-attachment-icon img[alt="dk"]')['src']) + .to match(%r{/#{project.full_path}/uploads/\h{32}/dk\.png$}) + end +end diff --git a/spec/support/dropzone_helper.rb b/spec/support/dropzone_helper.rb new file mode 100644 index 00000000000..984ec7d2741 --- /dev/null +++ b/spec/support/dropzone_helper.rb @@ -0,0 +1,37 @@ +module DropzoneHelper + # Provides a way to perform `attach_file` for a Dropzone-based file input + # + # This is accomplished by creating a standard HTML file input on the page, + # performing `attach_file` on that field, and then triggering the appropriate + # Dropzone events to perform the actual upload. + # + # This method waits for the upload to complete before returning. + def dropzone_file(file_path) + # Generate a fake file input that Capybara can attach to + page.execute_script <<-JS.strip_heredoc + var fakeFileInput = window.$('').attr( + {id: 'fakeFileInput', type: 'file'} + ).appendTo('body'); + + window._dropzoneComplete = false; + JS + + # Attach the file to the fake input selector with Capybara + attach_file('fakeFileInput', file_path) + + # Manually trigger a Dropzone "drop" event with the fake input's file list + page.execute_script <<-JS.strip_heredoc + var fileList = [$('#fakeFileInput')[0].files[0]]; + var e = jQuery.Event('drop', { dataTransfer : { files : fileList } }); + + var dropzone = $('.div-dropzone')[0].dropzone; + dropzone.on('queuecomplete', function() { + window._dropzoneComplete = true; + }); + dropzone.listeners[0].events.drop(e); + JS + + # Wait until Dropzone's fired `queuecomplete` + loop until page.evaluate_script('window._dropzoneComplete === true') + end +end -- cgit v1.2.3 From 043e9404855959dc81ea8bd3b9cfebef9658d337 Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Fri, 24 Feb 2017 22:42:11 +0000 Subject: remove duplicate backup skip details [skip ci] --- doc/raketasks/backup_restore.md | 32 ++++++++------------------------ 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index a5b8cd6455c..6be398922d3 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -38,23 +38,6 @@ If you are running GitLab within a Docker container, you can run the backup from docker exec -t gitlab-rake gitlab:backup:create ``` -You can specify that portions of the application data be skipped using the -environment variable `SKIP`. You can skip: - -- `db` (database) -- `uploads` (attachments) -- `repositories` (Git repositories data) -- `builds` (CI job output logs) -- `artifacts` (CI job artifacts) -- `lfs` (LFS objects) -- `registry` (Container Registry images) - -Separate multiple data types to skip using a comma. For example: - -``` -sudo gitlab-rake gitlab:backup:create SKIP=db,uploads -``` - Example output: ``` @@ -111,13 +94,14 @@ To use the `copy` strategy instead of the default streaming strategy, specify You can choose what should be backed up by adding the environment variable `SKIP`. The available options are: -* `db` -* `uploads` (attachments) -* `repositories` -* `builds` (CI build output logs) -* `artifacts` (CI build artifacts) -* `lfs` (LFS objects) -* `pages` (pages content) +- `db` (database) +- `uploads` (attachments) +- `repositories` (Git repositories data) +- `builds` (CI job output logs) +- `artifacts` (CI job artifacts) +- `lfs` (LFS objects) +- `registry` (Container Registry images) +- `pages` (Pages content) Use a comma to specify several options at the same time: -- cgit v1.2.3 From 1286151578af69e88d267d0dc3294ef1ed0062c2 Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira Date: Fri, 24 Feb 2017 16:56:34 -0300 Subject: Add performance query regression fix for !9088 affecting #27267 --- app/models/event.rb | 2 +- .../unreleased/27267-events-project-query-performance-regression.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/27267-events-project-query-performance-regression.yml diff --git a/app/models/event.rb b/app/models/event.rb index 4b8eac9accf..d7ca8e3c599 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -36,7 +36,7 @@ class Event < ActiveRecord::Base scope :code_push, -> { where(action: PUSHED) } scope :in_projects, ->(projects) do - where(project_id: projects).recent + where(project_id: projects.pluck(:id)).recent end scope :with_associations, -> { includes(:author, :project, project: :namespace).preload(:target) } diff --git a/changelogs/unreleased/27267-events-project-query-performance-regression.yml b/changelogs/unreleased/27267-events-project-query-performance-regression.yml new file mode 100644 index 00000000000..a1697b57eac --- /dev/null +++ b/changelogs/unreleased/27267-events-project-query-performance-regression.yml @@ -0,0 +1,4 @@ +--- +title: 'Add performance query regression fix for !9088 affecting #27267' +merge_request: +author: -- cgit v1.2.3 From f17109c04610489cc7fd1f760e83b2bca3c56f3c Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 24 Feb 2017 19:02:57 +0000 Subject: Load SVGs into Pipelines --- .../javascripts/vue_pipelines_index/index.js.es6 | 11 +----- .../vue_pipelines_index/pipeline_actions.js.es6 | 12 ++++-- .../vue_pipelines_index/pipelines.js.es6 | 7 +--- .../javascripts/vue_pipelines_index/stage.js.es6 | 42 ++++++++++++-------- .../javascripts/vue_pipelines_index/status.js.es6 | 38 +++++++++++++++--- .../vue_pipelines_index/time_ago.js.es6 | 7 +++- .../vue_shared/components/pipelines_table.js.es6 | 11 +----- .../components/pipelines_table_row.js.es6 | 45 +++------------------- 8 files changed, 81 insertions(+), 92 deletions(-) diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6 index e7432afb56e..a90bd1518e9 100644 --- a/app/assets/javascripts/vue_pipelines_index/index.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/index.js.es6 @@ -11,15 +11,10 @@ $(() => new Vue({ data() { const project = document.querySelector('.pipelines'); - const svgs = document.querySelector('.pipeline-svgs').dataset; - - // Transform svgs DOMStringMap to a plain Object. - const svgsObject = gl.utils.DOMStringMapToObject(svgs); return { scope: project.dataset.url, store: new gl.PipelineStore(), - svgs: svgsObject, }; }, components: { @@ -27,10 +22,8 @@ $(() => new Vue({ }, template: ` + :scope="scope" + :store="store"> `, })); diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 index 54e8f977a47..34ea7512d2b 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 @@ -1,9 +1,10 @@ /* global Vue, Flash, gl */ /* eslint-disable no-param-reassign */ +const playIconSvg = require('../../../views/shared/icons/_icon_play.svg'); ((gl) => { gl.VuePipelineActions = Vue.extend({ - props: ['pipeline', 'svgs'], + props: ['pipeline'], computed: { actions() { return this.pipeline.details.manual_actions.length > 0; @@ -17,6 +18,11 @@ return `Download ${name} artifacts`; }, }, + + data() { + return { playIconSvg }; + }, + template: `
    @@ -30,7 +36,7 @@ data-placement="top" aria-label="Manual job" > - +
    - - +
    { gl.VueStage = Vue.extend({ data() { + const svgsDictionary = { + icon_status_canceled: canceledSvg, + icon_status_created: createdSvg, + icon_status_failed: failedSvg, + icon_status_manual: manualSvg, + icon_status_pending: pendingSvg, + icon_status_running: runningSvg, + icon_status_skipped: skippedSvg, + icon_status_success: successSvg, + icon_status_warning: warningSvg, + }; + return { builds: '', spinner: '', + svg: svgsDictionary[this.stage.status.icon], }; }, + props: { stage: { type: Object, required: true, }, - svgs: { - type: Object, - required: true, - }, - match: { - type: Function, - required: true, - }, }, updated() { @@ -73,11 +88,6 @@ tooltip() { return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`; }, - svg() { - const { icon } = this.stage.status; - const stageIcon = icon.replace(/icon/i, 'stage_icon'); - return this.svgs[this.match(stageIcon)]; - }, triggerButtonClass() { return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`; }, @@ -91,8 +101,7 @@ data-placement="top" data-toggle="dropdown" type="button" - :aria-label="stage.title" - > + :aria-label="stage.title"> @@ -101,8 +110,7 @@
    + v-html="buildsOrSpinner">
    diff --git a/app/assets/javascripts/vue_pipelines_index/status.js.es6 b/app/assets/javascripts/vue_pipelines_index/status.js.es6 index 05175082704..acdd82a480e 100644 --- a/app/assets/javascripts/vue_pipelines_index/status.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/status.js.es6 @@ -1,20 +1,47 @@ /* global Vue, gl */ /* eslint-disable no-param-reassign */ +import canceledSvg from '../../../views/shared/icons/_icon_status_canceled.svg'; +import createdSvg from '../../../views/shared/icons/_icon_status_created.svg'; +import failedSvg from '../../../views/shared/icons/_icon_status_failed.svg'; +import manualSvg from '../../../views/shared/icons/_icon_status_manual.svg'; +import pendingSvg from '../../../views/shared/icons/_icon_status_pending.svg'; +import runningSvg from '../../../views/shared/icons/_icon_status_running.svg'; +import skippedSvg from '../../../views/shared/icons/_icon_status_skipped.svg'; +import successSvg from '../../../views/shared/icons/_icon_status_success.svg'; +import warningSvg from '../../../views/shared/icons/_icon_status_warning.svg'; + ((gl) => { gl.VueStatusScope = Vue.extend({ props: [ - 'pipeline', 'svgs', 'match', + 'pipeline', ], + + data() { + const svgsDictionary = { + icon_status_canceled: canceledSvg, + icon_status_created: createdSvg, + icon_status_failed: failedSvg, + icon_status_manual: manualSvg, + icon_status_pending: pendingSvg, + icon_status_running: runningSvg, + icon_status_skipped: skippedSvg, + icon_status_success: successSvg, + icon_status_warning: warningSvg, + }; + + return { + svg: svgsDictionary[this.pipeline.details.status.icon], + }; + }, + computed: { cssClasses() { const cssObject = { 'ci-status': true }; cssObject[`ci-${this.pipeline.details.status.group}`] = true; return cssObject; }, - svg() { - return this.svgs[this.match(this.pipeline.details.status.icon)]; - }, + detailsPath() { const { status } = this.pipeline.details; return status.has_details ? status.details_path : false; @@ -25,8 +52,7 @@ + v-html="svg + pipeline.details.status.text"> `, diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 index 3598da11573..22f1b1a8483 100644 --- a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 @@ -4,14 +4,17 @@ window.Vue = require('vue'); require('../lib/utils/datetime_utility'); +const iconTimerSvg = require('../../../views/shared/icons/_icon_timer.svg'); + ((gl) => { gl.VueTimeAgo = Vue.extend({ data() { return { currentTime: new Date(), + iconTimerSvg, }; }, - props: ['pipeline', 'svgs'], + props: ['pipeline'], computed: { timeAgo() { return gl.utils.getTimeago(); @@ -56,7 +59,7 @@ require('../lib/utils/datetime_utility'); template: `

    - + {{duration}}

    diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 index 4bdaef31ee9..1c41f8b437d 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 +++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 @@ -21,14 +21,6 @@ require('./pipelines_table_row'); default: () => ([]), }, - /** - * TODO: Remove this when we have webpack. - */ - svgs: { - type: Object, - required: true, - default: () => ({}), - }, }, components: { @@ -51,8 +43,7 @@ require('./pipelines_table_row'); diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 index 61c1b72d9d2..e5e88186a85 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 +++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 @@ -25,14 +25,6 @@ require('./commit'); default: () => ({}), }, - /** - * TODO: Remove this when we have webpack; - */ - svgs: { - type: Object, - required: true, - default: () => ({}), - }, }, components: { @@ -174,30 +166,9 @@ require('./commit'); }, }, - methods: { - /** - * FIXME: This should not be in this component but in the components that - * need this function. - * - * Used to render SVGs in the following components: - * - status-scope - * - dropdown-stage - * - * @param {String} string - * @return {String} - */ - match(string) { - return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase()); - }, - }, - template: ` - - + @@ -208,26 +179,20 @@ require('./commit'); :commit-url="commitUrl" :short-sha="commitShortSha" :title="commitTitle" - :author="commitAuthor" - :commit-icon-svg="svgs.commitIconSvg"> - + :author="commitAuthor"/>

    - + - + `, }); -- cgit v1.2.3 From 92d80b68bcd7c253b2cc1fab1384783f93e65661 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Sat, 25 Feb 2017 00:44:46 +0000 Subject: Use vhtml only when necessary --- app/assets/javascripts/environments/components/environment.js.es6 | 3 +-- .../javascripts/environments/components/environment_actions.js.es6 | 4 +--- .../environments/components/environment_terminal_button.js.es6 | 2 +- app/assets/javascripts/vue_shared/components/table_pagination.js.es6 | 1 - 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index a5a6514f4df..204934b673d 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -173,8 +173,7 @@ module.exports = Vue.component('environment-component', { - + :can-read-environment="canReadEnvironmentParsed"/> - - - + ${playIconSvg} {{action.name}} diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 index fbaf78d6e36..693cb852152 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 +++ b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 @@ -20,7 +20,7 @@ module.exports = Vue.component('terminal-button-component', { template: ` - + ${terminalIconSvg} `, }); diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 index d8042a9b7fc..dca4530188e 100644 --- a/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 +++ b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 @@ -19,7 +19,6 @@ window.Vue = require('vue'); /** This function will take the information given by the pagination component - And make a new Turbolinks call Here is an example `change` method: -- cgit v1.2.3 From d8ce9f3062bdcb1f69f6d47a912d446bf69d50b8 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Sat, 25 Feb 2017 00:48:19 +0000 Subject: Load svgs in cycle analytics --- .../cycle_analytics/components/stage_plan_component.js.es6 | 3 ++- .../cycle_analytics/components/stage_staging_component.js.es6 | 3 ++- .../cycle_analytics/components/stage_test_component.js.es6 | 6 ++++-- .../javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 | 3 --- app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6 | 7 ------- app/assets/javascripts/cycle_analytics/svg/icon_branch.svg | 1 + app/assets/javascripts/cycle_analytics/svg/icon_build_status.svg | 1 + app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6 | 7 ------- app/assets/javascripts/cycle_analytics/svg/icon_commit.svg | 1 + 9 files changed, 11 insertions(+), 21 deletions(-) delete mode 100644 app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/svg/icon_branch.svg create mode 100644 app/assets/javascripts/cycle_analytics/svg/icon_build_status.svg delete mode 100644 app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/svg/icon_commit.svg 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 8652479e7bf..32b13872aea 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,5 +1,6 @@ /* eslint-disable no-param-reassign */ /* global Vue */ +import iconCommit from '../svg/icon_commit.svg'; ((global) => { global.cycleAnalytics = global.cycleAnalytics || {}; @@ -31,7 +32,7 @@ First - ${global.cycleAnalytics.svgs.iconCommit} + ${iconCommit} {{ commit.shortSha }} pushed by 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 82622232f64..fd6e2516952 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,5 +1,6 @@ /* eslint-disable no-param-reassign */ /* global Vue */ +import iconBranch from '../svg/icon_branch.svg'; ((global) => { global.cycleAnalytics = global.cycleAnalytics || {}; @@ -22,7 +23,7 @@ #{{ build.id }} {{ build.branch.name }} - ${global.cycleAnalytics.svgs.iconBranch} + ${iconBranch} {{ build.shortSha }} 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 4bfd363a1f1..34729e70f5e 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,5 +1,7 @@ /* eslint-disable no-param-reassign */ /* global Vue */ +import iconBuildStatus from '../svg/icon_build_status.svg'; +import iconBranch from '../svg/icon_branch.svg'; ((global) => { global.cycleAnalytics = global.cycleAnalytics || {}; @@ -18,13 +20,13 @@
  • - ${global.cycleAnalytics.svgs.iconBuildStatus} + ${iconBuildStatus} {{ build.name }} · #{{ build.id }} {{ build.branch.name }} - ${global.cycleAnalytics.svgs.iconBranch} + ${iconBranch} {{ build.shortSha }}
    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 411ac7b24b2..beff293b587 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 @@ -4,9 +4,6 @@ window.Vue = require('vue'); window.Cookies = require('js-cookie'); -require('./svg/icon_branch'); -require('./svg/icon_build_status'); -require('./svg/icon_commit'); require('./components/stage_code_component'); require('./components/stage_issue_component'); require('./components/stage_plan_component'); diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6 deleted file mode 100644 index 5d486bcaf66..00000000000 --- a/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6 +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable no-param-reassign */ -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {}; - - global.cycleAnalytics.svgs.iconBranch = ''; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_branch.svg b/app/assets/javascripts/cycle_analytics/svg/icon_branch.svg new file mode 100644 index 00000000000..9f547d3d744 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/svg/icon_branch.svg @@ -0,0 +1 @@ + diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_build_status.svg b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.svg new file mode 100644 index 00000000000..b932d90618a --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.svg @@ -0,0 +1 @@ + diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6 deleted file mode 100644 index 2208c27a619..00000000000 --- a/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6 +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable no-param-reassign */ -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {}; - - global.cycleAnalytics.svgs.iconCommit = ''; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_commit.svg b/app/assets/javascripts/cycle_analytics/svg/icon_commit.svg new file mode 100644 index 00000000000..6a517756058 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/svg/icon_commit.svg @@ -0,0 +1 @@ + -- cgit v1.2.3 From 70f7d77d342183a7efdaf50263b5b821f9fa53c8 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Sat, 25 Feb 2017 00:55:47 +0000 Subject: remove tech debt code --- .../commit/pipelines/pipelines_table.js.es6 | 10 +-------- .../cycle_analytics/svg/icon_build_status.js.es6 | 7 ------- .../components/collapsed_state.js.es6 | 5 +++-- .../time_tracking/components/time_tracker.js.es6 | 4 +--- .../javascripts/lib/utils/common_utils.js.es6 | 11 ---------- app/views/projects/commit/_pipelines_list.haml | 21 ------------------- app/views/projects/pipelines/index.html.haml | 24 +--------------------- app/views/shared/issuable/_sidebar.html.haml | 2 +- 8 files changed, 7 insertions(+), 77 deletions(-) delete mode 100644 app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6 diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 index e7c6c063413..95e84710937 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 @@ -39,15 +39,10 @@ const PipelineStore = require('./pipelines_store'); */ data() { const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; - const svgsData = document.querySelector('.pipeline-svgs').dataset; const store = new PipelineStore(); - // Transform svgs DOMStringMap to a plain Object. - const svgsObject = gl.utils.DOMStringMapToObject(svgsData); - return { endpoint: pipelinesTableData.endpoint, - svgs: svgsObject, store, state: store.state, isLoading: false, @@ -99,10 +94,7 @@ const PipelineStore = require('./pipelines_store');
    - - +
    `, diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6 deleted file mode 100644 index 661bf9e9f1c..00000000000 --- a/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6 +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable no-param-reassign */ -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {}; - - global.cycleAnalytics.svgs.iconBuildStatus = ''; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 index bf27fbac5d7..d8097eaa7b2 100644 --- a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 +++ b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 @@ -1,4 +1,6 @@ /* global Vue */ +import stopwatchSvg from '../../../../../views/shared/icons/_icon_stopwatch.svg'; + require('../../../lib/utils/pretty_time'); (() => { @@ -11,7 +13,6 @@ require('../../../lib/utils/pretty_time'); 'showNoTimeTrackingState', 'timeSpentHumanReadable', 'timeEstimateHumanReadable', - 'stopwatchSvg', ], methods: { abbreviateTime(timeStr) { @@ -20,7 +21,7 @@ require('../../../lib/utils/pretty_time'); }, template: `
  • ${name} ${name}
  • ' + }, + // Team Members + Members: { + template: '
  • ${avatarTag} ${username} ${title}
  • ' + }, + Labels: { + template: '
  • ${title}
  • ' + }, + // Issues and MergeRequests + Issues: { + template: '
  • ${id} ${title}
  • ' + }, + // Milestones + Milestones: { + template: '
  • ${title}
  • ' + }, + Loading: { + template: '
  • Loading...
  • ' + }, + DefaultOptions: { + sorter: function(query, items, searchKey) { + this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0; + if (gl.GfmAutoComplete.isLoading(items)) { + this.setting.highlightFirst = false; + return items; + } + return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey); + }, + filter: function(query, data, searchKey) { + if (gl.GfmAutoComplete.isLoading(data)) { + gl.GfmAutoComplete.fetchData(this.$inputor, this.at); + return data; + } else { + 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 + '"'; + } + return value; + }, + matcher: function (flag, subtext) { + // 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, atSymbolsWithBar, atSymbolsWithoutBar; + atSymbolsWithBar = Object.keys(this.app.controllers).join('|'); + atSymbolsWithoutBar = Object.keys(this.app.controllers).join(''); + subtext = subtext.split(/\s+/g).pop(); + flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + + _a = decodeURI("%C3%80"); + _y = decodeURI("%C3%BF"); + + regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?!" + atSymbolsWithBar + ")((?:[A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi'); + + match = regexp.exec(subtext); + + if (match) { + return match[1]; + } else { + return null; + } + } + }, + setup: function(input) { + // Add GFM auto-completion to all input fields, that accept GFM input. + this.input = input || $('.js-gfm-input'); + this.setupLifecycle(); + }, + setupLifecycle() { + this.input.each((i, input) => { + const $input = $(input); + $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); + // This triggers at.js again + // Needed for slash commands with suffixes (ex: /label ~) + $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); + }); + }, + setupAtWho: function($input) { + // Emoji + $input.atwho({ + at: ':', + displayTpl: function(value) { + return value.path != null ? this.Emoji.template : this.Loading.template; + }.bind(this), + insertTpl: ':${name}:', + skipSpecialCharacterTest: true, + data: this.defaultLoadingData, + callbacks: { + sorter: this.DefaultOptions.sorter, + beforeInsert: this.DefaultOptions.beforeInsert, + filter: this.DefaultOptions.filter + } + }); + // Team Members + $input.atwho({ + at: '@', + displayTpl: function(value) { + return value.username != null ? this.Members.template : this.Loading.template; + }.bind(this), + insertTpl: '${atwho-at}${username}', + searchKey: 'search', + alwaysHighlightFirst: true, + skipSpecialCharacterTest: true, + data: this.defaultLoadingData, + callbacks: { + sorter: this.DefaultOptions.sorter, + filter: this.DefaultOptions.filter, + beforeInsert: this.DefaultOptions.beforeInsert, + matcher: this.DefaultOptions.matcher, + beforeSave: function(members) { + return $.map(members, function(m) { + let title = ''; + if (m.username == null) { + return m; + } + title = m.name; + if (m.count) { + title += " (" + m.count + ")"; + } + + const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase(); + const imgAvatar = `${m.username}`; + const txtAvatar = `
    ${autoCompleteAvatar}
    `; + + return { + username: m.username, + avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar, + title: sanitize(title), + search: sanitize(m.username + " " + m.name) + }; + }); + } + } + }); + $input.atwho({ + at: '#', + alias: 'issues', + searchKey: 'search', + 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, + filter: this.DefaultOptions.filter, + beforeInsert: this.DefaultOptions.beforeInsert, + matcher: this.DefaultOptions.matcher, + beforeSave: function(issues) { + return $.map(issues, function(i) { + if (i.title == null) { + return i; + } + return { + id: i.iid, + title: sanitize(i.title), + search: i.iid + " " + i.title + }; + }); + } + } + }); + $input.atwho({ + at: '%', + alias: 'milestones', + searchKey: 'search', + insertTpl: '${atwho-at}${title}', + displayTpl: function(value) { + return value.title != null ? this.Milestones.template : this.Loading.template; + }.bind(this), + 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) { + return m; + } + return { + id: m.iid, + title: sanitize(m.title), + search: "" + m.title + }; + }); + } + } + }); + $input.atwho({ + at: '!', + alias: 'mergerequests', + searchKey: 'search', + 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, + filter: this.DefaultOptions.filter, + beforeInsert: this.DefaultOptions.beforeInsert, + matcher: this.DefaultOptions.matcher, + beforeSave: function(merges) { + return $.map(merges, function(m) { + if (m.title == null) { + return m; + } + return { + id: m.iid, + title: sanitize(m.title), + search: m.iid + " " + m.title + }; + }); + } + } + }); + $input.atwho({ + at: '~', + alias: 'labels', + searchKey: 'search', + data: this.defaultLoadingData, + displayTpl: function(value) { + return this.isLoading(value) ? this.Loading.template : this.Labels.template; + }.bind(this), + insertTpl: '${atwho-at}${title}', + callbacks: { + matcher: this.DefaultOptions.matcher, + beforeInsert: this.DefaultOptions.beforeInsert, + filter: this.DefaultOptions.filter, + sorter: this.DefaultOptions.sorter, + 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), + color: m.color, + search: "" + m.title + }; + }); + } + } + }); + // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms + $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 = '
  • /${name}'; + if (value.aliases.length > 0) { + tpl += ' (or /<%- aliases.join(", /") %>)'; + } + if (value.params.length > 0) { + tpl += ' <%- params.join(" ") %>'; + } + if (value.description !== '') { + tpl += '<%- description %>'; + } + tpl += '
  • '; + return _.template(tpl)(value); + }.bind(this), + insertTpl: function(value) { + var tpl = "/${name} "; + var reference_prefix = null; + if (value.params.length > 0) { + reference_prefix = value.params[0][0]; + if (/^[@%~]/.test(reference_prefix)) { + tpl += '<%- reference_prefix %>'; + } + } + return _.template(tpl)({ reference_prefix: reference_prefix }); + }, + suffix: '', + callbacks: { + sorter: this.DefaultOptions.sorter, + 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) { + search = search + " " + c.aliases.join(" "); + } + return { + name: c.name, + aliases: c.aliases, + params: c.params, + description: c.description, + search: search + }; + }); + }, + matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) { + var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi; + var match = regexp.exec(subtext); + if (match) { + return match[1]; + } else { + return null; + } + } + } + }); + return; + }, + 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($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 $input.trigger('keyup'); + }, + isLoading(data) { + var dataToInspect = data; + if (data && data.length > 0) { + dataToInspect = data[0]; + } + + var loadingState = this.defaultLoadingData[0]; + return dataToInspect && + (dataToInspect === loadingState || dataToInspect.name === loadingState); + } + }; +}).call(window); diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 deleted file mode 100644 index 60d6658dc16..00000000000 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ /dev/null @@ -1,383 +0,0 @@ -/* 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, vars-on-top, max-len */ - -// Creates the variables for setting up GFM auto-completion -(function() { - if (window.gl == null) { - window.gl = {}; - } - - function sanitize(str) { - return str.replace(/<(?:.|\n)*?>/gm, ''); - } - - window.gl.GfmAutoComplete = { - dataSources: {}, - defaultLoadingData: ['loading'], - cachedData: {}, - isLoadingData: {}, - atTypeMap: { - ':': 'emojis', - '@': 'members', - '#': 'issues', - '!': 'mergeRequests', - '~': 'labels', - '%': 'milestones', - '/': 'commands' - }, - // Emoji - Emoji: { - template: '
  • ${name} ${name}
  • ' - }, - // Team Members - Members: { - template: '
  • ${avatarTag} ${username} ${title}
  • ' - }, - Labels: { - template: '
  • ${title}
  • ' - }, - // Issues and MergeRequests - Issues: { - template: '
  • ${id} ${title}
  • ' - }, - // Milestones - Milestones: { - template: '
  • ${title}
  • ' - }, - Loading: { - template: '
  • Loading...
  • ' - }, - DefaultOptions: { - sorter: function(query, items, searchKey) { - this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0; - if (gl.GfmAutoComplete.isLoading(items)) { - this.setting.highlightFirst = false; - return items; - } - return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey); - }, - filter: function(query, data, searchKey) { - if (gl.GfmAutoComplete.isLoading(data)) { - gl.GfmAutoComplete.fetchData(this.$inputor, this.at); - return data; - } else { - 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 + '"'; - } - return value; - }, - matcher: function (flag, subtext) { - // 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, atSymbolsWithBar, atSymbolsWithoutBar; - atSymbolsWithBar = Object.keys(this.app.controllers).join('|'); - atSymbolsWithoutBar = Object.keys(this.app.controllers).join(''); - subtext = subtext.split(/\s+/g).pop(); - flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - - _a = decodeURI("%C3%80"); - _y = decodeURI("%C3%BF"); - - regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?!" + atSymbolsWithBar + ")((?:[A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi'); - - match = regexp.exec(subtext); - - if (match) { - return match[1]; - } else { - return null; - } - } - }, - setup: function(input) { - // Add GFM auto-completion to all input fields, that accept GFM input. - this.input = input || $('.js-gfm-input'); - this.setupLifecycle(); - }, - setupLifecycle() { - this.input.each((i, input) => { - const $input = $(input); - $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); - // This triggers at.js again - // Needed for slash commands with suffixes (ex: /label ~) - $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); - }); - }, - setupAtWho: function($input) { - // Emoji - $input.atwho({ - at: ':', - displayTpl: function(value) { - return value.path != null ? this.Emoji.template : this.Loading.template; - }.bind(this), - insertTpl: ':${name}:', - skipSpecialCharacterTest: true, - data: this.defaultLoadingData, - callbacks: { - sorter: this.DefaultOptions.sorter, - beforeInsert: this.DefaultOptions.beforeInsert, - filter: this.DefaultOptions.filter - } - }); - // Team Members - $input.atwho({ - at: '@', - displayTpl: function(value) { - return value.username != null ? this.Members.template : this.Loading.template; - }.bind(this), - insertTpl: '${atwho-at}${username}', - searchKey: 'search', - alwaysHighlightFirst: true, - skipSpecialCharacterTest: true, - data: this.defaultLoadingData, - callbacks: { - sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert, - matcher: this.DefaultOptions.matcher, - beforeSave: function(members) { - return $.map(members, function(m) { - let title = ''; - if (m.username == null) { - return m; - } - title = m.name; - if (m.count) { - title += " (" + m.count + ")"; - } - - const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase(); - const imgAvatar = `${m.username}`; - const txtAvatar = `
    ${autoCompleteAvatar}
    `; - - return { - username: m.username, - avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar, - title: sanitize(title), - search: sanitize(m.username + " " + m.name) - }; - }); - } - } - }); - $input.atwho({ - at: '#', - alias: 'issues', - searchKey: 'search', - 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, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert, - matcher: this.DefaultOptions.matcher, - beforeSave: function(issues) { - return $.map(issues, function(i) { - if (i.title == null) { - return i; - } - return { - id: i.iid, - title: sanitize(i.title), - search: i.iid + " " + i.title - }; - }); - } - } - }); - $input.atwho({ - at: '%', - alias: 'milestones', - searchKey: 'search', - insertTpl: '${atwho-at}${title}', - displayTpl: function(value) { - return value.title != null ? this.Milestones.template : this.Loading.template; - }.bind(this), - 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) { - return m; - } - return { - id: m.iid, - title: sanitize(m.title), - search: "" + m.title - }; - }); - } - } - }); - $input.atwho({ - at: '!', - alias: 'mergerequests', - searchKey: 'search', - 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, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert, - matcher: this.DefaultOptions.matcher, - beforeSave: function(merges) { - return $.map(merges, function(m) { - if (m.title == null) { - return m; - } - return { - id: m.iid, - title: sanitize(m.title), - search: m.iid + " " + m.title - }; - }); - } - } - }); - $input.atwho({ - at: '~', - alias: 'labels', - searchKey: 'search', - data: this.defaultLoadingData, - displayTpl: function(value) { - return this.isLoading(value) ? this.Loading.template : this.Labels.template; - }.bind(this), - insertTpl: '${atwho-at}${title}', - callbacks: { - matcher: this.DefaultOptions.matcher, - beforeInsert: this.DefaultOptions.beforeInsert, - filter: this.DefaultOptions.filter, - sorter: this.DefaultOptions.sorter, - 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), - color: m.color, - search: "" + m.title - }; - }); - } - } - }); - // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms - $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 = '
  • /${name}'; - if (value.aliases.length > 0) { - tpl += ' (or /<%- aliases.join(", /") %>)'; - } - if (value.params.length > 0) { - tpl += ' <%- params.join(" ") %>'; - } - if (value.description !== '') { - tpl += '<%- description %>'; - } - tpl += '
  • '; - return _.template(tpl)(value); - }.bind(this), - insertTpl: function(value) { - var tpl = "/${name} "; - var reference_prefix = null; - if (value.params.length > 0) { - reference_prefix = value.params[0][0]; - if (/^[@%~]/.test(reference_prefix)) { - tpl += '<%- reference_prefix %>'; - } - } - return _.template(tpl)({ reference_prefix: reference_prefix }); - }, - suffix: '', - callbacks: { - sorter: this.DefaultOptions.sorter, - 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) { - search = search + " " + c.aliases.join(" "); - } - return { - name: c.name, - aliases: c.aliases, - params: c.params, - description: c.description, - search: search - }; - }); - }, - matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) { - var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi; - var match = regexp.exec(subtext); - if (match) { - return match[1]; - } else { - return null; - } - } - } - }); - return; - }, - 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($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 $input.trigger('keyup'); - }, - isLoading(data) { - var dataToInspect = data; - if (data && data.length > 0) { - dataToInspect = data[0]; - } - - var loadingState = this.defaultLoadingData[0]; - return dataToInspect && - (dataToInspect === loadingState || dataToInspect.name === loadingState); - } - }; -}).call(window); diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js new file mode 100644 index 00000000000..f7cbecc0385 --- /dev/null +++ b/app/assets/javascripts/gl_field_error.js @@ -0,0 +1,164 @@ +/* eslint-disable no-param-reassign */ +((global) => { + /* + * This class overrides the browser's validation error bubbles, displaying custom + * error messages for invalid fields instead. To begin validating any form, add the + * class `gl-show-field-errors` to the form element, and ensure error messages are + * declared in each inputs' `title` attribute. If no title is declared for an invalid + * field the user attempts to submit, "This field is required." will be shown by default. + * + * Opt not to validate certain fields by adding the class `gl-field-error-ignore` to the input. + * + * Set a custom error anchor for error message to be injected after with the + * class `gl-field-error-anchor` + * + * Examples: + * + * Basic: + * + *
    + * + *
    + * + * Ignore specific inputs (e.g. UsernameValidator): + * + *
    + *
    + * + *
    + *
    + * + * Custom Error Anchor (allows error message to be injected after specified element): + * + *
    + *
    + * + * // Error message typically injected here + *
    + * // Error message now injected here + *
    + * + * */ + + /* + * Regex Patterns in use: + * + * Only alphanumeric: : "[a-zA-Z0-9]+" + * No special characters : "[a-zA-Z0-9-_]+", + * + * */ + + const errorMessageClass = 'gl-field-error'; + const inputErrorClass = 'gl-field-error-outline'; + const errorAnchorSelector = '.gl-field-error-anchor'; + const ignoreInputSelector = '.gl-field-error-ignore'; + + class GlFieldError { + constructor({ input, formErrors }) { + this.inputElement = $(input); + this.inputDomElement = this.inputElement.get(0); + this.form = formErrors; + this.errorMessage = this.inputElement.attr('title') || 'This field is required.'; + this.fieldErrorElement = $(`

    ${this.errorMessage}

    `); + + this.state = { + valid: false, + empty: true, + }; + + this.initFieldValidation(); + } + + initFieldValidation() { + const customErrorAnchor = this.inputElement.parents(errorAnchorSelector); + const errorAnchor = customErrorAnchor.length ? customErrorAnchor : this.inputElement; + + // hidden when injected into DOM + errorAnchor.after(this.fieldErrorElement); + this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this)); + this.scopedSiblings = this.safelySelectSiblings(); + } + + safelySelectSiblings() { + // Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled + const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreInputSelector})`); + const parentContainer = this.inputElement.parent('.form-group'); + + // Only select siblings when they're scoped within a form-group with one input + const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1; + + return safelyScoped ? unignoredSiblings : this.fieldErrorElement; + } + + renderValidity() { + this.renderClear(); + + if (this.state.valid) { + this.renderValid(); + } else if (this.state.empty) { + this.renderEmpty(); + } else if (!this.state.valid) { + this.renderInvalid(); + } + } + + handleInvalidSubmit(event) { + event.preventDefault(); + const currentValue = this.accessCurrentValue(); + this.state.valid = false; + this.state.empty = currentValue === ''; + + this.renderValidity(); + this.form.focusOnFirstInvalid.apply(this.form); + // For UX, wait til after first invalid submission to check each keyup + this.inputElement.off('keyup.fieldValidator') + .on('keyup.fieldValidator', this.updateValidity.bind(this)); + } + + /* Get or set current input value */ + accessCurrentValue(newVal) { + return newVal ? this.inputElement.val(newVal) : this.inputElement.val(); + } + + getInputValidity() { + return this.inputDomElement.validity.valid; + } + + updateValidity() { + const inputVal = this.accessCurrentValue(); + this.state.empty = !inputVal.length; + this.state.valid = this.getInputValidity(); + this.renderValidity(); + } + + renderValid() { + return this.renderClear(); + } + + renderEmpty() { + return this.renderInvalid(); + } + + renderInvalid() { + this.inputElement.addClass(inputErrorClass); + this.scopedSiblings.hide(); + return this.fieldErrorElement.show(); + } + + renderClear() { + const inputVal = this.accessCurrentValue(); + if (!inputVal.split(' ').length) { + const trimmedInput = inputVal.trim(); + this.accessCurrentValue(trimmedInput); + } + this.inputElement.removeClass(inputErrorClass); + this.scopedSiblings.hide(); + this.fieldErrorElement.hide(); + } + } + + global.GlFieldError = GlFieldError; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/gl_field_error.js.es6 b/app/assets/javascripts/gl_field_error.js.es6 deleted file mode 100644 index f7cbecc0385..00000000000 --- a/app/assets/javascripts/gl_field_error.js.es6 +++ /dev/null @@ -1,164 +0,0 @@ -/* eslint-disable no-param-reassign */ -((global) => { - /* - * This class overrides the browser's validation error bubbles, displaying custom - * error messages for invalid fields instead. To begin validating any form, add the - * class `gl-show-field-errors` to the form element, and ensure error messages are - * declared in each inputs' `title` attribute. If no title is declared for an invalid - * field the user attempts to submit, "This field is required." will be shown by default. - * - * Opt not to validate certain fields by adding the class `gl-field-error-ignore` to the input. - * - * Set a custom error anchor for error message to be injected after with the - * class `gl-field-error-anchor` - * - * Examples: - * - * Basic: - * - *
    - * - *
    - * - * Ignore specific inputs (e.g. UsernameValidator): - * - *
    - *
    - * - *
    - *
    - * - * Custom Error Anchor (allows error message to be injected after specified element): - * - *
    - *
    - * - * // Error message typically injected here - *
    - * // Error message now injected here - *
    - * - * */ - - /* - * Regex Patterns in use: - * - * Only alphanumeric: : "[a-zA-Z0-9]+" - * No special characters : "[a-zA-Z0-9-_]+", - * - * */ - - const errorMessageClass = 'gl-field-error'; - const inputErrorClass = 'gl-field-error-outline'; - const errorAnchorSelector = '.gl-field-error-anchor'; - const ignoreInputSelector = '.gl-field-error-ignore'; - - class GlFieldError { - constructor({ input, formErrors }) { - this.inputElement = $(input); - this.inputDomElement = this.inputElement.get(0); - this.form = formErrors; - this.errorMessage = this.inputElement.attr('title') || 'This field is required.'; - this.fieldErrorElement = $(`

    ${this.errorMessage}

    `); - - this.state = { - valid: false, - empty: true, - }; - - this.initFieldValidation(); - } - - initFieldValidation() { - const customErrorAnchor = this.inputElement.parents(errorAnchorSelector); - const errorAnchor = customErrorAnchor.length ? customErrorAnchor : this.inputElement; - - // hidden when injected into DOM - errorAnchor.after(this.fieldErrorElement); - this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this)); - this.scopedSiblings = this.safelySelectSiblings(); - } - - safelySelectSiblings() { - // Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled - const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreInputSelector})`); - const parentContainer = this.inputElement.parent('.form-group'); - - // Only select siblings when they're scoped within a form-group with one input - const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1; - - return safelyScoped ? unignoredSiblings : this.fieldErrorElement; - } - - renderValidity() { - this.renderClear(); - - if (this.state.valid) { - this.renderValid(); - } else if (this.state.empty) { - this.renderEmpty(); - } else if (!this.state.valid) { - this.renderInvalid(); - } - } - - handleInvalidSubmit(event) { - event.preventDefault(); - const currentValue = this.accessCurrentValue(); - this.state.valid = false; - this.state.empty = currentValue === ''; - - this.renderValidity(); - this.form.focusOnFirstInvalid.apply(this.form); - // For UX, wait til after first invalid submission to check each keyup - this.inputElement.off('keyup.fieldValidator') - .on('keyup.fieldValidator', this.updateValidity.bind(this)); - } - - /* Get or set current input value */ - accessCurrentValue(newVal) { - return newVal ? this.inputElement.val(newVal) : this.inputElement.val(); - } - - getInputValidity() { - return this.inputDomElement.validity.valid; - } - - updateValidity() { - const inputVal = this.accessCurrentValue(); - this.state.empty = !inputVal.length; - this.state.valid = this.getInputValidity(); - this.renderValidity(); - } - - renderValid() { - return this.renderClear(); - } - - renderEmpty() { - return this.renderInvalid(); - } - - renderInvalid() { - this.inputElement.addClass(inputErrorClass); - this.scopedSiblings.hide(); - return this.fieldErrorElement.show(); - } - - renderClear() { - const inputVal = this.accessCurrentValue(); - if (!inputVal.split(' ').length) { - const trimmedInput = inputVal.trim(); - this.accessCurrentValue(trimmedInput); - } - this.inputElement.removeClass(inputErrorClass); - this.scopedSiblings.hide(); - this.fieldErrorElement.hide(); - } - } - - global.GlFieldError = GlFieldError; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js new file mode 100644 index 00000000000..e9add115429 --- /dev/null +++ b/app/assets/javascripts/gl_field_errors.js @@ -0,0 +1,48 @@ +/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign */ + +require('./gl_field_error'); + +((global) => { + const customValidationFlag = 'gl-field-error-ignore'; + + class GlFieldErrors { + constructor(form) { + this.form = $(form); + this.state = { + inputs: [], + valid: false + }; + this.initValidators(); + } + + initValidators () { + // register selectors here as needed + const validateSelectors = [':text', ':password', '[type=email]'] + .map((selector) => `input${selector}`).join(','); + + this.state.inputs = this.form.find(validateSelectors).toArray() + .filter((input) => !input.classList.contains(customValidationFlag)) + .map((input) => new global.GlFieldError({ input, formErrors: this })); + + this.form.on('submit', this.catchInvalidFormSubmit); + } + + /* Neccessary to prevent intercept and override invalid form submit + * because Safari & iOS quietly allow form submission when form is invalid + * and prevents disabling of invalid submit button by application.js */ + + catchInvalidFormSubmit (event) { + if (!event.currentTarget.checkValidity()) { + event.preventDefault(); + event.stopPropagation(); + } + } + + focusOnFirstInvalid () { + const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0]; + firstInvalid.inputElement.focus(); + } + } + + global.GlFieldErrors = GlFieldErrors; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6 deleted file mode 100644 index e9add115429..00000000000 --- a/app/assets/javascripts/gl_field_errors.js.es6 +++ /dev/null @@ -1,48 +0,0 @@ -/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign */ - -require('./gl_field_error'); - -((global) => { - const customValidationFlag = 'gl-field-error-ignore'; - - class GlFieldErrors { - constructor(form) { - this.form = $(form); - this.state = { - inputs: [], - valid: false - }; - this.initValidators(); - } - - initValidators () { - // register selectors here as needed - const validateSelectors = [':text', ':password', '[type=email]'] - .map((selector) => `input${selector}`).join(','); - - this.state.inputs = this.form.find(validateSelectors).toArray() - .filter((input) => !input.classList.contains(customValidationFlag)) - .map((input) => new global.GlFieldError({ input, formErrors: this })); - - this.form.on('submit', this.catchInvalidFormSubmit); - } - - /* Neccessary to prevent intercept and override invalid form submit - * because Safari & iOS quietly allow form submission when form is invalid - * and prevents disabling of invalid submit button by application.js */ - - catchInvalidFormSubmit (event) { - if (!event.currentTarget.checkValidity()) { - event.preventDefault(); - event.stopPropagation(); - } - } - - focusOnFirstInvalid () { - const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0]; - firstInvalid.inputElement.focus(); - } - } - - global.GlFieldErrors = GlFieldErrors; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js new file mode 100644 index 00000000000..0b446ff364a --- /dev/null +++ b/app/assets/javascripts/gl_form.js @@ -0,0 +1,92 @@ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, max-len */ +/* global GitLab */ +/* global DropzoneInput */ +/* global autosize */ + +(() => { + const global = window.gl || (window.gl = {}); + + function GLForm(form) { + this.form = form; + this.textarea = this.form.find('textarea.js-gfm-input'); + // Before we start, we should clean up any previous data for this form + this.destroy(); + // Setup the form + this.setupForm(); + this.form.data('gl-form', this); + } + + GLForm.prototype.destroy = function() { + // Clean form listeners + this.clearEventListeners(); + return this.form.data('gl-form', null); + }; + + GLForm.prototype.setupForm = function() { + var isNewForm; + isNewForm = this.form.is(':not(.gfm-form)'); + this.form.removeClass('js-new-note-form'); + if (isNewForm) { + this.form.find('.div-dropzone').remove(); + 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')); + gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input')); + new DropzoneInput(this.form); + autosize(this.textarea); + // form and textarea event listeners + this.addEventListeners(); + } + gl.text.init(this.form); + // hide discard button + this.form.find('.js-note-discard').hide(); + this.form.show(); + if (this.isAutosizeable) this.setupAutosize(); + }; + + GLForm.prototype.setupAutosize = function () { + this.textarea.off('autosize:resized') + .on('autosize:resized', this.setHeightData.bind(this)); + + this.textarea.off('mouseup.autosize') + .on('mouseup.autosize', this.destroyAutosize.bind(this)); + + setTimeout(() => { + autosize(this.textarea); + this.textarea.css('resize', 'vertical'); + }, 0); + }; + + GLForm.prototype.setHeightData = function () { + this.textarea.data('height', this.textarea.outerHeight()); + }; + + GLForm.prototype.destroyAutosize = function () { + const outerHeight = this.textarea.outerHeight(); + + if (this.textarea.data('height') === outerHeight) return; + + autosize.destroy(this.textarea); + + this.textarea.data('height', outerHeight); + this.textarea.outerHeight(outerHeight); + this.textarea.css('max-height', window.outerHeight); + }; + + GLForm.prototype.clearEventListeners = function() { + this.textarea.off('focus'); + this.textarea.off('blur'); + return gl.text.removeListeners(this.form); + }; + + GLForm.prototype.addEventListeners = function() { + this.textarea.on('focus', function() { + return $(this).closest('.md-area').addClass('is-focused'); + }); + return this.textarea.on('blur', function() { + return $(this).closest('.md-area').removeClass('is-focused'); + }); + }; + + global.GLForm = GLForm; +})(); diff --git a/app/assets/javascripts/gl_form.js.es6 b/app/assets/javascripts/gl_form.js.es6 deleted file mode 100644 index 0b446ff364a..00000000000 --- a/app/assets/javascripts/gl_form.js.es6 +++ /dev/null @@ -1,92 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, max-len */ -/* global GitLab */ -/* global DropzoneInput */ -/* global autosize */ - -(() => { - const global = window.gl || (window.gl = {}); - - function GLForm(form) { - this.form = form; - this.textarea = this.form.find('textarea.js-gfm-input'); - // Before we start, we should clean up any previous data for this form - this.destroy(); - // Setup the form - this.setupForm(); - this.form.data('gl-form', this); - } - - GLForm.prototype.destroy = function() { - // Clean form listeners - this.clearEventListeners(); - return this.form.data('gl-form', null); - }; - - GLForm.prototype.setupForm = function() { - var isNewForm; - isNewForm = this.form.is(':not(.gfm-form)'); - this.form.removeClass('js-new-note-form'); - if (isNewForm) { - this.form.find('.div-dropzone').remove(); - 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')); - gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input')); - new DropzoneInput(this.form); - autosize(this.textarea); - // form and textarea event listeners - this.addEventListeners(); - } - gl.text.init(this.form); - // hide discard button - this.form.find('.js-note-discard').hide(); - this.form.show(); - if (this.isAutosizeable) this.setupAutosize(); - }; - - GLForm.prototype.setupAutosize = function () { - this.textarea.off('autosize:resized') - .on('autosize:resized', this.setHeightData.bind(this)); - - this.textarea.off('mouseup.autosize') - .on('mouseup.autosize', this.destroyAutosize.bind(this)); - - setTimeout(() => { - autosize(this.textarea); - this.textarea.css('resize', 'vertical'); - }, 0); - }; - - GLForm.prototype.setHeightData = function () { - this.textarea.data('height', this.textarea.outerHeight()); - }; - - GLForm.prototype.destroyAutosize = function () { - const outerHeight = this.textarea.outerHeight(); - - if (this.textarea.data('height') === outerHeight) return; - - autosize.destroy(this.textarea); - - this.textarea.data('height', outerHeight); - this.textarea.outerHeight(outerHeight); - this.textarea.css('max-height', window.outerHeight); - }; - - GLForm.prototype.clearEventListeners = function() { - this.textarea.off('focus'); - this.textarea.off('blur'); - return gl.text.removeListeners(this.form); - }; - - GLForm.prototype.addEventListeners = function() { - this.textarea.on('focus', function() { - return $(this).closest('.md-area').addClass('is-focused'); - }); - return this.textarea.on('blur', function() { - return $(this).closest('.md-area').removeClass('is-focused'); - }); - }; - - global.GLForm = GLForm; -})(); diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js new file mode 100644 index 00000000000..15e695e81cf --- /dev/null +++ b/app/assets/javascripts/group_label_subscription.js @@ -0,0 +1,53 @@ +/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, max-len */ + +(function(global) { + class GroupLabelSubscription { + constructor(container) { + const $container = $(container); + this.$dropdown = $container.find('.dropdown'); + this.$subscribeButtons = $container.find('.js-subscribe-button'); + this.$unsubscribeButtons = $container.find('.js-unsubscribe-button'); + + this.$subscribeButtons.on('click', this.subscribe.bind(this)); + this.$unsubscribeButtons.on('click', this.unsubscribe.bind(this)); + } + + unsubscribe(event) { + event.preventDefault(); + + const url = this.$unsubscribeButtons.attr('data-url'); + + $.ajax({ + type: 'POST', + url: url + }).done(() => { + this.toggleSubscriptionButtons(); + this.$unsubscribeButtons.removeAttr('data-url'); + }); + } + + subscribe(event) { + event.preventDefault(); + + const $btn = $(event.currentTarget); + const url = $btn.attr('data-url'); + + this.$unsubscribeButtons.attr('data-url', url); + + $.ajax({ + type: 'POST', + url: url + }).done(() => { + this.toggleSubscriptionButtons(); + }); + } + + toggleSubscriptionButtons() { + this.$dropdown.toggleClass('hidden'); + this.$subscribeButtons.toggleClass('hidden'); + this.$unsubscribeButtons.toggleClass('hidden'); + } + } + + global.GroupLabelSubscription = GroupLabelSubscription; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/group_label_subscription.js.es6 b/app/assets/javascripts/group_label_subscription.js.es6 deleted file mode 100644 index 15e695e81cf..00000000000 --- a/app/assets/javascripts/group_label_subscription.js.es6 +++ /dev/null @@ -1,53 +0,0 @@ -/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, max-len */ - -(function(global) { - class GroupLabelSubscription { - constructor(container) { - const $container = $(container); - this.$dropdown = $container.find('.dropdown'); - this.$subscribeButtons = $container.find('.js-subscribe-button'); - this.$unsubscribeButtons = $container.find('.js-unsubscribe-button'); - - this.$subscribeButtons.on('click', this.subscribe.bind(this)); - this.$unsubscribeButtons.on('click', this.unsubscribe.bind(this)); - } - - unsubscribe(event) { - event.preventDefault(); - - const url = this.$unsubscribeButtons.attr('data-url'); - - $.ajax({ - type: 'POST', - url: url - }).done(() => { - this.toggleSubscriptionButtons(); - this.$unsubscribeButtons.removeAttr('data-url'); - }); - } - - subscribe(event) { - event.preventDefault(); - - const $btn = $(event.currentTarget); - const url = $btn.attr('data-url'); - - this.$unsubscribeButtons.attr('data-url', url); - - $.ajax({ - type: 'POST', - url: url - }).done(() => { - this.toggleSubscriptionButtons(); - }); - } - - toggleSubscriptionButtons() { - this.$dropdown.toggleClass('hidden'); - this.$subscribeButtons.toggleClass('hidden'); - this.$unsubscribeButtons.toggleClass('hidden'); - } - } - - global.GroupLabelSubscription = GroupLabelSubscription; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/issuable.js b/app/assets/javascripts/issuable.js new file mode 100644 index 00000000000..3bfce32768a --- /dev/null +++ b/app/assets/javascripts/issuable.js @@ -0,0 +1,188 @@ +/* eslint-disable no-param-reassign, 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, wrap-iife, max-len */ +/* global Issuable */ + +((global) => { + var issuable_created; + + issuable_created = false; + + global.Issuable = { + init: function() { + Issuable.initTemplates(); + Issuable.initSearch(); + Issuable.initChecks(); + Issuable.initResetFilters(); + Issuable.resetIncomingEmailToken(); + return Issuable.initLabelFilterRemove(); + }, + initTemplates: function() { + return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <%- label.title %> <% }); %>'); + }, + initSearch: function() { + const $searchInput = $('#issuable_search'); + + Issuable.initSearchState($searchInput); + + // `immediate` param set to false debounces on the `trailing` edge, lets user finish typing + const debouncedExecSearch = _.debounce(Issuable.executeSearch, 1000, false); + + $searchInput.off('keyup').on('keyup', debouncedExecSearch); + + // ensures existing filters are preserved when manually submitted + $('#issuable_search_form').on('submit', (e) => { + e.preventDefault(); + debouncedExecSearch(e); + }); + }, + initSearchState: function($searchInput) { + const currentSearchVal = $searchInput.val(); + + Issuable.searchState = { + elem: $searchInput, + current: currentSearchVal + }; + + Issuable.maybeFocusOnSearch(); + }, + accessSearchPristine: function(set) { + // store reference to previous value to prevent search on non-mutating keyup + const state = Issuable.searchState; + const currentSearchVal = state.elem.val(); + + if (set) { + state.current = currentSearchVal; + } else { + return state.current === currentSearchVal; + } + }, + maybeFocusOnSearch: function() { + const currentSearchVal = Issuable.searchState.current; + if (currentSearchVal && currentSearchVal !== '') { + const queryLength = currentSearchVal.length; + const $searchInput = Issuable.searchState.elem; + + /* The following ensures that the cursor is initially placed at + * the end of search input when focus is applied. It accounts + * for differences in browser implementations of `setSelectionRange` + * and cursor placement for elements in focus. + */ + $searchInput.focus(); + if ($searchInput.setSelectionRange) { + $searchInput.setSelectionRange(queryLength, queryLength); + } else { + $searchInput.val(currentSearchVal); + } + } + }, + executeSearch: function(e) { + const $search = $('#issuable_search'); + const $searchName = $search.attr('name'); + const $searchValue = $search.val(); + const $filtersForm = $('.js-filter-form'); + const $input = $(`input[name='${$searchName}']`, $filtersForm); + const isPristine = Issuable.accessSearchPristine(); + + if (isPristine) { + return; + } + + if (!$input.length) { + $filtersForm.append(``); + } else { + $input.val($searchValue); + } + + Issuable.filterResults($filtersForm); + }, + initLabelFilterRemove: function() { + return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) { + var $button; + $button = $(this); + // Remove the label input box + $('input[name="label_name[]"]').filter(function() { + return this.value === $button.data('label'); + }).remove(); + // Submit the form to get new data + Issuable.filterResults($('.filter-form')); + }); + }, + filterResults: (function(_this) { + return function(form) { + var formAction, formData, issuesUrl; + formData = form.serializeArray(); + formData = formData.filter(function(data) { + return data.value !== ''; + }); + formData = $.param(formData); + formAction = form.attr('action'); + issuesUrl = formAction; + issuesUrl += "" + (formAction.indexOf('?') === -1 ? '?' : '&'); + issuesUrl += formData; + return gl.utils.visitUrl(issuesUrl); + }; + })(this), + initResetFilters: function() { + $('.reset-filters').on('click', function(e) { + e.preventDefault(); + const target = e.target; + const $form = $(target).parents('.js-filter-form'); + const baseIssuesUrl = target.href; + + $form.attr('action', baseIssuesUrl); + gl.utils.visitUrl(baseIssuesUrl); + }); + }, + initChecks: function() { + this.issuableBulkActions = $('.bulk-update').data('bulkActions'); + $('.check_all_issues').off('click').on('click', function() { + $('.selected_issue').prop('checked', this.checked); + return Issuable.checkChanged(); + }); + return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this)); + }, + checkChanged: function() { + const $checkedIssues = $('.selected_issue:checked'); + const $updateIssuesIds = $('#update_issuable_ids'); + const $issuesOtherFilters = $('.issues-other-filters'); + const $issuesBulkUpdate = $('.issues_bulk_update'); + + this.issuableBulkActions.willUpdateLabels = false; + this.issuableBulkActions.setOriginalDropdownData(); + + if ($checkedIssues.length > 0) { + const ids = $.map($checkedIssues, function(value) { + return $(value).data('id'); + }); + $updateIssuesIds.val(ids); + $issuesOtherFilters.hide(); + $issuesBulkUpdate.show(); + } else { + $updateIssuesIds.val([]); + $issuesBulkUpdate.hide(); + $issuesOtherFilters.show(); + } + return true; + }, + + resetIncomingEmailToken: function() { + $('.incoming-email-token-reset').on('click', function(e) { + e.preventDefault(); + + $.ajax({ + type: 'PUT', + url: $('.incoming-email-token-reset').attr('href'), + dataType: 'json', + success: function(response) { + $('#issue_email').val(response.new_issue_address).focus(); + }, + beforeSend: function() { + $('.incoming-email-token-reset').text('resetting...'); + }, + complete: function() { + $('.incoming-email-token-reset').text('reset it'); + } + }); + }); + } + }; +})(window); diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 deleted file mode 100644 index 3bfce32768a..00000000000 --- a/app/assets/javascripts/issuable.js.es6 +++ /dev/null @@ -1,188 +0,0 @@ -/* eslint-disable no-param-reassign, 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, wrap-iife, max-len */ -/* global Issuable */ - -((global) => { - var issuable_created; - - issuable_created = false; - - global.Issuable = { - init: function() { - Issuable.initTemplates(); - Issuable.initSearch(); - Issuable.initChecks(); - Issuable.initResetFilters(); - Issuable.resetIncomingEmailToken(); - return Issuable.initLabelFilterRemove(); - }, - initTemplates: function() { - return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <%- label.title %> <% }); %>'); - }, - initSearch: function() { - const $searchInput = $('#issuable_search'); - - Issuable.initSearchState($searchInput); - - // `immediate` param set to false debounces on the `trailing` edge, lets user finish typing - const debouncedExecSearch = _.debounce(Issuable.executeSearch, 1000, false); - - $searchInput.off('keyup').on('keyup', debouncedExecSearch); - - // ensures existing filters are preserved when manually submitted - $('#issuable_search_form').on('submit', (e) => { - e.preventDefault(); - debouncedExecSearch(e); - }); - }, - initSearchState: function($searchInput) { - const currentSearchVal = $searchInput.val(); - - Issuable.searchState = { - elem: $searchInput, - current: currentSearchVal - }; - - Issuable.maybeFocusOnSearch(); - }, - accessSearchPristine: function(set) { - // store reference to previous value to prevent search on non-mutating keyup - const state = Issuable.searchState; - const currentSearchVal = state.elem.val(); - - if (set) { - state.current = currentSearchVal; - } else { - return state.current === currentSearchVal; - } - }, - maybeFocusOnSearch: function() { - const currentSearchVal = Issuable.searchState.current; - if (currentSearchVal && currentSearchVal !== '') { - const queryLength = currentSearchVal.length; - const $searchInput = Issuable.searchState.elem; - - /* The following ensures that the cursor is initially placed at - * the end of search input when focus is applied. It accounts - * for differences in browser implementations of `setSelectionRange` - * and cursor placement for elements in focus. - */ - $searchInput.focus(); - if ($searchInput.setSelectionRange) { - $searchInput.setSelectionRange(queryLength, queryLength); - } else { - $searchInput.val(currentSearchVal); - } - } - }, - executeSearch: function(e) { - const $search = $('#issuable_search'); - const $searchName = $search.attr('name'); - const $searchValue = $search.val(); - const $filtersForm = $('.js-filter-form'); - const $input = $(`input[name='${$searchName}']`, $filtersForm); - const isPristine = Issuable.accessSearchPristine(); - - if (isPristine) { - return; - } - - if (!$input.length) { - $filtersForm.append(``); - } else { - $input.val($searchValue); - } - - Issuable.filterResults($filtersForm); - }, - initLabelFilterRemove: function() { - return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) { - var $button; - $button = $(this); - // Remove the label input box - $('input[name="label_name[]"]').filter(function() { - return this.value === $button.data('label'); - }).remove(); - // Submit the form to get new data - Issuable.filterResults($('.filter-form')); - }); - }, - filterResults: (function(_this) { - return function(form) { - var formAction, formData, issuesUrl; - formData = form.serializeArray(); - formData = formData.filter(function(data) { - return data.value !== ''; - }); - formData = $.param(formData); - formAction = form.attr('action'); - issuesUrl = formAction; - issuesUrl += "" + (formAction.indexOf('?') === -1 ? '?' : '&'); - issuesUrl += formData; - return gl.utils.visitUrl(issuesUrl); - }; - })(this), - initResetFilters: function() { - $('.reset-filters').on('click', function(e) { - e.preventDefault(); - const target = e.target; - const $form = $(target).parents('.js-filter-form'); - const baseIssuesUrl = target.href; - - $form.attr('action', baseIssuesUrl); - gl.utils.visitUrl(baseIssuesUrl); - }); - }, - initChecks: function() { - this.issuableBulkActions = $('.bulk-update').data('bulkActions'); - $('.check_all_issues').off('click').on('click', function() { - $('.selected_issue').prop('checked', this.checked); - return Issuable.checkChanged(); - }); - return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this)); - }, - checkChanged: function() { - const $checkedIssues = $('.selected_issue:checked'); - const $updateIssuesIds = $('#update_issuable_ids'); - const $issuesOtherFilters = $('.issues-other-filters'); - const $issuesBulkUpdate = $('.issues_bulk_update'); - - this.issuableBulkActions.willUpdateLabels = false; - this.issuableBulkActions.setOriginalDropdownData(); - - if ($checkedIssues.length > 0) { - const ids = $.map($checkedIssues, function(value) { - return $(value).data('id'); - }); - $updateIssuesIds.val(ids); - $issuesOtherFilters.hide(); - $issuesBulkUpdate.show(); - } else { - $updateIssuesIds.val([]); - $issuesBulkUpdate.hide(); - $issuesOtherFilters.show(); - } - return true; - }, - - resetIncomingEmailToken: function() { - $('.incoming-email-token-reset').on('click', function(e) { - e.preventDefault(); - - $.ajax({ - type: 'PUT', - url: $('.incoming-email-token-reset').attr('href'), - dataType: 'json', - success: function(response) { - $('#issue_email').val(response.new_issue_address).focus(); - }, - beforeSend: function() { - $('.incoming-email-token-reset').text('resetting...'); - }, - complete: function() { - $('.incoming-email-token-reset').text('reset it'); - } - }); - }); - } - }; -})(window); diff --git a/app/assets/javascripts/issuable/issuable_bundle.js b/app/assets/javascripts/issuable/issuable_bundle.js new file mode 100644 index 00000000000..e927cc0077c --- /dev/null +++ b/app/assets/javascripts/issuable/issuable_bundle.js @@ -0,0 +1 @@ +require('./time_tracking/time_tracking_bundle'); diff --git a/app/assets/javascripts/issuable/issuable_bundle.js.es6 b/app/assets/javascripts/issuable/issuable_bundle.js.es6 deleted file mode 100644 index e927cc0077c..00000000000 --- a/app/assets/javascripts/issuable/issuable_bundle.js.es6 +++ /dev/null @@ -1 +0,0 @@ -require('./time_tracking/time_tracking_bundle'); diff --git a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js new file mode 100644 index 00000000000..357b3487ca9 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js @@ -0,0 +1,42 @@ +/* global Vue */ +import stopwatchSvg from 'icons/_icon_stopwatch.svg'; + +require('../../../lib/utils/pretty_time'); + +(() => { + Vue.component('time-tracking-collapsed-state', { + name: 'time-tracking-collapsed-state', + props: [ + 'showComparisonState', + 'showSpentOnlyState', + 'showEstimateOnlyState', + 'showNoTimeTrackingState', + 'timeSpentHumanReadable', + 'timeEstimateHumanReadable', + ], + methods: { + abbreviateTime(timeStr) { + return gl.utils.prettyTime.abbreviateTime(timeStr); + }, + }, + template: ` + + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 deleted file mode 100644 index 357b3487ca9..00000000000 --- a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 +++ /dev/null @@ -1,42 +0,0 @@ -/* global Vue */ -import stopwatchSvg from 'icons/_icon_stopwatch.svg'; - -require('../../../lib/utils/pretty_time'); - -(() => { - Vue.component('time-tracking-collapsed-state', { - name: 'time-tracking-collapsed-state', - props: [ - 'showComparisonState', - 'showSpentOnlyState', - 'showEstimateOnlyState', - 'showNoTimeTrackingState', - 'timeSpentHumanReadable', - 'timeEstimateHumanReadable', - ], - methods: { - abbreviateTime(timeStr) { - return gl.utils.prettyTime.abbreviateTime(timeStr); - }, - }, - template: ` - - `, - }); -})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js new file mode 100644 index 00000000000..750468c679b --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js @@ -0,0 +1,69 @@ +/* global Vue */ +require('../../../lib/utils/pretty_time'); + +(() => { + const prettyTime = gl.utils.prettyTime; + + Vue.component('time-tracking-comparison-pane', { + name: 'time-tracking-comparison-pane', + props: [ + 'timeSpent', + 'timeEstimate', + 'timeSpentHumanReadable', + 'timeEstimateHumanReadable', + ], + computed: { + parsedRemaining() { + const diffSeconds = this.timeEstimate - this.timeSpent; + return prettyTime.parseSeconds(diffSeconds); + }, + timeRemainingHumanReadable() { + return prettyTime.stringifyTime(this.parsedRemaining); + }, + timeRemainingTooltip() { + const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:'; + return `${prefix} ${this.timeRemainingHumanReadable}`; + }, + /* Diff values for comparison meter */ + timeRemainingMinutes() { + return this.timeEstimate - this.timeSpent; + }, + timeRemainingPercent() { + return `${Math.floor((this.timeSpent / this.timeEstimate) * 100)}%`; + }, + timeRemainingStatusClass() { + return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate'; + }, + /* Parsed time values */ + parsedEstimate() { + return prettyTime.parseSeconds(this.timeEstimate); + }, + parsedSpent() { + return prettyTime.parseSeconds(this.timeSpent); + }, + }, + template: ` +
    +
    +
    +
    +
    +
    +
    + Spent + {{ timeSpentHumanReadable }} +
    +
    + Est + {{ timeEstimateHumanReadable }} +
    +
    +
    +
    + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 deleted file mode 100644 index 750468c679b..00000000000 --- a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 +++ /dev/null @@ -1,69 +0,0 @@ -/* global Vue */ -require('../../../lib/utils/pretty_time'); - -(() => { - const prettyTime = gl.utils.prettyTime; - - Vue.component('time-tracking-comparison-pane', { - name: 'time-tracking-comparison-pane', - props: [ - 'timeSpent', - 'timeEstimate', - 'timeSpentHumanReadable', - 'timeEstimateHumanReadable', - ], - computed: { - parsedRemaining() { - const diffSeconds = this.timeEstimate - this.timeSpent; - return prettyTime.parseSeconds(diffSeconds); - }, - timeRemainingHumanReadable() { - return prettyTime.stringifyTime(this.parsedRemaining); - }, - timeRemainingTooltip() { - const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:'; - return `${prefix} ${this.timeRemainingHumanReadable}`; - }, - /* Diff values for comparison meter */ - timeRemainingMinutes() { - return this.timeEstimate - this.timeSpent; - }, - timeRemainingPercent() { - return `${Math.floor((this.timeSpent / this.timeEstimate) * 100)}%`; - }, - timeRemainingStatusClass() { - return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate'; - }, - /* Parsed time values */ - parsedEstimate() { - return prettyTime.parseSeconds(this.timeEstimate); - }, - parsedSpent() { - return prettyTime.parseSeconds(this.timeSpent); - }, - }, - template: ` -
    -
    -
    -
    -
    -
    -
    - Spent - {{ timeSpentHumanReadable }} -
    -
    - Est - {{ timeEstimateHumanReadable }} -
    -
    -
    -
    - `, - }); -})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js new file mode 100644 index 00000000000..309e9f2f9ef --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js @@ -0,0 +1,13 @@ +/* global Vue */ +(() => { + Vue.component('time-tracking-estimate-only-pane', { + name: 'time-tracking-estimate-only-pane', + props: ['timeEstimateHumanReadable'], + template: ` +
    + Estimated: + {{ timeEstimateHumanReadable }} +
    + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 deleted file mode 100644 index 309e9f2f9ef..00000000000 --- a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 +++ /dev/null @@ -1,13 +0,0 @@ -/* global Vue */ -(() => { - Vue.component('time-tracking-estimate-only-pane', { - name: 'time-tracking-estimate-only-pane', - props: ['timeEstimateHumanReadable'], - template: ` -
    - Estimated: - {{ timeEstimateHumanReadable }} -
    - `, - }); -})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/help_state.js b/app/assets/javascripts/issuable/time_tracking/components/help_state.js new file mode 100644 index 00000000000..d7ced6d7151 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/help_state.js @@ -0,0 +1,24 @@ +/* global Vue */ +(() => { + Vue.component('time-tracking-help-state', { + name: 'time-tracking-help-state', + props: ['docsUrl'], + template: ` +
    +
    +

    Track time with slash commands

    +

    Slash commands can be used in the issues description and comment boxes.

    +

    + /estimate + will update the estimated time with the latest command. +

    +

    + /spend + will update the sum of the time spent. +

    + Learn more +
    +
    + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 deleted file mode 100644 index d7ced6d7151..00000000000 --- a/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 +++ /dev/null @@ -1,24 +0,0 @@ -/* global Vue */ -(() => { - Vue.component('time-tracking-help-state', { - name: 'time-tracking-help-state', - props: ['docsUrl'], - template: ` -
    -
    -

    Track time with slash commands

    -

    Slash commands can be used in the issues description and comment boxes.

    -

    - /estimate - will update the estimated time with the latest command. -

    -

    - /spend - will update the sum of the time spent. -

    - Learn more -
    -
    - `, - }); -})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js new file mode 100644 index 00000000000..1d2ca643b5b --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js @@ -0,0 +1,11 @@ +/* global Vue */ +(() => { + Vue.component('time-tracking-no-tracking-pane', { + name: 'time-tracking-no-tracking-pane', + template: ` +
    + No estimate or time spent +
    + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 deleted file mode 100644 index 1d2ca643b5b..00000000000 --- a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 +++ /dev/null @@ -1,11 +0,0 @@ -/* global Vue */ -(() => { - Vue.component('time-tracking-no-tracking-pane', { - name: 'time-tracking-no-tracking-pane', - template: ` -
    - No estimate or time spent -
    - `, - }); -})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js new file mode 100644 index 00000000000..ed283fec3c3 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js @@ -0,0 +1,13 @@ +/* global Vue */ +(() => { + Vue.component('time-tracking-spent-only-pane', { + name: 'time-tracking-spent-only-pane', + props: ['timeSpentHumanReadable'], + template: ` +
    + Spent: + {{ timeSpentHumanReadable }} +
    + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6 deleted file mode 100644 index ed283fec3c3..00000000000 --- a/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6 +++ /dev/null @@ -1,13 +0,0 @@ -/* global Vue */ -(() => { - Vue.component('time-tracking-spent-only-pane', { - name: 'time-tracking-spent-only-pane', - props: ['timeSpentHumanReadable'], - template: ` -
    - Spent: - {{ timeSpentHumanReadable }} -
    - `, - }); -})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js new file mode 100644 index 00000000000..1fae2d62b14 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js @@ -0,0 +1,117 @@ +/* global Vue */ + +require('./help_state'); +require('./collapsed_state'); +require('./spent_only_pane'); +require('./no_tracking_pane'); +require('./estimate_only_pane'); +require('./comparison_pane'); + +(() => { + Vue.component('issuable-time-tracker', { + name: 'issuable-time-tracker', + props: [ + 'time_estimate', + 'time_spent', + 'human_time_estimate', + 'human_time_spent', + 'docsUrl', + ], + data() { + return { + showHelp: false, + }; + }, + computed: { + timeSpent() { + return this.time_spent; + }, + timeEstimate() { + return this.time_estimate; + }, + timeEstimateHumanReadable() { + return this.human_time_estimate; + }, + timeSpentHumanReadable() { + return this.human_time_spent; + }, + hasTimeSpent() { + return !!this.timeSpent; + }, + hasTimeEstimate() { + return !!this.timeEstimate; + }, + showComparisonState() { + return this.hasTimeEstimate && this.hasTimeSpent; + }, + showEstimateOnlyState() { + return this.hasTimeEstimate && !this.hasTimeSpent; + }, + showSpentOnlyState() { + return this.hasTimeSpent && !this.hasTimeEstimate; + }, + showNoTimeTrackingState() { + return !this.hasTimeEstimate && !this.hasTimeSpent; + }, + showHelpState() { + return !!this.showHelp; + }, + }, + methods: { + toggleHelpState(show) { + this.showHelp = show; + }, + }, + template: ` +
    + + +
    + Time tracking +
    + +
    +
    + +
    +
    +
    + + + + + + + + + + + + +
    +
    + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 deleted file mode 100644 index 1fae2d62b14..00000000000 --- a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 +++ /dev/null @@ -1,117 +0,0 @@ -/* global Vue */ - -require('./help_state'); -require('./collapsed_state'); -require('./spent_only_pane'); -require('./no_tracking_pane'); -require('./estimate_only_pane'); -require('./comparison_pane'); - -(() => { - Vue.component('issuable-time-tracker', { - name: 'issuable-time-tracker', - props: [ - 'time_estimate', - 'time_spent', - 'human_time_estimate', - 'human_time_spent', - 'docsUrl', - ], - data() { - return { - showHelp: false, - }; - }, - computed: { - timeSpent() { - return this.time_spent; - }, - timeEstimate() { - return this.time_estimate; - }, - timeEstimateHumanReadable() { - return this.human_time_estimate; - }, - timeSpentHumanReadable() { - return this.human_time_spent; - }, - hasTimeSpent() { - return !!this.timeSpent; - }, - hasTimeEstimate() { - return !!this.timeEstimate; - }, - showComparisonState() { - return this.hasTimeEstimate && this.hasTimeSpent; - }, - showEstimateOnlyState() { - return this.hasTimeEstimate && !this.hasTimeSpent; - }, - showSpentOnlyState() { - return this.hasTimeSpent && !this.hasTimeEstimate; - }, - showNoTimeTrackingState() { - return !this.hasTimeEstimate && !this.hasTimeSpent; - }, - showHelpState() { - return !!this.showHelp; - }, - }, - methods: { - toggleHelpState(show) { - this.showHelp = show; - }, - }, - template: ` -
    - - -
    - Time tracking -
    - -
    -
    - -
    -
    -
    - - - - - - - - - - - - -
    -
    - `, - }); -})(); diff --git a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js new file mode 100644 index 00000000000..0134b7cb6f3 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js @@ -0,0 +1,65 @@ +/* global Vue */ + +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); +require('./components/time_tracker'); +require('../../smart_interval'); +require('../../subbable_resource'); + +(() => { + /* This Vue instance represents what will become the parent instance for the + * sidebar. It will be responsible for managing `issuable` state and propagating + * changes to sidebar components. We will want to create a separate service to + * interface with the server at that point. + */ + + class IssuableTimeTracking { + constructor(issuableJSON) { + const parsedIssuable = JSON.parse(issuableJSON); + return this.initComponent(parsedIssuable); + } + + initComponent(parsedIssuable) { + this.parentInstance = new Vue({ + el: '#issuable-time-tracker', + data: { + issuable: parsedIssuable, + }, + methods: { + fetchIssuable() { + return gl.IssuableResource.get.call(gl.IssuableResource, { + type: 'GET', + url: gl.IssuableResource.endpoint, + }); + }, + updateState(data) { + this.issuable = data; + }, + subscribeToUpdates() { + gl.IssuableResource.subscribe(data => this.updateState(data)); + }, + listenForSlashCommands() { + $(document).on('ajax:success', '.gfm-form', (e, data) => { + const subscribedCommands = ['spend_time', 'time_estimate']; + const changedCommands = data.commands_changes + ? Object.keys(data.commands_changes) + : []; + if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) { + this.fetchIssuable(); + } + }); + }, + }, + created() { + this.fetchIssuable(); + }, + mounted() { + this.subscribeToUpdates(); + this.listenForSlashCommands(); + }, + }); + } + } + + gl.IssuableTimeTracking = IssuableTimeTracking; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 deleted file mode 100644 index 0134b7cb6f3..00000000000 --- a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 +++ /dev/null @@ -1,65 +0,0 @@ -/* global Vue */ - -window.Vue = require('vue'); -window.Vue.use(require('vue-resource')); -require('./components/time_tracker'); -require('../../smart_interval'); -require('../../subbable_resource'); - -(() => { - /* This Vue instance represents what will become the parent instance for the - * sidebar. It will be responsible for managing `issuable` state and propagating - * changes to sidebar components. We will want to create a separate service to - * interface with the server at that point. - */ - - class IssuableTimeTracking { - constructor(issuableJSON) { - const parsedIssuable = JSON.parse(issuableJSON); - return this.initComponent(parsedIssuable); - } - - initComponent(parsedIssuable) { - this.parentInstance = new Vue({ - el: '#issuable-time-tracker', - data: { - issuable: parsedIssuable, - }, - methods: { - fetchIssuable() { - return gl.IssuableResource.get.call(gl.IssuableResource, { - type: 'GET', - url: gl.IssuableResource.endpoint, - }); - }, - updateState(data) { - this.issuable = data; - }, - subscribeToUpdates() { - gl.IssuableResource.subscribe(data => this.updateState(data)); - }, - listenForSlashCommands() { - $(document).on('ajax:success', '.gfm-form', (e, data) => { - const subscribedCommands = ['spend_time', 'time_estimate']; - const changedCommands = data.commands_changes - ? Object.keys(data.commands_changes) - : []; - if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) { - this.fetchIssuable(); - } - }); - }, - }, - created() { - this.fetchIssuable(); - }, - mounted() { - this.subscribeToUpdates(); - this.listenForSlashCommands(); - }, - }); - } - } - - gl.IssuableTimeTracking = IssuableTimeTracking; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/issues_bulk_assignment.js b/app/assets/javascripts/issues_bulk_assignment.js new file mode 100644 index 00000000000..e0ebd36a65c --- /dev/null +++ b/app/assets/javascripts/issues_bulk_assignment.js @@ -0,0 +1,163 @@ +/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */ +/* global Issuable */ +/* global Flash */ + +((global) => { + class IssuableBulkActions { + 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'); + this.form.data('bulkActions', this); + this.willUpdateLabels = false; + this.bindEvents(); + // Fixes bulk-assign not working when navigating through pages + Issuable.initChecks(); + } + + bindEvents() { + return this.form.off('submit').on('submit', this.onFormSubmit.bind(this)); + } + + onFormSubmit(e) { + e.preventDefault(); + return this.submit(); + } + + submit() { + const _this = this; + const xhr = $.ajax({ + url: this.form.attr('action'), + method: this.form.attr('method'), + dataType: 'JSON', + data: this.getFormDataAsObject() + }); + xhr.done(() => window.location.reload()); + xhr.fail(() => new Flash("Issue update failed")); + return xhr.always(this.onFormSubmitAlways.bind(this)); + } + + onFormSubmitAlways() { + return this.form.find('[type="submit"]').enable(); + } + + getSelectedIssues() { + return this.issues.has('.selected_issue:checked'); + } + + getLabelsFromSelection() { + const labels = []; + this.getSelectedIssues().map(function() { + const labelsData = $(this).data('labels'); + if (labelsData) { + return labelsData.map(function(labelId) { + if (labels.indexOf(labelId) === -1) { + return labels.push(labelId); + } + }); + } + }); + return labels; + } + + /** + * Will return only labels that were marked previously and the user has unmarked + * @return {Array} Label IDs + */ + + getUnmarkedIndeterminedLabels() { + const result = []; + const labelsToKeep = this.$labelDropdown.data('indeterminate'); + + this.getLabelsFromSelection().forEach((id) => { + if (labelsToKeep.indexOf(id) === -1) { + result.push(id); + } + }); + + return result; + } + + /** + * Simple form serialization, it will return just what we need + * Returns key/value pairs from form data + */ + + getFormDataAsObject() { + const formData = { + update: { + state_event: this.form.find('input[name="update[state_event]"]').val(), + assignee_id: this.form.find('input[name="update[assignee_id]"]').val(), + milestone_id: this.form.find('input[name="update[milestone_id]"]').val(), + issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(), + subscription_event: this.form.find('input[name="update[subscription_event]"]').val(), + add_label_ids: [], + remove_label_ids: [] + } + }; + if (this.willUpdateLabels) { + formData.update.add_label_ids = this.$labelDropdown.data('marked'); + formData.update.remove_label_ids = this.$labelDropdown.data('unmarked'); + } + return formData; + } + + setOriginalDropdownData() { + const $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 + getOriginalCommonIds() { + const labelIds = []; + + this.getElement('.selected_issue:checked').each((i, el) => { + labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels')); + }); + return _.intersection.apply(this, labelIds); + } + + // From issuable's initial bulk selection + getOriginalMarkedIds() { + const labelIds = []; + this.getElement('.selected_issue:checked').each((i, el) => { + labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels')); + }); + return _.intersection.apply(this, labelIds); + } + + // From issuable's initial bulk selection + getOriginalIndeterminateIds() { + const uniqueIds = []; + const 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) { + this.scopeEl = this.scopeEl || $('.content'); + return this.scopeEl.find(selector); + } + } + + global.IssuableBulkActions = IssuableBulkActions; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6 deleted file mode 100644 index e0ebd36a65c..00000000000 --- a/app/assets/javascripts/issues_bulk_assignment.js.es6 +++ /dev/null @@ -1,163 +0,0 @@ -/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */ -/* global Issuable */ -/* global Flash */ - -((global) => { - class IssuableBulkActions { - 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'); - this.form.data('bulkActions', this); - this.willUpdateLabels = false; - this.bindEvents(); - // Fixes bulk-assign not working when navigating through pages - Issuable.initChecks(); - } - - bindEvents() { - return this.form.off('submit').on('submit', this.onFormSubmit.bind(this)); - } - - onFormSubmit(e) { - e.preventDefault(); - return this.submit(); - } - - submit() { - const _this = this; - const xhr = $.ajax({ - url: this.form.attr('action'), - method: this.form.attr('method'), - dataType: 'JSON', - data: this.getFormDataAsObject() - }); - xhr.done(() => window.location.reload()); - xhr.fail(() => new Flash("Issue update failed")); - return xhr.always(this.onFormSubmitAlways.bind(this)); - } - - onFormSubmitAlways() { - return this.form.find('[type="submit"]').enable(); - } - - getSelectedIssues() { - return this.issues.has('.selected_issue:checked'); - } - - getLabelsFromSelection() { - const labels = []; - this.getSelectedIssues().map(function() { - const labelsData = $(this).data('labels'); - if (labelsData) { - return labelsData.map(function(labelId) { - if (labels.indexOf(labelId) === -1) { - return labels.push(labelId); - } - }); - } - }); - return labels; - } - - /** - * Will return only labels that were marked previously and the user has unmarked - * @return {Array} Label IDs - */ - - getUnmarkedIndeterminedLabels() { - const result = []; - const labelsToKeep = this.$labelDropdown.data('indeterminate'); - - this.getLabelsFromSelection().forEach((id) => { - if (labelsToKeep.indexOf(id) === -1) { - result.push(id); - } - }); - - return result; - } - - /** - * Simple form serialization, it will return just what we need - * Returns key/value pairs from form data - */ - - getFormDataAsObject() { - const formData = { - update: { - state_event: this.form.find('input[name="update[state_event]"]').val(), - assignee_id: this.form.find('input[name="update[assignee_id]"]').val(), - milestone_id: this.form.find('input[name="update[milestone_id]"]').val(), - issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(), - subscription_event: this.form.find('input[name="update[subscription_event]"]').val(), - add_label_ids: [], - remove_label_ids: [] - } - }; - if (this.willUpdateLabels) { - formData.update.add_label_ids = this.$labelDropdown.data('marked'); - formData.update.remove_label_ids = this.$labelDropdown.data('unmarked'); - } - return formData; - } - - setOriginalDropdownData() { - const $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 - getOriginalCommonIds() { - const labelIds = []; - - this.getElement('.selected_issue:checked').each((i, el) => { - labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels')); - }); - return _.intersection.apply(this, labelIds); - } - - // From issuable's initial bulk selection - getOriginalMarkedIds() { - const labelIds = []; - this.getElement('.selected_issue:checked').each((i, el) => { - labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels')); - }); - return _.intersection.apply(this, labelIds); - } - - // From issuable's initial bulk selection - getOriginalIndeterminateIds() { - const uniqueIds = []; - const 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) { - this.scopeEl = this.scopeEl || $('.content'); - return this.scopeEl.find(selector); - } - } - - global.IssuableBulkActions = IssuableBulkActions; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js new file mode 100644 index 00000000000..38b2eb9ff14 --- /dev/null +++ b/app/assets/javascripts/label_manager.js @@ -0,0 +1,118 @@ +/* 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, max-len */ +/* global Flash */ +/* global Sortable */ + +((global) => { + class LabelManager { + constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) { + this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority'); + this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels'); + this.otherLabels = otherLabels || $('.js-other-labels'); + this.errorMessage = 'Unable to update label prioritization at this time'; + this.emptyState = document.querySelector('#js-priority-labels-empty-state'); + this.sortable = Sortable.create(this.prioritizedLabels.get(0), { + filter: '.empty-message', + forceFallback: true, + fallbackClass: 'is-dragging', + dataIdAttr: 'data-id', + onUpdate: this.onPrioritySortUpdate.bind(this), + }); + this.bindEvents(); + } + + bindEvents() { + return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick); + } + + onTogglePriorityClick(e) { + e.preventDefault(); + const _this = e.data; + const $btn = $(e.currentTarget); + const $label = $(`#${$btn.data('domId')}`); + const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add'; + const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`); + $tooltip.tooltip('destroy'); + _this.toggleLabelPriority($label, action); + _this.toggleEmptyState($label, $btn, action); + } + + toggleEmptyState($label, $btn, action) { + this.emptyState.classList.toggle('hidden', !!this.prioritizedLabels[0].querySelector(':scope > li')); + } + + toggleLabelPriority($label, action, persistState) { + if (persistState == null) { + persistState = true; + } + let xhr; + const _this = this; + const url = $label.find('.js-toggle-priority').data('url'); + let $target = this.prioritizedLabels; + let $from = this.otherLabels; + if (action === 'remove') { + $target = this.otherLabels; + $from = this.prioritizedLabels; + } + $label.detach().appendTo($target); + if ($from.find('li').length) { + $from.find('.empty-message').removeClass('hidden'); + } + if ($target.find('> li:not(.empty-message)').length) { + $target.find('.empty-message').addClass('hidden'); + } + // Return if we are not persisting state + if (!persistState) { + return; + } + if (action === 'remove') { + xhr = $.ajax({ + url, + type: 'DELETE' + }); + // Restore empty message + if (!$from.find('li').length) { + $from.find('.empty-message').removeClass('hidden'); + } + } else { + xhr = this.savePrioritySort($label, action); + } + return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action)); + } + + onPrioritySortUpdate() { + const xhr = this.savePrioritySort(); + return xhr.fail(function() { + return new Flash(this.errorMessage, 'alert'); + }); + } + + savePrioritySort() { + return $.post({ + url: this.prioritizedLabels.data('url'), + data: { + label_ids: this.getSortedLabelsIds() + } + }); + } + + rollbackLabelPosition($label, originalAction) { + const action = originalAction === 'remove' ? 'add' : 'remove'; + this.toggleLabelPriority($label, action, false); + return new Flash(this.errorMessage, 'alert'); + } + + getSortedLabelsIds() { + const sortedIds = []; + this.prioritizedLabels.find('> li').each(function() { + const id = $(this).data('id'); + + if (id) { + sortedIds.push(id); + } + }); + return sortedIds; + } + } + + gl.LabelManager = LabelManager; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js.es6 deleted file mode 100644 index 38b2eb9ff14..00000000000 --- a/app/assets/javascripts/label_manager.js.es6 +++ /dev/null @@ -1,118 +0,0 @@ -/* 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, max-len */ -/* global Flash */ -/* global Sortable */ - -((global) => { - class LabelManager { - constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) { - this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority'); - this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels'); - this.otherLabels = otherLabels || $('.js-other-labels'); - this.errorMessage = 'Unable to update label prioritization at this time'; - this.emptyState = document.querySelector('#js-priority-labels-empty-state'); - this.sortable = Sortable.create(this.prioritizedLabels.get(0), { - filter: '.empty-message', - forceFallback: true, - fallbackClass: 'is-dragging', - dataIdAttr: 'data-id', - onUpdate: this.onPrioritySortUpdate.bind(this), - }); - this.bindEvents(); - } - - bindEvents() { - return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick); - } - - onTogglePriorityClick(e) { - e.preventDefault(); - const _this = e.data; - const $btn = $(e.currentTarget); - const $label = $(`#${$btn.data('domId')}`); - const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add'; - const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`); - $tooltip.tooltip('destroy'); - _this.toggleLabelPriority($label, action); - _this.toggleEmptyState($label, $btn, action); - } - - toggleEmptyState($label, $btn, action) { - this.emptyState.classList.toggle('hidden', !!this.prioritizedLabels[0].querySelector(':scope > li')); - } - - toggleLabelPriority($label, action, persistState) { - if (persistState == null) { - persistState = true; - } - let xhr; - const _this = this; - const url = $label.find('.js-toggle-priority').data('url'); - let $target = this.prioritizedLabels; - let $from = this.otherLabels; - if (action === 'remove') { - $target = this.otherLabels; - $from = this.prioritizedLabels; - } - $label.detach().appendTo($target); - if ($from.find('li').length) { - $from.find('.empty-message').removeClass('hidden'); - } - if ($target.find('> li:not(.empty-message)').length) { - $target.find('.empty-message').addClass('hidden'); - } - // Return if we are not persisting state - if (!persistState) { - return; - } - if (action === 'remove') { - xhr = $.ajax({ - url, - type: 'DELETE' - }); - // Restore empty message - if (!$from.find('li').length) { - $from.find('.empty-message').removeClass('hidden'); - } - } else { - xhr = this.savePrioritySort($label, action); - } - return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action)); - } - - onPrioritySortUpdate() { - const xhr = this.savePrioritySort(); - return xhr.fail(function() { - return new Flash(this.errorMessage, 'alert'); - }); - } - - savePrioritySort() { - return $.post({ - url: this.prioritizedLabels.data('url'), - data: { - label_ids: this.getSortedLabelsIds() - } - }); - } - - rollbackLabelPosition($label, originalAction) { - const action = originalAction === 'remove' ? 'add' : 'remove'; - this.toggleLabelPriority($label, action, false); - return new Flash(this.errorMessage, 'alert'); - } - - getSortedLabelsIds() { - const sortedIds = []; - this.prioritizedLabels.find('> li').each(function() { - const id = $(this).data('id'); - - if (id) { - sortedIds.push(id); - } - }); - return sortedIds; - } - } - - gl.LabelManager = LabelManager; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js new file mode 100644 index 00000000000..2955bda1a36 --- /dev/null +++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js @@ -0,0 +1,112 @@ +/** + * Linked Tabs + * + * Handles persisting and restores the current tab selection and content. + * Reusable component for static content. + * + * ### Example Markup + * + *