From a1b3cd40647e8f7768b6db0bc64179e60f5d5937 Mon Sep 17 00:00:00 2001 From: Mircea Danila Dumitrescu Date: Mon, 2 Oct 2017 20:32:36 +0000 Subject: namespace should be lowercased in kubernetes. This is also true for the scenario where the namespace is generated from the project group-name. --- app/models/project_services/kubernetes_service.rb | 12 +++++++++++- changelogs/unreleased/mr-14642.yml | 6 ++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/mr-14642.yml diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 8ba07173c74..45a544e3674 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -153,7 +153,17 @@ class KubernetesService < DeploymentService end def default_namespace - "#{project.path}-#{project.id}" if project.present? + return unless project + + # 1. lowercase + # 2. replace non kubernetes characters with dash + # 3. trim dash from the beginning and end + + slugified = "#{project.path}-#{project.id}" + slugified.downcase! + slugified.gsub!(/[^a-z0-9]/, '-') + slugified.gsub!(/^-+|-+$/, '') + slugified end def build_kubeclient!(api_path: 'api', api_version: 'v1') diff --git a/changelogs/unreleased/mr-14642.yml b/changelogs/unreleased/mr-14642.yml new file mode 100644 index 00000000000..048cc79e323 --- /dev/null +++ b/changelogs/unreleased/mr-14642.yml @@ -0,0 +1,6 @@ +--- +title: Auto Devops kubernetes default namespace is now correctly built out of gitlab + project group-name +merge_request: 14642 +author: Mircea Danila Dumitrescu +type: fixed -- cgit v1.2.1 From b0c2772a900bd4390d0ead7192e1bda3acd01bab Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 12 Oct 2017 11:31:29 -0500 Subject: convert Autosave into pure es module and remove global export --- app/assets/javascripts/autosave.js | 31 +++++++++------------- app/assets/javascripts/issuable_form.js | 2 +- app/assets/javascripts/notes.js | 4 +-- .../notes/components/issue_comment_form.vue | 3 +-- app/assets/javascripts/notes/mixins/autosave.js | 3 +-- 5 files changed, 18 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js index 4d2d4db7c0e..73bdab4ecb7 100644 --- a/app/assets/javascripts/autosave.js +++ b/app/assets/javascripts/autosave.js @@ -1,8 +1,9 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, max-len */ +/* eslint-disable no-param-reassign, prefer-template, no-var, no-void, consistent-return */ + import AccessorUtilities from './lib/utils/accessor'; -window.Autosave = (function() { - function Autosave(field, key, resource) { +export default class Autosave { + constructor(field, key, resource) { this.field = field; this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); this.resource = resource; @@ -12,14 +13,10 @@ window.Autosave = (function() { this.key = 'autosave/' + key; this.field.data('autosave', this); this.restore(); - this.field.on('input', (function(_this) { - return function() { - return _this.save(); - }; - })(this)); + this.field.on('input', () => this.save()); } - Autosave.prototype.restore = function() { + restore() { var text; if (!this.isLocalStorageAvailable) return; @@ -40,9 +37,9 @@ window.Autosave = (function() { field.dispatchEvent(event); } } - }; + } - Autosave.prototype.save = function() { + save() { var text; text = this.field.val(); @@ -51,15 +48,13 @@ window.Autosave = (function() { } return this.reset(); - }; + } - Autosave.prototype.reset = function() { + reset() { if (!this.isLocalStorageAvailable) return; return window.localStorage.removeItem(this.key); - }; - - return Autosave; -})(); + } +} -export default window.Autosave; +window.Autosave = Autosave; diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 470c39c6f76..10f853066ca 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -1,9 +1,9 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */ /* global GitLab */ -/* global Autosave */ /* global dateFormat */ import Pikaday from 'pikaday'; +import Autosave from './autosave'; import UsersSelect from './users_select'; import GfmAutoComplete from './gfm_auto_complete'; import ZenMode from './zen_mode'; diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 790f78d2e11..a09f938a281 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -5,7 +5,7 @@ default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape, class-methods-use-this */ -/* global Autosave */ + /* global ResolveService */ /* global mrRefreshWidgetUrl */ @@ -21,7 +21,7 @@ import Flash from './flash'; import CommentTypeToggle from './comment_type_toggle'; import GLForm from './gl_form'; import loadAwardsHandler from './awards_handler'; -import './autosave'; +import Autosave from './autosave'; import './dropzone_input'; import TaskList from './task_list'; import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils'; diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 2ce52e4538a..ad384a1cc36 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -1,10 +1,9 @@ @@ -73,8 +91,13 @@ export default { :img-alt="imgAlt" :css-classes="imgCssClasses" :size="imgSize" - :tooltip-text="tooltipText" + :tooltip-text="avatarTooltipText" + :tooltip-placement="tooltipPlacement" + /> + >{{username}} diff --git a/spec/javascripts/vue_shared/components/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar_link_spec.js index 52e450e9ba5..ce75df6fc7b 100644 --- a/spec/javascripts/vue_shared/components/user_avatar_link_spec.js +++ b/spec/javascripts/vue_shared/components/user_avatar_link_spec.js @@ -11,6 +11,7 @@ describe('User Avatar Link Component', function () { imgCssClasses: 'myextraavatarclass', tooltipText: 'tooltip text', tooltipPlacement: 'bottom', + username: 'username', }; const UserAvatarLinkComponent = Vue.extend(UserAvatarLink); @@ -47,4 +48,42 @@ describe('User Avatar Link Component', function () { expect(this.userAvatarLink[key]).toBeDefined(); }); }); + + describe('no username', function () { + beforeEach(function (done) { + this.userAvatarLink.username = ''; + + Vue.nextTick(done); + }); + + it('should not render as a child element', function () { + expect(this.userAvatarLink.$el.querySelector('span')).toBeNull(); + }); + + it('should render avatar image tooltip', function () { + expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual(this.propsData.tooltipText); + }); + }); + + describe('username', function () { + it('should not render avatar image tooltip', function () { + expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual(''); + }); + + it('should render as a child element', function () { + expect(this.userAvatarLink.$el.querySelector('span')).toBeDefined(); + }); + + it('should render username prop in ', function () { + expect(this.userAvatarLink.$el.querySelector('span').innerText.trim()).toEqual(this.propsData.username); + }); + + it('should render text tooltip for ', function () { + expect(this.userAvatarLink.$el.querySelector('span').dataset.originalTitle).toEqual(this.propsData.tooltipText); + }); + + it('should render text tooltip placement for ', function () { + expect(this.userAvatarLink.$el.querySelector('span').getAttribute('tooltip-placement')).toEqual(this.propsData.tooltipPlacement); + }); + }); }); -- cgit v1.2.1 From 1ab8aeeefd2ee826485a0be9d1c862782eaba3d4 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 19 Oct 2017 10:36:20 +0100 Subject: Moves placeholders components into shared folder with documentation. Makes them easier to reuse in MR and Snippets comments --- .../notes/components/issue_discussion.vue | 4 +- .../notes/components/issue_notes_app.vue | 10 +-- .../notes/components/issue_placeholder_note.vue | 53 ---------------- .../components/issue_placeholder_system_note.vue | 21 ------- .../notes/components/issue_system_note.vue | 54 ---------------- .../components/notes/placeholder_note.vue | 70 +++++++++++++++++++++ .../components/notes/placeholder_system_note.vue | 29 +++++++++ .../vue_shared/components/notes/system_note.vue | 73 ++++++++++++++++++++++ .../unreleased/38178-fl-mr-notes-components.yml | 6 ++ .../components/issue_placeholder_note_spec.js | 39 ------------ .../issue_placeholder_system_note_spec.js | 24 ------- .../notes/components/issue_system_note_spec.js | 53 ---------------- .../components/notes/placeholder_note_spec.js | 39 ++++++++++++ .../notes/placeholder_system_note_spec.js | 25 ++++++++ .../components/notes/system_note_spec.js | 57 +++++++++++++++++ 15 files changed, 306 insertions(+), 251 deletions(-) delete mode 100644 app/assets/javascripts/notes/components/issue_placeholder_note.vue delete mode 100644 app/assets/javascripts/notes/components/issue_placeholder_system_note.vue delete mode 100644 app/assets/javascripts/notes/components/issue_system_note.vue create mode 100644 app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue create mode 100644 app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue create mode 100644 app/assets/javascripts/vue_shared/components/notes/system_note.vue create mode 100644 changelogs/unreleased/38178-fl-mr-notes-components.yml delete mode 100644 spec/javascripts/notes/components/issue_placeholder_note_spec.js delete mode 100644 spec/javascripts/notes/components/issue_placeholder_system_note_spec.js delete mode 100644 spec/javascripts/notes/components/issue_system_note_spec.js create mode 100644 spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js create mode 100644 spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js create mode 100644 spec/javascripts/vue_shared/components/notes/system_note_spec.js diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index baf43190d9e..0f13221b81e 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -9,8 +9,8 @@ import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; import issueNoteEditedText from './issue_note_edited_text.vue'; import issueNoteForm from './issue_note_form.vue'; - import placeholderNote from './issue_placeholder_note.vue'; - import placeholderSystemNote from './issue_placeholder_system_note.vue'; + import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; + import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; import autosave from '../mixins/autosave'; export default { diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index aecd1f957e5..5c9119644e3 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -5,10 +5,10 @@ import * as constants from '../constants'; import issueNote from './issue_note.vue'; import issueDiscussion from './issue_discussion.vue'; - import issueSystemNote from './issue_system_note.vue'; + import systemNote from '../../vue_shared/components/notes/system_note.vue'; import issueCommentForm from './issue_comment_form.vue'; - import placeholderNote from './issue_placeholder_note.vue'; - import placeholderSystemNote from './issue_placeholder_system_note.vue'; + import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; + import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; export default { @@ -37,7 +37,7 @@ components: { issueNote, issueDiscussion, - issueSystemNote, + systemNote, issueCommentForm, loadingIcon, placeholderNote, @@ -68,7 +68,7 @@ } return placeholderNote; } else if (note.individual_note) { - return note.notes[0].system ? issueSystemNote : issueNote; + return note.notes[0].system ? systemNote : issueNote; } return issueDiscussion; diff --git a/app/assets/javascripts/notes/components/issue_placeholder_note.vue b/app/assets/javascripts/notes/components/issue_placeholder_note.vue deleted file mode 100644 index 6921d91372f..00000000000 --- a/app/assets/javascripts/notes/components/issue_placeholder_note.vue +++ /dev/null @@ -1,53 +0,0 @@ - - - diff --git a/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue b/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue deleted file mode 100644 index 80a8ef56a83..00000000000 --- a/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/app/assets/javascripts/notes/components/issue_system_note.vue b/app/assets/javascripts/notes/components/issue_system_note.vue deleted file mode 100644 index 0cfb6522e77..00000000000 --- a/app/assets/javascripts/notes/components/issue_system_note.vue +++ /dev/null @@ -1,54 +0,0 @@ - - - diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue new file mode 100644 index 00000000000..e467ca56704 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue @@ -0,0 +1,70 @@ + + + diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue new file mode 100644 index 00000000000..d805fea8006 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue @@ -0,0 +1,29 @@ + + + diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue new file mode 100644 index 00000000000..98f8f32557d --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -0,0 +1,73 @@ + + + diff --git a/changelogs/unreleased/38178-fl-mr-notes-components.yml b/changelogs/unreleased/38178-fl-mr-notes-components.yml new file mode 100644 index 00000000000..244ccfb3071 --- /dev/null +++ b/changelogs/unreleased/38178-fl-mr-notes-components.yml @@ -0,0 +1,6 @@ +--- +title: Moves placeholders components into shared folder with documentation. Makes + them easier to reuse in MR and Snippets comments +merge_request: +author: +type: other diff --git a/spec/javascripts/notes/components/issue_placeholder_note_spec.js b/spec/javascripts/notes/components/issue_placeholder_note_spec.js deleted file mode 100644 index 6e5275087f3..00000000000 --- a/spec/javascripts/notes/components/issue_placeholder_note_spec.js +++ /dev/null @@ -1,39 +0,0 @@ -import Vue from 'vue'; -import issuePlaceholderNote from '~/notes/components/issue_placeholder_note.vue'; -import store from '~/notes/stores'; -import { userDataMock } from '../mock_data'; - -describe('issue placeholder system note component', () => { - let vm; - - beforeEach(() => { - const Component = Vue.extend(issuePlaceholderNote); - store.dispatch('setUserData', userDataMock); - vm = new Component({ - store, - propsData: { note: { body: 'Foo' } }, - }).$mount(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('user information', () => { - it('should render user avatar with link', () => { - expect(vm.$el.querySelector('.user-avatar-link').getAttribute('href')).toEqual(userDataMock.path); - expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(userDataMock.avatar_url); - }); - }); - - describe('note content', () => { - it('should render note header information', () => { - expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual(userDataMock.path); - expect(vm.$el.querySelector('.note-header-info .note-headline-light').textContent.trim()).toEqual(`@${userDataMock.username}`); - }); - - it('should render note body', () => { - expect(vm.$el.querySelector('.note-text p').textContent.trim()).toEqual('Foo'); - }); - }); -}); diff --git a/spec/javascripts/notes/components/issue_placeholder_system_note_spec.js b/spec/javascripts/notes/components/issue_placeholder_system_note_spec.js deleted file mode 100644 index d508a49f710..00000000000 --- a/spec/javascripts/notes/components/issue_placeholder_system_note_spec.js +++ /dev/null @@ -1,24 +0,0 @@ -import Vue from 'vue'; -import placeholderSystemNote from '~/notes/components/issue_placeholder_system_note.vue'; - -describe('issue placeholder system note component', () => { - let mountComponent; - beforeEach(() => { - const PlaceholderSystemNote = Vue.extend(placeholderSystemNote); - - mountComponent = props => new PlaceholderSystemNote({ - propsData: { - note: { - body: props, - }, - }, - }).$mount(); - }); - - it('should render system note placeholder with plain text', () => { - const vm = mountComponent('This is a placeholder'); - - expect(vm.$el.tagName).toEqual('LI'); - expect(vm.$el.querySelector('.timeline-content em').textContent.trim()).toEqual('This is a placeholder'); - }); -}); diff --git a/spec/javascripts/notes/components/issue_system_note_spec.js b/spec/javascripts/notes/components/issue_system_note_spec.js deleted file mode 100644 index c317ce32716..00000000000 --- a/spec/javascripts/notes/components/issue_system_note_spec.js +++ /dev/null @@ -1,53 +0,0 @@ -import Vue from 'vue'; -import issueSystemNote from '~/notes/components/issue_system_note.vue'; -import store from '~/notes/stores'; - -describe('issue system note', () => { - let vm; - let props; - - beforeEach(() => { - props = { - note: { - id: 1424, - author: { - id: 1, - name: 'Root', - username: 'root', - state: 'active', - avatar_url: 'path', - path: '/root', - }, - note_html: '

closed

', - system_note_icon_name: 'icon_status_closed', - created_at: '2017-08-02T10:51:58.559Z', - }, - }; - - store.dispatch('setTargetNoteHash', `note_${props.note.id}`); - - const Component = Vue.extend(issueSystemNote); - vm = new Component({ - store, - propsData: props, - }).$mount(); - }); - - it('should render a list item with correct id', () => { - expect(vm.$el.getAttribute('id')).toEqual(`note_${props.note.id}`); - }); - - it('should render target class is note is target note', () => { - expect(vm.$el.classList).toContain('target'); - }); - - it('should render svg icon', () => { - expect(vm.$el.querySelector('.timeline-icon svg')).toBeDefined(); - }); - - it('should render note header component', () => { - expect( - vm.$el.querySelector('.system-note-message').innerHTML, - ).toEqual(props.note.note_html); - }); -}); diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js b/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js new file mode 100644 index 00000000000..ba8ab0b2cd7 --- /dev/null +++ b/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; +import issuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue'; +import store from '~/notes/stores'; +import { userDataMock } from '../../../notes/mock_data'; + +describe('issue placeholder system note component', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(issuePlaceholderNote); + store.dispatch('setUserData', userDataMock); + vm = new Component({ + store, + propsData: { note: { body: 'Foo' } }, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('user information', () => { + it('should render user avatar with link', () => { + expect(vm.$el.querySelector('.user-avatar-link').getAttribute('href')).toEqual(userDataMock.path); + expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(userDataMock.avatar_url); + }); + }); + + describe('note content', () => { + it('should render note header information', () => { + expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual(userDataMock.path); + expect(vm.$el.querySelector('.note-header-info .note-headline-light').textContent.trim()).toEqual(`@${userDataMock.username}`); + }); + + it('should render note body', () => { + expect(vm.$el.querySelector('.note-text p').textContent.trim()).toEqual('Foo'); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js b/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js new file mode 100644 index 00000000000..7b8e6c330c2 --- /dev/null +++ b/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js @@ -0,0 +1,25 @@ +import Vue from 'vue'; +import placeholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; + +describe('placeholder system note component', () => { + let PlaceholderSystemNote; + let vm; + + beforeEach(() => { + PlaceholderSystemNote = Vue.extend(placeholderSystemNote); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render system note placeholder with plain text', () => { + vm = mountComponent(PlaceholderSystemNote, { + note: { body: 'This is a placeholder' }, + }); + + expect(vm.$el.tagName).toEqual('LI'); + expect(vm.$el.querySelector('.timeline-content em').textContent.trim()).toEqual('This is a placeholder'); + }); +}); diff --git a/spec/javascripts/vue_shared/components/notes/system_note_spec.js b/spec/javascripts/vue_shared/components/notes/system_note_spec.js new file mode 100644 index 00000000000..36aaf0a6c2e --- /dev/null +++ b/spec/javascripts/vue_shared/components/notes/system_note_spec.js @@ -0,0 +1,57 @@ +import Vue from 'vue'; +import issueSystemNote from '~/vue_shared/components/notes/system_note.vue'; +import store from '~/notes/stores'; + +describe('issue system note', () => { + let vm; + let props; + + beforeEach(() => { + props = { + note: { + id: 1424, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: 'path', + path: '/root', + }, + note_html: '

closed

', + system_note_icon_name: 'icon_status_closed', + created_at: '2017-08-02T10:51:58.559Z', + }, + }; + + store.dispatch('setTargetNoteHash', `note_${props.note.id}`); + + const Component = Vue.extend(issueSystemNote); + vm = new Component({ + store, + propsData: props, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render a list item with correct id', () => { + expect(vm.$el.getAttribute('id')).toEqual(`note_${props.note.id}`); + }); + + it('should render target class is note is target note', () => { + expect(vm.$el.classList).toContain('target'); + }); + + it('should render svg icon', () => { + expect(vm.$el.querySelector('.timeline-icon svg')).toBeDefined(); + }); + + it('should render note header component', () => { + expect( + vm.$el.querySelector('.system-note-message').innerHTML, + ).toEqual(props.note.note_html); + }); +}); -- cgit v1.2.1 From 6d04f3789cc16f7211fb3d465956bbd84c9430b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Thu, 19 Oct 2017 15:57:20 -0300 Subject: Avoid calling underlying methods on non-existing repos This saves us Rugged/gRPC invocations --- app/models/repository.rb | 9 +++++++-- spec/models/repository_spec.rb | 34 ++++++++++++++++++++++++++++------ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 4324ea46aac..8a1b81b5337 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1031,6 +1031,10 @@ class Repository if instance_variable_defined?(ivar) instance_variable_get(ivar) else + # If the repository doesn't exist and a fallback was specified we return + # that value inmediately. This saves us Rugged/gRPC invocations. + return fallback unless fallback.nil? || exists? + begin value = if memoize_only @@ -1040,8 +1044,9 @@ class Repository end instance_variable_set(ivar, value) rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository - # if e.g. HEAD or the entire repository doesn't exist we want to - # gracefully handle this and not cache anything. + # Even if the above `#exists?` check passes these errors might still + # occur (for example because of a non-existing HEAD). We want to + # gracefully handle this and not cache anything fallback end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 39d188f18af..874368ada67 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -2110,19 +2110,41 @@ describe Repository do end describe '#cache_method_output', :use_clean_rails_memory_store_caching do + let(:fallback) { 10 } + context 'with a non-existing repository' do - let(:value) do - repository.cache_method_output(:cats, fallback: 10) do - raise Rugged::ReferenceError + let(:project) { create(:project) } # No repository + + subject do + repository.cache_method_output(:cats, fallback: fallback) do + repository.cats_call_stub end end - it 'returns a fallback value' do - expect(value).to eq(10) + it 'returns the fallback value' do + expect(subject).to eq(fallback) + end + + it 'avoids calling the original method' do + expect(repository).not_to receive(:cats_call_stub) + + subject + end + end + + context 'with a method throwing a non-existing-repository error' do + subject do + repository.cache_method_output(:cats, fallback: fallback) do + raise Gitlab::Git::Repository::NoRepository + end + end + + it 'returns the fallback value' do + expect(subject).to eq(fallback) end it 'does not cache the data' do - value + subject expect(repository.instance_variable_defined?(:@cats)).to eq(false) expect(repository.send(:cache).exist?(:cats)).to eq(false) -- cgit v1.2.1 From 8bb17358b535fd288edb46bee389acf481a5e2f9 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sat, 21 Oct 2017 20:52:17 +0300 Subject: Update docs on creating MRs --- doc/gitlab-basics/add-merge-request.md | 23 +++++++++------------ doc/gitlab-basics/img/merge_request_new.png | Bin 2234 -> 0 bytes .../img/merge_request_select_branch.png | Bin 20332 -> 16668 bytes doc/gitlab-basics/img/project_navbar.png | Bin 3259 -> 0 bytes 4 files changed, 10 insertions(+), 13 deletions(-) delete mode 100644 doc/gitlab-basics/img/merge_request_new.png delete mode 100644 doc/gitlab-basics/img/project_navbar.png diff --git a/doc/gitlab-basics/add-merge-request.md b/doc/gitlab-basics/add-merge-request.md index bf01fe51dc3..5cc014419ad 100644 --- a/doc/gitlab-basics/add-merge-request.md +++ b/doc/gitlab-basics/add-merge-request.md @@ -3,31 +3,28 @@ Merge requests are useful to integrate separate changes that you've made to a project, on different branches. This is a brief guide on how to create a merge request. For more information, check the -[merge requests documentation](../user/project/merge_requests.md). +[merge requests documentation](../user/project/merge_requests/index.md). --- 1. Before you start, you should have already [created a branch](create-branch.md) and [pushed your changes](basic-git-commands.md) to GitLab. - -1. You can then go to the project where you'd like to merge your changes and - click on the **Merge requests** tab. - - ![Merge requests](img/project_navbar.png) - +1. Go to the project where you'd like to merge your changes and click on the + **Merge requests** tab. 1. Click on **New merge request** on the right side of the screen. - - ![New Merge Request](img/merge_request_new.png) - -1. Select a source branch and click on the **Compare branches and continue** button. +1. From there on, you have the option to select the source branch and the target + branch you'd like to compare to. The default target project is the upstream + repository, but you can choose to compare across any of its forks. ![Select a branch](img/merge_request_select_branch.png) +1. When ready, click on the **Compare branches and continue** button. 1. At a minimum, add a title and a description to your merge request. Optionally, select a user to review your merge request and to accept or close it. You may also select a milestone and labels. ![New merge request page](img/merge_request_page.png) -1. When ready, click on the **Submit merge request** button. Your merge request - will be ready to be approved and published. +1. When ready, click on the **Submit merge request** button. + +Your merge request will be ready to be approved and merged. diff --git a/doc/gitlab-basics/img/merge_request_new.png b/doc/gitlab-basics/img/merge_request_new.png deleted file mode 100644 index 6fcd7bebada..00000000000 Binary files a/doc/gitlab-basics/img/merge_request_new.png and /dev/null differ diff --git a/doc/gitlab-basics/img/merge_request_select_branch.png b/doc/gitlab-basics/img/merge_request_select_branch.png index 9f6b93943a9..57ea0e65f34 100644 Binary files a/doc/gitlab-basics/img/merge_request_select_branch.png and b/doc/gitlab-basics/img/merge_request_select_branch.png differ diff --git a/doc/gitlab-basics/img/project_navbar.png b/doc/gitlab-basics/img/project_navbar.png deleted file mode 100644 index be6f38ede32..00000000000 Binary files a/doc/gitlab-basics/img/project_navbar.png and /dev/null differ -- cgit v1.2.1 From 1cf35c3d1d274a24bf7b6283bf5d43ca0ffe8a10 Mon Sep 17 00:00:00 2001 From: George Andrinopoulos Date: Sun, 22 Oct 2017 17:50:58 +0300 Subject: Add case insensitive branches search --- app/finders/branches_finder.rb | 2 +- changelogs/unreleased/35199-case-insensitive-branches-search.yml | 5 +++++ spec/finders/branches_finder_spec.rb | 9 +++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/35199-case-insensitive-branches-search.yml diff --git a/app/finders/branches_finder.rb b/app/finders/branches_finder.rb index 533076585c0..852eac3647d 100644 --- a/app/finders/branches_finder.rb +++ b/app/finders/branches_finder.rb @@ -23,7 +23,7 @@ class BranchesFinder def filter_by_name(branches) if search - branches.select { |branch| branch.name.include?(search) } + branches.select { |branch| branch.name.upcase.include?(search.upcase) } else branches end diff --git a/changelogs/unreleased/35199-case-insensitive-branches-search.yml b/changelogs/unreleased/35199-case-insensitive-branches-search.yml new file mode 100644 index 00000000000..da2729e9e55 --- /dev/null +++ b/changelogs/unreleased/35199-case-insensitive-branches-search.yml @@ -0,0 +1,5 @@ +--- +title: Case insensitive search for branches +merge_request: 14995 +author: George Andrinopoulos +type: fixed diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb index 91f34973ba5..9e3f2c69606 100644 --- a/spec/finders/branches_finder_spec.rb +++ b/spec/finders/branches_finder_spec.rb @@ -46,6 +46,15 @@ describe BranchesFinder do expect(result.count).to eq(1) end + it 'filters branches by name ignoring letter case' do + branches_finder = described_class.new(repository, { search: 'FiX' }) + + result = branches_finder.execute + + expect(result.first.name).to eq('fix') + expect(result.count).to eq(1) + end + it 'does not find any branch with that name' do branches_finder = described_class.new(repository, { search: 'random' }) -- cgit v1.2.1 From 57a275791ff0e263dbe07ba7f1bfc4fdf53842cf Mon Sep 17 00:00:00 2001 From: Brett Walker Date: Mon, 23 Oct 2017 19:04:57 +0300 Subject: grab the correct username when confirming secondary email --- app/controllers/confirmations_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index 80ab681ed87..43b5d557429 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -15,7 +15,8 @@ class ConfirmationsController < Devise::ConfirmationsController if signed_in?(:user) after_sign_in(resource) else - Gitlab::AppLogger.info("Email Confirmed: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip}") + username = (_resource_name == :email ? resource.user.username : resource.username) + Gitlab::AppLogger.info("Email Confirmed: username=#{username} email=#{resource.email} ip=#{request.remote_ip}") flash[:notice] += " Please sign in." new_session_path(:user) end -- cgit v1.2.1 From 196df264467c2332fbeed1ff350ef176e92d0fd6 Mon Sep 17 00:00:00 2001 From: Brett Walker Date: Tue, 24 Oct 2017 09:23:35 +0300 Subject: fix to pass static-analysis --- app/controllers/confirmations_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index 43b5d557429..8ca01a6e2c6 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -10,12 +10,12 @@ class ConfirmationsController < Devise::ConfirmationsController users_almost_there_path end - def after_confirmation_path_for(_resource_name, resource) + def after_confirmation_path_for(resource_name, resource) # incoming resource can either be a :user or an :email if signed_in?(:user) after_sign_in(resource) else - username = (_resource_name == :email ? resource.user.username : resource.username) + username = (resource_name == :email ? resource.user.username : resource.username) Gitlab::AppLogger.info("Email Confirmed: username=#{username} email=#{resource.email} ip=#{request.remote_ip}") flash[:notice] += " Please sign in." new_session_path(:user) -- cgit v1.2.1 From cc5ba3d907c42175b70d3374c3772d0d25d12080 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Tue, 24 Oct 2017 10:35:21 +0300 Subject: Validate username/pw for Jiraservice, require them in the API --- app/models/project_services/jira_service.rb | 2 ++ changelogs/unreleased/api-configure-jira.yml | 5 +++++ lib/api/services.rb | 4 ++-- spec/models/project_services/jira_service_spec.rb | 14 ++++++++++++++ 4 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/api-configure-jira.yml diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 9ee3a533c1e..b487378edd2 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -3,6 +3,8 @@ class JiraService < IssueTrackerService validates :url, url: true, presence: true, if: :activated? validates :api_url, url: true, allow_blank: true + validates :username, presence: true, if: :activated? + validates :password, presence: true, if: :activated? prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id, :title, :description diff --git a/changelogs/unreleased/api-configure-jira.yml b/changelogs/unreleased/api-configure-jira.yml new file mode 100644 index 00000000000..40a644810c8 --- /dev/null +++ b/changelogs/unreleased/api-configure-jira.yml @@ -0,0 +1,5 @@ +--- +title: Validate username/pw for Jiraservice, require them in the API +merge_request: +author: Robert Schilling +type: fixed diff --git a/lib/api/services.rb b/lib/api/services.rb index 2cbd0517dc3..1e4f7c29633 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -313,13 +313,13 @@ module API desc: 'The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., https://jira-api.example.com' }, { - required: false, + required: true, name: :username, type: String, desc: 'The username of the user created to be used with GitLab/JIRA' }, { - required: false, + required: true, name: :password, type: String, desc: 'The password of the user created to be used with GitLab/JIRA' diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 63bf131cfc5..ad22fb2a386 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -24,6 +24,8 @@ describe JiraService do end it { is_expected.not_to validate_presence_of(:url) } + it { is_expected.not_to validate_presence_of(:username) } + it { is_expected.not_to validate_presence_of(:password) } end context 'validating urls' do @@ -54,6 +56,18 @@ describe JiraService do expect(service).not_to be_valid end + it 'is not valid when username is missing' do + service.username = nil + + expect(service).not_to be_valid + end + + it 'is not valid when password is missing' do + service.password = nil + + expect(service).not_to be_valid + end + it 'is valid when api url is a valid url' do service.api_url = 'http://jira.test.com/api' -- cgit v1.2.1 From 4fa8df677785b1945f6513ac8596637f1aa65b88 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Tue, 24 Oct 2017 10:40:26 +0300 Subject: Update API docs --- changelogs/unreleased/api-configure-jira.yml | 2 +- doc/api/services.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/changelogs/unreleased/api-configure-jira.yml b/changelogs/unreleased/api-configure-jira.yml index 40a644810c8..3ac52d573b0 100644 --- a/changelogs/unreleased/api-configure-jira.yml +++ b/changelogs/unreleased/api-configure-jira.yml @@ -1,5 +1,5 @@ --- title: Validate username/pw for Jiraservice, require them in the API -merge_request: +merge_request: 15025 author: Robert Schilling type: fixed diff --git a/doc/api/services.md b/doc/api/services.md index 49b87a4228c..6c8f196fd5c 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -478,8 +478,8 @@ PUT /projects/:id/services/jira | --------- | ---- | -------- | ----------- | | `url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project, e.g., `https://jira.example.com`. | | `project_key` | string | yes | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. | -| `username` | string | no | The username of the user created to be used with GitLab/JIRA. | -| `password` | string | no | The password of the user created to be used with GitLab/JIRA. | +| `username` | string | yes | The username of the user created to be used with GitLab/JIRA. | +| `password` | string | yes | The password of the user created to be used with GitLab/JIRA. | | `jira_issue_transition_id` | integer | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. | ### Delete JIRA service -- cgit v1.2.1 From 50aa340e374ac07dea82f99f39f2d0fff9b1175d Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Tue, 24 Oct 2017 12:17:41 +0300 Subject: Factories need to set required attributes --- spec/factories/services.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/factories/services.rb b/spec/factories/services.rb index c2674ce2d11..ccf63f3ffa4 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -38,6 +38,8 @@ FactoryGirl.define do active true properties( url: 'https://jira.example.com', + username: 'jira_user', + password: 'my-secret-password', project_key: 'jira-key' ) end -- cgit v1.2.1 From 525f043c29dc426a025b371de08818934f757083 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Tue, 24 Oct 2017 13:40:22 +0300 Subject: Update factories for git push service --- spec/support/jira_service_helper.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb index 0b5f66597fd..88a7aeba461 100644 --- a/spec/support/jira_service_helper.rb +++ b/spec/support/jira_service_helper.rb @@ -6,6 +6,8 @@ module JiraServiceHelper properties = { title: "JIRA tracker", url: JIRA_URL, + username: 'jira-user', + password: 'my-secret-password', project_key: "JIRA", jira_issue_transition_id: '1' } -- cgit v1.2.1 From a64601b9298d4b79bfc5d4f782b4dcc79ff33b74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Mon, 23 Oct 2017 14:16:10 -0300 Subject: Move all rugged operation for ff_merge inside Gitlab::Git We also delete some unused code related to the aforementioned feature. --- app/models/repository.rb | 18 +++--------- lib/gitlab/git/repository.rb | 10 +++++++ lib/gitlab/git_access.rb | 4 --- spec/lib/gitlab/git/repository_spec.rb | 54 ++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 18 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 4324ea46aac..327dbd2ea18 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -862,22 +862,12 @@ class Repository end def ff_merge(user, source, target_branch, merge_request: nil) - our_commit = rugged.branches[target_branch].target - their_commit = - if source.is_a?(Gitlab::Git::Commit) - source.raw_commit - else - rugged.lookup(source) - end + their_commit_id = commit(source)&.id + raise 'Invalid merge source' if their_commit_id.nil? - raise 'Invalid merge target' if our_commit.nil? - raise 'Invalid merge source' if their_commit.nil? + merge_request&.update(in_progress_merge_commit_sha: their_commit_id) - with_branch(user, target_branch) do |start_commit| - merge_request&.update(in_progress_merge_commit_sha: their_commit.oid) - - their_commit.oid - end + with_cache_hooks { raw.ff_merge(user, their_commit_id, target_branch) } end def revert( diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 59a54b48ed9..95265b41878 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -745,6 +745,16 @@ module Gitlab nil end + def ff_merge(user, source_sha, target_branch) + OperationService.new(user, self).with_branch(target_branch) do |our_commit| + raise ArgumentError, 'Invalid merge target' unless our_commit + + source_sha + end + rescue Rugged::ReferenceError + raise ArgumentError, 'Invalid merge source' + end + def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:) OperationService.new(user, self).with_branch( branch_name, diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 42b59c106e2..8998c4b1a83 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -215,10 +215,6 @@ module Gitlab ).exec end - def matching_merge_request?(newrev, branch_name) - Checks::MatchingMergeRequest.new(newrev, branch_name, project).match? - end - def deploy_key actor if deploy_key? end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index b2d2f770e3d..e3d320631bc 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1564,6 +1564,60 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#ff_merge' do + let(:repository) do + Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') + end + let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' } + let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } + let(:user) { build(:user) } + let(:target_branch) { 'test-ff-target-branch' } + + before do + repository.create_branch(target_branch, branch_head) + end + + after do + ensure_seeds + end + + subject { repository.ff_merge(user, source_sha, target_branch) } + + it 'performs a ff_merge' do + expect(subject.newrev).to eq(source_sha) + expect(subject.repo_created).to be(false) + expect(subject.branch_created).to be(false) + + expect(repository.commit(target_branch).id).to eq(source_sha) + end + + context 'with a non-existing target branch' do + subject { repository.ff_merge(user, source_sha, 'this-isnt-real') } + + it 'throws an ArgumentError' do + expect { subject }.to raise_error(ArgumentError) + end + end + + context 'with a non-existing source commit' do + let(:source_sha) { 'f001' } + + it 'throws an ArgumentError' do + expect { subject }.to raise_error(ArgumentError) + end + end + + context 'when the source sha is not a descendant of the branch head' do + let(:source_sha) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' } + + it "doesn't perform the ff_merge" do + expect { subject }.to raise_error(Gitlab::Git::CommitError) + + expect(repository.commit(target_branch).id).to eq(branch_head) + end + end + end + def create_remote_branch(repository, remote_name, branch_name, source_branch_name) source_branch = repository.branches.find { |branch| branch.name == source_branch_name } rugged = repository.rugged -- cgit v1.2.1 From cd0a7b475f77d7b7f76784e37b3cee7c547ce29b Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Thu, 19 Oct 2017 15:06:48 -0700 Subject: Remove filter icon from search bar --- app/assets/stylesheets/framework/filters.scss | 13 +------------ app/views/shared/issuable/_search_bar.html.haml | 1 - changelogs/unreleased/32318-filter-icon.yml | 5 +++++ 3 files changed, 6 insertions(+), 13 deletions(-) create mode 100644 changelogs/unreleased/32318-filter-icon.yml diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index b2847c348eb..0d80a85d521 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -65,7 +65,7 @@ display: flex; flex: 1; -webkit-flex: 1; - padding-left: 30px; + padding-left: 12px; position: relative; margin-bottom: 0; } @@ -221,10 +221,6 @@ box-shadow: 0 0 4px $search-input-focus-shadow-color; } - &.focus .fa-filter { - color: $common-gray-dark; - } - gl-emoji { display: inline-block; font-family: inherit; @@ -251,13 +247,6 @@ } } - .fa-filter { - position: absolute; - top: 10px; - left: 10px; - color: $gray-darkest; - } - .fa-times { right: 10px; color: $gray-darkest; diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 161b1c9fd72..fabb17c7340 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -25,7 +25,6 @@ %ul.tokens-container.list-unstyled %li.input-token %input.form-control.filtered-search{ search_filter_input_options(type) } - = icon('filter') #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown %ul{ data: { dropdown: true } } %li.filter-dropdown-item{ data: { action: 'submit' } } diff --git a/changelogs/unreleased/32318-filter-icon.yml b/changelogs/unreleased/32318-filter-icon.yml new file mode 100644 index 00000000000..71e7c2c4dac --- /dev/null +++ b/changelogs/unreleased/32318-filter-icon.yml @@ -0,0 +1,5 @@ +--- +title: Remove filter icon from search bar +merge_request: +author: +type: other -- cgit v1.2.1 From 5b88d1bb13de3feb7a1adf7696d86a2a5aa653f4 Mon Sep 17 00:00:00 2001 From: Niek Nijland Date: Wed, 25 Oct 2017 07:57:52 +0000 Subject: Fix broken table of content links --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 83e41a11e52..9b2ee157193 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,10 +21,10 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._ - [Workflow labels](#workflow-labels) - [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc) - [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc) - - [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-ci-discussion-edge-platform-etc) + - [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc) - [Priority labels (~Deliverable and ~Stretch)](#priority-labels-deliverable-and-stretch) - [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests) -- [Implement design & UI elements](#implement-design--ui-elements) +- [Implement design & UI elements](#implement-design-ui-elements) - [Issue tracker](#issue-tracker) - [Issue triaging](#issue-triaging) - [Feature proposals](#feature-proposals) -- cgit v1.2.1 From 7d20b693d07e79bbe8387ee2d2da4a2b6e3534e3 Mon Sep 17 00:00:00 2001 From: Victor Wu Date: Wed, 25 Oct 2017 08:26:58 +0000 Subject: Resolve "Remove overzealous tooltips in projects page tabs" --- app/views/dashboard/_projects_head.html.haml | 6 +++--- changelogs/unreleased/39419-remove-overzealous-tooltips.yml | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/39419-remove-overzealous-tooltips.yml diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index fd2ba9ac1ca..9038c4fbebd 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -6,13 +6,13 @@ .fade-right= icon('angle-right') %ul.nav-links.scrolling-tabs = nav_link(page: [dashboard_projects_path, root_path]) do - = link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do + = link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do Your projects = nav_link(page: starred_dashboard_projects_path) do - = link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do + = link_to starred_dashboard_projects_path, data: {placement: 'right'} do Starred projects = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do - = link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do + = link_to explore_root_path, data: {placement: 'right'} do Explore projects .nav-controls diff --git a/changelogs/unreleased/39419-remove-overzealous-tooltips.yml b/changelogs/unreleased/39419-remove-overzealous-tooltips.yml new file mode 100644 index 00000000000..d6cf60bebfa --- /dev/null +++ b/changelogs/unreleased/39419-remove-overzealous-tooltips.yml @@ -0,0 +1,5 @@ +--- +title: Remove overzealous tooltips in projects page tabs +merge_request: 15017 +author: +type: removed -- cgit v1.2.1 From 42a45392a92e44d63b49c86b67bacb82b0dea860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Wed, 25 Oct 2017 14:01:57 -0300 Subject: Fix the incorrect value being used to set GL_USERNAME on hooks --- lib/gitlab/git/hooks_service.rb | 2 +- spec/lib/gitlab/git/hooks_service_spec.rb | 28 +++++++++++++++------------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/gitlab/git/hooks_service.rb b/lib/gitlab/git/hooks_service.rb index c327e9b1616..f302b852b35 100644 --- a/lib/gitlab/git/hooks_service.rb +++ b/lib/gitlab/git/hooks_service.rb @@ -8,7 +8,7 @@ module Gitlab def execute(pusher, repository, oldrev, newrev, ref) @repository = repository @gl_id = pusher.gl_id - @gl_username = pusher.name + @gl_username = pusher.username @oldrev = oldrev @newrev = newrev @ref = ref diff --git a/spec/lib/gitlab/git/hooks_service_spec.rb b/spec/lib/gitlab/git/hooks_service_spec.rb index 51e4e3fdad1..3ed3feb4c74 100644 --- a/spec/lib/gitlab/git/hooks_service_spec.rb +++ b/spec/lib/gitlab/git/hooks_service_spec.rb @@ -1,24 +1,26 @@ require 'spec_helper' describe Gitlab::Git::HooksService, seed_helper: true do - let(:user) { Gitlab::Git::User.new('janedoe', 'Jane Doe', 'janedoe@example.com', 'user-456') } + let(:gl_id) { 'user-456' } + let(:gl_username) { 'janedoe' } + let(:user) { Gitlab::Git::User.new(gl_username, 'Jane Doe', 'janedoe@example.com', gl_id) } let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, 'project-123') } let(:service) { described_class.new } - - before do - @blankrev = Gitlab::Git::BLANK_SHA - @oldrev = SeedRepo::Commit::PARENT_ID - @newrev = SeedRepo::Commit::ID - @ref = 'refs/heads/feature' - end + let(:blankrev) { Gitlab::Git::BLANK_SHA } + let(:oldrev) { SeedRepo::Commit::PARENT_ID } + let(:newrev) { SeedRepo::Commit::ID } + let(:ref) { 'refs/heads/feature' } describe '#execute' do context 'when receive hooks were successful' do - it 'calls post-receive hook' do - hook = double(trigger: [true, nil]) + let(:hook) { double(:hook) } + + it 'calls all three hooks' do expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook) + expect(hook).to receive(:trigger).with(gl_id, gl_username, blankrev, newrev, ref) + .exactly(3).times.and_return([true, nil]) - service.execute(user, repository, @blankrev, @newrev, @ref) { } + service.execute(user, repository, blankrev, newrev, ref) { } end end @@ -28,7 +30,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do expect(service).not_to receive(:run_hook).with('post-receive') expect do - service.execute(user, repository, @blankrev, @newrev, @ref) + service.execute(user, repository, blankrev, newrev, ref) end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) end end @@ -40,7 +42,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do expect(service).not_to receive(:run_hook).with('post-receive') expect do - service.execute(user, repository, @blankrev, @newrev, @ref) + service.execute(user, repository, blankrev, newrev, ref) end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) end end -- cgit v1.2.1 From 294f40e2c8f51239bfa0e3514e7fe4f3c8ae00cb Mon Sep 17 00:00:00 2001 From: Francisco Lopez Date: Thu, 24 Aug 2017 16:34:36 +0200 Subject: Added ssh fingerprint, gitlab ci and pages information in an instance configuration page Closes #25142 --- app/controllers/help_controller.rb | 4 + app/helpers/instance_configuration_helper.rb | 18 ++++ app/models/instance_configuration.rb | 71 ++++++++++++++ app/views/help/index.html.haml | 2 + app/views/help/instance_configuration.html.haml | 17 ++++ .../instance_configuration/_gitlab_ci.html.haml | 24 +++++ .../instance_configuration/_gitlab_pages.html.haml | 35 +++++++ .../instance_configuration/_ssh_info.html.haml | 27 +++++ .../unreleased/feature-ssh_host_fingerprint.yml | 5 + config/routes/help.rb | 9 +- spec/factories/instance_configuration.rb | 5 + spec/fixtures/ssh_host_example_key.pub | 1 + spec/helpers/instance_configuration_helper_spec.rb | 51 ++++++++++ spec/models/instance_configuration_spec.rb | 109 +++++++++++++++++++++ spec/views/help/index.html.haml_spec.rb | 8 ++ .../help/instance_configuration.html.haml_spec.rb | 29 ++++++ 16 files changed, 411 insertions(+), 4 deletions(-) create mode 100644 app/helpers/instance_configuration_helper.rb create mode 100644 app/models/instance_configuration.rb create mode 100644 app/views/help/instance_configuration.html.haml create mode 100644 app/views/help/instance_configuration/_gitlab_ci.html.haml create mode 100644 app/views/help/instance_configuration/_gitlab_pages.html.haml create mode 100644 app/views/help/instance_configuration/_ssh_info.html.haml create mode 100644 changelogs/unreleased/feature-ssh_host_fingerprint.yml create mode 100644 spec/factories/instance_configuration.rb create mode 100644 spec/fixtures/ssh_host_example_key.pub create mode 100644 spec/helpers/instance_configuration_helper_spec.rb create mode 100644 spec/models/instance_configuration_spec.rb create mode 100644 spec/views/help/instance_configuration.html.haml_spec.rb diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index 572915a4930..38f379dbf4f 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -57,6 +57,10 @@ class HelpController < ApplicationController def shortcuts end + def instance_configuration + @instance_configuration = InstanceConfiguration.new + end + def ui @user = User.new(id: 0, name: 'John Doe', username: '@johndoe') end diff --git a/app/helpers/instance_configuration_helper.rb b/app/helpers/instance_configuration_helper.rb new file mode 100644 index 00000000000..cee319f20bc --- /dev/null +++ b/app/helpers/instance_configuration_helper.rb @@ -0,0 +1,18 @@ +module InstanceConfigurationHelper + def instance_configuration_cell_html(value, &block) + return '-' unless value.to_s.presence + + block_given? ? yield(value) : value + end + + def instance_configuration_host(host) + @instance_configuration_host ||= instance_configuration_cell_html(host).capitalize + end + + # Value must be in bytes + def instance_configuration_human_size_cell(value) + instance_configuration_cell_html(value) do |v| + number_to_human_size(v, strip_insignificant_zeros: true, significant: false) + end + end +end diff --git a/app/models/instance_configuration.rb b/app/models/instance_configuration.rb new file mode 100644 index 00000000000..b30b707e5fe --- /dev/null +++ b/app/models/instance_configuration.rb @@ -0,0 +1,71 @@ +require 'resolv' + +class InstanceConfiguration + SSH_ALGORITHMS = %w(DSA ECDSA ED25519 RSA).freeze + SSH_ALGORITHMS_PATH = '/etc/ssh/'.freeze + CACHE_KEY = 'instance_configuration'.freeze + EXPIRATION_TIME = 24.hours + + def settings + @configuration ||= Rails.cache.fetch(CACHE_KEY, expires_in: EXPIRATION_TIME) do + { ssh_algorithms_hashes: ssh_algorithms_hashes, + host: host, + gitlab_pages: gitlab_pages, + gitlab_ci: gitlab_ci }.deep_symbolize_keys + end + end + + private + + def ssh_algorithms_hashes + SSH_ALGORITHMS.map { |algo| ssh_algorithm_hashes(algo) }.compact + end + + def host + Settings.gitlab.host + end + + def gitlab_pages + Settings.pages.to_h.merge(ip_address: resolv_dns(Settings.pages.host)) + end + + def resolv_dns(dns) + Resolv.getaddress(dns) + rescue Resolv::ResolvError + end + + def gitlab_ci + Settings.gitlab_ci + .to_h + .merge(artifacts_max_size: { value: Settings.artifacts.max_size&.megabytes, + default: 100.megabytes }) + end + + def ssh_algorithm_file(algorithm) + File.join(SSH_ALGORITHMS_PATH, "ssh_host_#{algorithm.downcase}_key.pub") + end + + def ssh_algorithm_hashes(algorithm) + content = ssh_algorithm_file_content(algorithm) + return unless content.present? + + { name: algorithm, + md5: ssh_algorithm_md5(content), + sha256: ssh_algorithm_sha256(content) } + end + + def ssh_algorithm_file_content(algorithm) + file = ssh_algorithm_file(algorithm) + return unless File.exist?(file) + + File.read(file) + end + + def ssh_algorithm_md5(ssh_file_content) + OpenSSL::Digest::MD5.hexdigest(ssh_file_content).scan(/../).join(':') + end + + def ssh_algorithm_sha256(ssh_file_content) + OpenSSL::Digest::SHA256.hexdigest(ssh_file_content) + end +end diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index c25eae63eec..d0c2e0b1d69 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -11,6 +11,7 @@ %span= Gitlab::VERSION %small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab::REVISION) = version_status_badge + %p.slead GitLab is open source software to collaborate on code. %br @@ -23,6 +24,7 @@ Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises. %br Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}. + %p= link_to 'Check the current instance configuration ', help_instance_configuration_url %hr .row.prepend-top-default diff --git a/app/views/help/instance_configuration.html.haml b/app/views/help/instance_configuration.html.haml new file mode 100644 index 00000000000..f09e3825a4b --- /dev/null +++ b/app/views/help/instance_configuration.html.haml @@ -0,0 +1,17 @@ +- page_title 'Instance Configuration' +.wiki.documentation + %h1 Instance Configuration + + %p + In this page you will find information about the settings that are used in your current instance. + + = render 'help/instance_configuration/ssh_info' + = render 'help/instance_configuration/gitlab_pages' + = render 'help/instance_configuration/gitlab_ci' + %p + %strong Table of contents + + %ul + = content_for :table_content + + = content_for :settings_content diff --git a/app/views/help/instance_configuration/_gitlab_ci.html.haml b/app/views/help/instance_configuration/_gitlab_ci.html.haml new file mode 100644 index 00000000000..7fa8bd086d4 --- /dev/null +++ b/app/views/help/instance_configuration/_gitlab_ci.html.haml @@ -0,0 +1,24 @@ +- content_for :table_content do + %li= link_to 'GitLab CI', '#gitlab-ci' + +- content_for :settings_content do + %h2#gitlab-ci + GitLab CI + + %p + Below are the current settings regarding + = succeed('.') { link_to('GitLab CI', 'https://about.gitlab.com/gitlab-ci', target: '_blank') } + + .table-responsive + %table + %thead + %tr + %th Setting + %th= instance_configuration_host(@instance_configuration.settings[:host]) + %th Default + %tbody + %tr + - artifacts_size = @instance_configuration.settings[:gitlab_ci][:artifacts_max_size] + %td Artifacts maximum size + %td= instance_configuration_human_size_cell(artifacts_size[:value]) + %td= instance_configuration_human_size_cell(artifacts_size[:default]) diff --git a/app/views/help/instance_configuration/_gitlab_pages.html.haml b/app/views/help/instance_configuration/_gitlab_pages.html.haml new file mode 100644 index 00000000000..bdd77730dcc --- /dev/null +++ b/app/views/help/instance_configuration/_gitlab_pages.html.haml @@ -0,0 +1,35 @@ +- gitlab_pages = @instance_configuration.settings[:gitlab_pages] +- content_for :table_content do + %li= link_to 'GitLab Pages', '#gitlab-pages' + +- content_for :settings_content do + %h2#gitlab-pages + GitLab Pages + + %p + Below are the settings for + = succeed('.') { link_to('Gitlab Pages', gitlab_pages[:url], target: '_blank') } + .table-responsive + %table + %thead + %tr + %th Setting + %th= instance_configuration_host(@instance_configuration.settings[:host]) + %tbody + %tr + %td Domain Name + %td + %code= instance_configuration_cell_html(gitlab_pages[:host]) + %tr + %td IP Address + %td + %code= instance_configuration_cell_html(gitlab_pages[:ip_address]) + %tr + %td Port + %td + %code= instance_configuration_cell_html(gitlab_pages[:port]) + %br + + %p + The maximum size of your Pages site is regulated by the artifacts maximum + size which is part of #{succeed('.') { link_to('GitLab CI', '#gitlab-ci') }} diff --git a/app/views/help/instance_configuration/_ssh_info.html.haml b/app/views/help/instance_configuration/_ssh_info.html.haml new file mode 100644 index 00000000000..987cc61b3f6 --- /dev/null +++ b/app/views/help/instance_configuration/_ssh_info.html.haml @@ -0,0 +1,27 @@ +- ssh_info = @instance_configuration.settings[:ssh_algorithms_hashes] +- if ssh_info.any? + - content_for :table_content do + %li= link_to 'SSH host keys fingerprints', '#ssh-host-keys-fingerprints' + + - content_for :settings_content do + %h2#ssh-host-keys-fingerprints + SSH host keys fingerprints + + %p + Below are the fingerprints for the current instance SSH host keys. + + .table-responsive + %table + %thead + %tr + %th Algorithm + %th MD5 + %th SHA256 + %tbody + - ssh_info.each do |algorithm| + %tr + %td= algorithm[:name] + %td + %code= instance_configuration_cell_html(algorithm[:md5]) + %td + %code= instance_configuration_cell_html(algorithm[:sha256]) diff --git a/changelogs/unreleased/feature-ssh_host_fingerprint.yml b/changelogs/unreleased/feature-ssh_host_fingerprint.yml new file mode 100644 index 00000000000..04f9fd1d6ed --- /dev/null +++ b/changelogs/unreleased/feature-ssh_host_fingerprint.yml @@ -0,0 +1,5 @@ +--- +title: Automatic configuration settings page +merge_request: 13850 +author: Francisco Lopez +type: added diff --git a/config/routes/help.rb b/config/routes/help.rb index d53822da9ec..2ea8bfd7aed 100644 --- a/config/routes/help.rb +++ b/config/routes/help.rb @@ -1,4 +1,5 @@ -get 'help' => 'help#index' -get 'help/shortcuts' => 'help#shortcuts' -get 'help/ui' => 'help#ui' -get 'help/*path' => 'help#show', as: :help_page +get 'help' => 'help#index' +get 'help/shortcuts' => 'help#shortcuts' +get 'help/ui' => 'help#ui' +get 'help/instance_configuration' => 'help#instance_configuration' +get 'help/*path' => 'help#show', as: :help_page diff --git a/spec/factories/instance_configuration.rb b/spec/factories/instance_configuration.rb new file mode 100644 index 00000000000..406c7c3caf1 --- /dev/null +++ b/spec/factories/instance_configuration.rb @@ -0,0 +1,5 @@ +FactoryGirl.define do + factory :instance_configuration do + skip_create + end +end diff --git a/spec/fixtures/ssh_host_example_key.pub b/spec/fixtures/ssh_host_example_key.pub new file mode 100644 index 00000000000..6bac42b3ad0 --- /dev/null +++ b/spec/fixtures/ssh_host_example_key.pub @@ -0,0 +1 @@ +random content diff --git a/spec/helpers/instance_configuration_helper_spec.rb b/spec/helpers/instance_configuration_helper_spec.rb new file mode 100644 index 00000000000..5d716b9191d --- /dev/null +++ b/spec/helpers/instance_configuration_helper_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe InstanceConfigurationHelper do + describe '#instance_configuration_cell_html' do + describe 'if not block is passed' do + it 'returns the parameter if present' do + expect(helper.instance_configuration_cell_html('gitlab')).to eq('gitlab') + end + + it 'returns "-" if the parameter is blank' do + expect(helper.instance_configuration_cell_html(nil)).to eq('-') + expect(helper.instance_configuration_cell_html('')).to eq('-') + end + end + + describe 'if a block is passed' do + let(:upcase_block) { ->(value) { value.upcase } } + + it 'returns the result of the block' do + expect(helper.instance_configuration_cell_html('gitlab', &upcase_block)).to eq('GITLAB') + expect(helper.instance_configuration_cell_html('gitlab') { |v| v.upcase }).to eq('GITLAB') + end + + it 'returns "-" if the parameter is blank' do + expect(helper.instance_configuration_cell_html(nil, &upcase_block)).to eq('-') + expect(helper.instance_configuration_cell_html(nil) { |v| v.upcase }).to eq('-') + expect(helper.instance_configuration_cell_html('', &upcase_block)).to eq('-') + end + end + + it 'boolean are valid values to display' do + expect(helper.instance_configuration_cell_html(true)).to eq(true) + expect(helper.instance_configuration_cell_html(false)).to eq(false) + end + end + + describe '#instance_configuration_human_size_cell' do + it 'returns "-" if the parameter is blank' do + expect(helper.instance_configuration_human_size_cell(nil)).to eq('-') + expect(helper.instance_configuration_human_size_cell('')).to eq('-') + end + + it 'accepts the value in bytes' do + expect(helper.instance_configuration_human_size_cell(1024)).to eq('1 KB') + end + + it 'returns the value in human size readable format' do + expect(helper.instance_configuration_human_size_cell(1048576)).to eq('1 MB') + end + end +end diff --git a/spec/models/instance_configuration_spec.rb b/spec/models/instance_configuration_spec.rb new file mode 100644 index 00000000000..8548fff5c76 --- /dev/null +++ b/spec/models/instance_configuration_spec.rb @@ -0,0 +1,109 @@ +require 'spec_helper' + +RSpec.describe InstanceConfiguration do + context 'without cache' do + describe '#settings' do + describe '#ssh_algorithms_hashes' do + let(:md5) { '54:e0:f8:70:d6:4f:4c:b1:b3:02:44:77:cf:cd:0d:fc' } + let(:sha256) { '9327f0d15a48c4d9f6a3aee65a1825baf9a3412001c98169c5fd022ac27762fc' } + + it 'does not return anything if file does not exist' do + stub_pub_file(exist: false) + + expect(subject.settings[:ssh_algorithms_hashes]).to be_empty + end + + it 'does not return anything if file is empty' do + stub_pub_file + + allow(File).to receive(:read).and_return('') + + expect(subject.settings[:ssh_algorithms_hashes]).to be_empty + end + + it 'returns the md5 and sha256 if file valid and exists' do + stub_pub_file + + result = subject.settings[:ssh_algorithms_hashes].select { |o| o[:md5] == md5 && o[:sha256] == sha256 } + + expect(result.size).to eq(InstanceConfiguration::SSH_ALGORITHMS.size) + end + + def stub_pub_file(exist: true) + path = 'spec/fixtures/ssh_host_example_key.pub' + path << 'random' unless exist + allow(subject).to receive(:ssh_algorithm_file).and_return(Rails.root.join(path)) + end + end + + describe '#host' do + it 'returns current instance host' do + allow(Settings.gitlab).to receive(:host).and_return('exampledomain') + + expect(subject.settings[:host]).to eq(Settings.gitlab.host) + end + end + + describe '#gitlab_pages' do + let(:gitlab_pages) { subject.settings[:gitlab_pages] } + it 'returns Settings.pages' do + gitlab_pages.delete(:ip_address) + + expect(gitlab_pages).to eq(Settings.pages.symbolize_keys) + end + + it 'returns the Gitlab\'s pages host ip address' do + expect(gitlab_pages.keys).to include(:ip_address) + end + + it 'returns the ip address as nil if the domain is invalid' do + allow(Settings.pages).to receive(:host).and_return('exampledomain') + + expect(gitlab_pages[:ip_address]).to eq nil + end + + it 'returns the ip address of the domain' do + allow(Settings.pages).to receive(:host).and_return('localhost') + + expect(gitlab_pages[:ip_address]).to eq('127.0.0.1').or eq('::1') + end + end + + describe '#gitlab_ci' do + let(:gitlab_ci) { subject.settings[:gitlab_ci] } + it 'returns Settings.gitalb_ci' do + gitlab_ci.delete(:artifacts_max_size) + + expect(gitlab_ci).to eq(Settings.gitlab_ci.symbolize_keys) + end + + it 'returns the key artifacts_max_size' do + expect(gitlab_ci.keys).to include(:artifacts_max_size) + end + end + end + end + + context 'with cache', :use_clean_rails_memory_store_caching do + it 'caches settings content' do + expect(Rails.cache.read(described_class::CACHE_KEY)).to be_nil + + settings = subject.settings + + expect(Rails.cache.read(described_class::CACHE_KEY)).to eq(settings) + end + + describe 'cached settings' do + before do + subject.settings + end + + it 'expires after EXPIRATION_TIME' do + allow(Time).to receive(:now).and_return(Time.now + described_class::EXPIRATION_TIME) + Rails.cache.cleanup + + expect(Rails.cache.read(described_class::CACHE_KEY)).to eq(nil) + end + end + end +end diff --git a/spec/views/help/index.html.haml_spec.rb b/spec/views/help/index.html.haml_spec.rb index c030129559e..0a78606171d 100644 --- a/spec/views/help/index.html.haml_spec.rb +++ b/spec/views/help/index.html.haml_spec.rb @@ -25,6 +25,14 @@ describe 'help/index' do end end + describe 'instance configuration link' do + it 'is visible to guests' do + render + + expect(rendered).to have_link(nil, help_instance_configuration_url) + end + end + def stub_user(user = double) allow(view).to receive(:user_signed_in?).and_return(user) end diff --git a/spec/views/help/instance_configuration.html.haml_spec.rb b/spec/views/help/instance_configuration.html.haml_spec.rb new file mode 100644 index 00000000000..f30b5881fde --- /dev/null +++ b/spec/views/help/instance_configuration.html.haml_spec.rb @@ -0,0 +1,29 @@ +require 'rails_helper' + +describe 'help/instance_configuration' do + describe 'General Sections:' do + let(:instance_configuration) { build(:instance_configuration)} + let(:settings) { instance_configuration.settings } + let(:ssh_settings) { settings[:ssh_algorithms_hashes] } + + before do + assign(:instance_configuration, instance_configuration) + end + + it 'has links to several sections' do + render + + expect(rendered).to have_link(nil, '#ssh-host-keys-fingerprints') if ssh_settings.any? + expect(rendered).to have_link(nil, '#gitlab-pages') + expect(rendered).to have_link(nil, '#gitlab-ci') + end + + it 'has several sections' do + render + + expect(rendered).to have_css('h2#ssh-host-keys-fingerprints') if ssh_settings.any? + expect(rendered).to have_css('h2#gitlab-pages') + expect(rendered).to have_css('h2#gitlab-ci') + end + end +end -- cgit v1.2.1 From acfa9090ec51a5944c4b330330075c4a47bc6cf1 Mon Sep 17 00:00:00 2001 From: Andrew Davis Date: Thu, 26 Oct 2017 00:40:19 +0000 Subject: Clarify the language around External Group membership with SAML SSO to clarify that this will NOT add users to GitLab Groups. --- doc/integration/saml.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/doc/integration/saml.md b/doc/integration/saml.md index b5b245c626f..3ae98adc465 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -132,14 +132,17 @@ On the sign in page there should now be a SAML button below the regular sign in Click the icon to begin the authentication process. If everything goes well the user will be returned to GitLab and will be signed in. -## External Groups +## Marking Users as External based on SAML Groups >**Note:** This setting is only available on GitLab 8.7 and above. -SAML login includes support for external groups. You can define in the SAML -settings which groups, to which your users belong in your IdP, you wish to be -marked as [external](../user/permissions.md). +SAML login includes support for automatically identifying whether a user should +be considered an [external](../user/permissions.md) user based on the user's group +membership in the SAML identity provider. This feature **does not** allow you to +automatically add users to GitLab [Groups](../user/group/index.md), it simply +allows you to mark users as External if they are members of certain groups in the +Identity Provider. ### Requirements -- cgit v1.2.1 From efdf51d838e68d954b0f06a6e01b11db26b9aba3 Mon Sep 17 00:00:00 2001 From: Andrew Newdigate Date: Thu, 26 Oct 2017 09:52:52 +0000 Subject: Make local_branches OPT_OUT --- lib/gitlab/git/repository.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 95265b41878..408616d174b 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -166,7 +166,7 @@ module Gitlab end def local_branches(sort_by: nil) - gitaly_migrate(:local_branches) do |is_enabled| + gitaly_migrate(:local_branches, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| if is_enabled gitaly_ref_client.local_branches(sort_by: sort_by) else -- cgit v1.2.1 From 76becfb5b4cf9030f4098e7eab435c8e42dd8a45 Mon Sep 17 00:00:00 2001 From: Ahmad Sherif Date: Mon, 23 Oct 2017 23:05:59 +0200 Subject: Add path attribute to WikiFile class Fixes #39420 --- changelogs/unreleased/fix-add-path-attr-to-wiki-file.yml | 5 +++++ lib/gitlab/git/wiki_file.rb | 3 ++- spec/lib/banzai/filter/gollum_tags_filter_spec.rb | 10 +++++++--- 3 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/fix-add-path-attr-to-wiki-file.yml diff --git a/changelogs/unreleased/fix-add-path-attr-to-wiki-file.yml b/changelogs/unreleased/fix-add-path-attr-to-wiki-file.yml new file mode 100644 index 00000000000..0847b5f6733 --- /dev/null +++ b/changelogs/unreleased/fix-add-path-attr-to-wiki-file.yml @@ -0,0 +1,5 @@ +--- +title: Fix broken wiki pages that link to a wiki file +merge_request: 15019 +author: +type: fixed diff --git a/lib/gitlab/git/wiki_file.rb b/lib/gitlab/git/wiki_file.rb index 527f2a44dea..84335aca4bc 100644 --- a/lib/gitlab/git/wiki_file.rb +++ b/lib/gitlab/git/wiki_file.rb @@ -1,7 +1,7 @@ module Gitlab module Git class WikiFile - attr_reader :mime_type, :raw_data, :name + attr_reader :mime_type, :raw_data, :name, :path # This class is meant to be serializable so that it can be constructed # by Gitaly and sent over the network to GitLab. @@ -13,6 +13,7 @@ module Gitlab @mime_type = gollum_file.mime_type @raw_data = gollum_file.raw_data @name = gollum_file.name + @path = gollum_file.path end end end diff --git a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb index 97d612e6347..ca76d6f0881 100644 --- a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb +++ b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb @@ -15,9 +15,13 @@ describe Banzai::Filter::GollumTagsFilter do context 'linking internal images' do it 'creates img tag if image exists' do - file = Gollum::File.new(project_wiki.wiki) - expect(file).to receive(:path).and_return('images/image.jpg') - expect(project_wiki).to receive(:find_file).with('images/image.jpg').and_return(file) + gollum_file_double = double('Gollum::File', + mime_type: 'image/jpeg', + name: 'images/image.jpg', + path: 'images/image.jpg', + raw_data: '') + wiki_file = Gitlab::Git::WikiFile.new(gollum_file_double) + expect(project_wiki).to receive(:find_file).with('images/image.jpg').and_return(wiki_file) tag = '[[images/image.jpg]]' doc = filter("See #{tag}", project_wiki: project_wiki) -- cgit v1.2.1 From 6798bab12a0faabf43f61eb65561b9f058824e4d Mon Sep 17 00:00:00 2001 From: Alessio Caiazza Date: Thu, 26 Oct 2017 16:04:28 +0200 Subject: Remove duped tests Likely caused by EE conflicts resolution --- .../project_services/kubernetes_service_spec.rb | 28 ---------------------- 1 file changed, 28 deletions(-) diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 537cdadd528..69da4375245 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -124,34 +124,6 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do end end - describe '#actual_namespace' do - subject { service.actual_namespace } - - it "returns the default namespace" do - is_expected.to eq(service.send(:default_namespace)) - end - - context 'when namespace is specified' do - before do - service.namespace = 'my-namespace' - end - - it "returns the user-namespace" do - is_expected.to eq('my-namespace') - end - end - - context 'when service is not assigned to project' do - before do - service.project = nil - end - - it "does not return namespace" do - is_expected.to be_nil - end - end - end - describe '#test' do let(:discovery_url) { 'https://kubernetes.example.com/api/v1' } -- cgit v1.2.1 From 7d8eb4ddb09f96fe19722476144d73d32228f3a8 Mon Sep 17 00:00:00 2001 From: Jarka Kadlecova Date: Thu, 26 Oct 2017 13:59:28 +0300 Subject: Fix bitbucket login --- changelogs/unreleased/39495-fix-bitbucket-login.yml | 5 +++++ lib/omni_auth/strategies/bitbucket.rb | 4 ++++ 2 files changed, 9 insertions(+) create mode 100644 changelogs/unreleased/39495-fix-bitbucket-login.yml diff --git a/changelogs/unreleased/39495-fix-bitbucket-login.yml b/changelogs/unreleased/39495-fix-bitbucket-login.yml new file mode 100644 index 00000000000..b48d557108b --- /dev/null +++ b/changelogs/unreleased/39495-fix-bitbucket-login.yml @@ -0,0 +1,5 @@ +--- +title: Fix bitbucket login +merge_request: 15051 +author: +type: fixed diff --git a/lib/omni_auth/strategies/bitbucket.rb b/lib/omni_auth/strategies/bitbucket.rb index 5a7d67c2390..ce1bdfe6ee4 100644 --- a/lib/omni_auth/strategies/bitbucket.rb +++ b/lib/omni_auth/strategies/bitbucket.rb @@ -36,6 +36,10 @@ module OmniAuth email_response = access_token.get('api/2.0/user/emails').parsed @emails ||= email_response && email_response['values'] || nil end + + def callback_url + options[:redirect_uri] || (full_host + script_name + callback_path) + end end end end -- cgit v1.2.1 From 17b4367045ec7943717d11cf1cafc19560da5f40 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 26 Oct 2017 12:04:07 +0100 Subject: Revert "Merge branch '36670-remove-edit-form' into 'master'" This reverts commit 915e35a2992a4e51db2ac32aac8d7a29b1f4449e, reversing changes made to 9533786f522e358f372d8a0ec4b4990ae9d88f37. --- app/controllers/projects/issues_controller.rb | 10 +- app/views/projects/issues/edit.html.haml | 7 ++ .../unreleased/39441-bring-edit-form-back.yml | 5 + .../controllers/projects/issues_controller_spec.rb | 23 +++++ spec/features/issues/form_spec.rb | 49 +++++++++- spec/features/issues_spec.rb | 108 +++++++++++++++++++-- .../security/project/internal_access_spec.rb | 15 +++ .../security/project/private_access_spec.rb | 15 +++ .../security/project/public_access_spec.rb | 15 +++ spec/support/update_invalid_issuable.rb | 27 +++--- 10 files changed, 246 insertions(+), 28 deletions(-) create mode 100644 app/views/projects/issues/edit.html.haml create mode 100644 changelogs/unreleased/39441-bring-edit-form-back.yml diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index b7a108a0ebd..fe1334c0cfe 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -16,7 +16,7 @@ class Projects::IssuesController < Projects::ApplicationController before_action :authorize_create_issue!, only: [:new, :create] # Allow modify issue - before_action :authorize_update_issue!, only: [:update, :move] + before_action :authorize_update_issue!, only: [:edit, :update, :move] # Allow create a new branch and empty WIP merge request from current issue before_action :authorize_create_merge_request!, only: [:create_merge_request] @@ -63,6 +63,10 @@ class Projects::IssuesController < Projects::ApplicationController respond_with(@issue) end + def edit + respond_with(@issue) + end + def show @noteable = @issue @note = @project.notes.new(noteable: @issue) @@ -122,6 +126,10 @@ class Projects::IssuesController < Projects::ApplicationController @issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue) respond_to do |format| + format.html do + recaptcha_check_with_fallback { render :edit } + end + format.json do render_issue_json end diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml new file mode 100644 index 00000000000..1b7d878c38c --- /dev/null +++ b/app/views/projects/issues/edit.html.haml @@ -0,0 +1,7 @@ +- page_title "Edit", "#{@issue.title} (#{@issue.to_reference})", "Issues" + +%h3.page-title + Edit Issue ##{@issue.iid} +%hr + += render "form" diff --git a/changelogs/unreleased/39441-bring-edit-form-back.yml b/changelogs/unreleased/39441-bring-edit-form-back.yml new file mode 100644 index 00000000000..025417e4da9 --- /dev/null +++ b/changelogs/unreleased/39441-bring-edit-form-back.yml @@ -0,0 +1,5 @@ +--- +title: Fix editing issue description in mobile view +merge_request: +author: +type: fixed diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 6f48f091a20..aecdfb50759 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -557,6 +557,29 @@ describe Projects::IssuesController do end end end + + describe 'GET #edit' do + it_behaves_like 'restricted action', success: 200 + + def go(id:) + get :edit, + namespace_id: project.namespace.to_param, + project_id: project, + id: id + end + end + + describe 'PUT #update' do + it_behaves_like 'restricted action', success: 302 + + def go(id:) + put :update, + namespace_id: project.namespace.to_param, + project_id: project, + id: id, + issue: { title: 'New title' } + end + end end describe 'POST #create' do diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index 8ce470fc288..2db6f9a2982 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -218,15 +218,54 @@ describe 'New/edit issue', :js do context 'edit issue' do before do - visit project_issue_path(project, issue) - page.within('.content .issuable-actions') do - click_on 'Edit' + visit edit_project_issue_path(project, issue) + end + + it 'allows user to update issue' do + expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user.id.to_s) + expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) + expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible + + page.within '.js-user-search' do + expect(page).to have_content user.name + end + + page.within '.js-milestone-select' do + expect(page).to have_content milestone.title + end + + click_button 'Labels' + page.within '.dropdown-menu-labels' do + click_link label.title + click_link label2.title + end + page.within '.js-label-select' do + expect(page).to have_content label.title + end + expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s) + expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s) + + click_button 'Save changes' + + page.within '.issuable-sidebar' do + page.within '.assignee' do + expect(page).to have_content user.name + end + + page.within '.milestone' do + expect(page).to have_content milestone.title + end + + page.within '.labels' do + expect(page).to have_content label.title + expect(page).to have_content label2.title + end end end it 'description has autocomplete' do - find_field('issue-description').native.send_keys('') - fill_in 'issue-description', with: '@' + find('#issue_description').native.send_keys('') + fill_in 'issue_description', with: '@' expect(page).to have_selector('.atwho-view') end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 25e99774575..d4fd3a50008 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Issues', :js do +describe 'Issues' do include DropzoneHelper include IssueHelpers include SortingHelper @@ -24,15 +24,109 @@ describe 'Issues', :js do end before do - visit project_issue_path(project, issue) - page.within('.content .issuable-actions') do - find('.issuable-edit').click - end - find('.issue-details .content-block .js-zen-enter').click + visit edit_project_issue_path(project, issue) + find('.js-zen-enter').click end it 'opens new issue popup' do - expect(page).to have_content(issue.description) + expect(page).to have_content("Issue ##{issue.iid}") + end + end + + describe 'Editing issue assignee' do + let!(:issue) do + create(:issue, + author: user, + assignees: [user], + project: project) + end + + it 'allows user to select unassigned', :js do + visit edit_project_issue_path(project, issue) + + expect(page).to have_content "Assignee #{user.name}" + + first('.js-user-search').click + click_link 'Unassigned' + + click_button 'Save changes' + + page.within('.assignee') do + expect(page).to have_content 'No assignee - assign yourself' + end + + expect(issue.reload.assignees).to be_empty + end + end + + describe 'due date', :js do + context 'on new form' do + before do + visit new_project_issue_path(project) + end + + it 'saves with due date' do + date = Date.today.at_beginning_of_month + + fill_in 'issue_title', with: 'bug 345' + fill_in 'issue_description', with: 'bug description' + find('#issuable-due-date').click + + page.within '.pika-single' do + click_button date.day + end + + expect(find('#issuable-due-date').value).to eq date.to_s + + click_button 'Submit issue' + + page.within '.issuable-sidebar' do + expect(page).to have_content date.to_s(:medium) + end + end + end + + context 'on edit form' do + let(:issue) { create(:issue, author: user, project: project, due_date: Date.today.at_beginning_of_month.to_s) } + + before do + visit edit_project_issue_path(project, issue) + end + + it 'saves with due date' do + date = Date.today.at_beginning_of_month + + expect(find('#issuable-due-date').value).to eq date.to_s + + date = date.tomorrow + + fill_in 'issue_title', with: 'bug 345' + fill_in 'issue_description', with: 'bug description' + find('#issuable-due-date').click + + page.within '.pika-single' do + click_button date.day + end + + expect(find('#issuable-due-date').value).to eq date.to_s + + click_button 'Save changes' + + page.within '.issuable-sidebar' do + expect(page).to have_content date.to_s(:medium) + end + end + + it 'warns about version conflict' do + issue.update(title: "New title") + + fill_in 'issue_title', with: 'bug 345' + fill_in 'issue_description', with: 'bug description' + + click_button 'Save changes' + + expect(page).to have_content 'Someone edited the issue the same time you did' + end end end diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index d70cf1527e7..a7928857b7d 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -181,6 +181,21 @@ describe "Internal Project Access" do it { is_expected.to be_denied_for(:visitor) } end + describe "GET /:project_path/issues/:id/edit" do + let(:issue) { create(:issue, project: project) } + subject { edit_project_issue_path(project, issue) } + + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } + end + describe "GET /:project_path/snippets" do subject { project_snippets_path(project) } diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index ea130606545..a4396b20afd 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -181,6 +181,21 @@ describe "Private Project Access" do it { is_expected.to be_denied_for(:visitor) } end + describe "GET /:project_path/issues/:id/edit" do + let(:issue) { create(:issue, project: project) } + subject { edit_project_issue_path(project, issue) } + + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } + end + describe "GET /:project_path/snippets" do subject { project_snippets_path(project) } diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index d15f5af66c9..fccdeb0e5b7 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -394,6 +394,21 @@ describe "Public Project Access" do it { is_expected.to be_allowed_for(:visitor) } end + describe "GET /:project_path/issues/:id/edit" do + let(:issue) { create(:issue, project: project) } + subject { edit_project_issue_path(project, issue) } + + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } + end + describe "GET /:project_path/snippets" do subject { project_snippets_path(project) } diff --git a/spec/support/update_invalid_issuable.rb b/spec/support/update_invalid_issuable.rb index 50a1d4a56e2..1490287681b 100644 --- a/spec/support/update_invalid_issuable.rb +++ b/spec/support/update_invalid_issuable.rb @@ -25,13 +25,11 @@ shared_examples 'update invalid issuable' do |klass| .and_raise(ActiveRecord::StaleObjectError.new(issuable, :save)) end - if klass == MergeRequest - it 'renders edit when format is html' do - put :update, params + it 'renders edit when format is html' do + put :update, params - expect(response).to render_template(:edit) - expect(assigns[:conflict]).to be_truthy - end + expect(response).to render_template(:edit) + expect(assigns[:conflict]).to be_truthy end it 'renders json error message when format is json' do @@ -44,17 +42,16 @@ shared_examples 'update invalid issuable' do |klass| end end - if klass == MergeRequest - context 'when updating an invalid issuable' do - before do - params[:merge_request][:title] = "" - 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 + it 'renders edit when merge request is invalid' do + put :update, params - expect(response).to render_template(:edit) - end + expect(response).to render_template(:edit) end end end -- cgit v1.2.1 From 3aafcc16fbdde08bf333eab97c5b1b3c4249a5cf Mon Sep 17 00:00:00 2001 From: Alessio Caiazza Date: Thu, 26 Oct 2017 16:38:10 +0200 Subject: Add KubernetesService#default_namespace tests --- app/models/project_services/kubernetes_service.rb | 11 ++------- .../project_services/kubernetes_service_spec.rb | 27 ++++++++++++++++++---- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 45a544e3674..5c0b3338a62 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -155,15 +155,8 @@ class KubernetesService < DeploymentService def default_namespace return unless project - # 1. lowercase - # 2. replace non kubernetes characters with dash - # 3. trim dash from the beginning and end - - slugified = "#{project.path}-#{project.id}" - slugified.downcase! - slugified.gsub!(/[^a-z0-9]/, '-') - slugified.gsub!(/^-+|-+$/, '') - slugified + slug = "#{project.path}-#{project.id}".downcase + slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '') end def build_kubeclient!(api_path: 'api', api_version: 'v1') diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 69da4375245..fa478a22cf4 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -99,8 +99,25 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do describe '#actual_namespace' do subject { service.actual_namespace } - it "returns the default namespace" do - is_expected.to eq(service.send(:default_namespace)) + shared_examples 'a correctly formatted namespace' do + it 'returns a valid Kubernetes namespace name' do + expect(subject).to match(Gitlab::Regex.kubernetes_namespace_regex) + expect(subject).to eq(expected_namespace) + end + end + + it_behaves_like 'a correctly formatted namespace' do + let(:expected_namespace) { service.send(:default_namespace) } + end + + context 'when the project path contains forbidden characters' do + before do + project.path = '-a_Strange.Path--forSure' + end + + it_behaves_like 'a correctly formatted namespace' do + let(:expected_namespace) { "a-strange-path--forsure-#{project.id}" } + end end context 'when namespace is specified' do @@ -108,8 +125,8 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do service.namespace = 'my-namespace' end - it "returns the user-namespace" do - is_expected.to eq('my-namespace') + it_behaves_like 'a correctly formatted namespace' do + let(:expected_namespace) { 'my-namespace' } end end @@ -118,7 +135,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do service.project = nil end - it "does not return namespace" do + it 'does not return namespace' do is_expected.to be_nil end end -- cgit v1.2.1 From 9a2b9d2345c504f888386012a1fefa29de837640 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 27 Oct 2017 16:11:02 +0800 Subject: Rename to shouldShowUsername --- .../vue_shared/components/user_avatar/user_avatar_link.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue index 792d8b29593..dc32e783258 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue @@ -69,11 +69,11 @@ export default { }, }, computed: { - showUsername() { + shouldShowUsername() { return this.username.length > 0; }, avatarTooltipText() { - return this.showUsername ? '' : this.tooltipText; + return this.shouldShowUsername ? '' : this.tooltipText; }, }, directives: { @@ -94,7 +94,7 @@ export default { :tooltip-text="avatarTooltipText" :tooltip-placement="tooltipPlacement" /> Date: Fri, 27 Oct 2017 16:20:53 +0800 Subject: Improve spec to check hidden component --- spec/javascripts/vue_shared/components/user_avatar_link_spec.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spec/javascripts/vue_shared/components/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar_link_spec.js index ce75df6fc7b..c10528360a3 100644 --- a/spec/javascripts/vue_shared/components/user_avatar_link_spec.js +++ b/spec/javascripts/vue_shared/components/user_avatar_link_spec.js @@ -56,8 +56,12 @@ describe('User Avatar Link Component', function () { Vue.nextTick(done); }); - it('should not render as a child element', function () { - expect(this.userAvatarLink.$el.querySelector('span')).toBeNull(); + it('should only render image tag in link', function () { + const childElements = this.userAvatarLink.$el.childNodes; + expect(childElements[0].tagName).toBe('IMG'); + + // Vue will render the hidden component as + expect(childElements[1].tagName).toBeUndefined(); }); it('should render avatar image tooltip', function () { -- cgit v1.2.1 From 4e8010aa9d8bdb05c3999691731daa2e8e99c9d8 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 27 Oct 2017 16:21:56 +0800 Subject: Remove repetitive karma spec --- spec/javascripts/vue_shared/components/user_avatar_link_spec.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/spec/javascripts/vue_shared/components/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar_link_spec.js index c10528360a3..8450ad9dbcb 100644 --- a/spec/javascripts/vue_shared/components/user_avatar_link_spec.js +++ b/spec/javascripts/vue_shared/components/user_avatar_link_spec.js @@ -74,10 +74,6 @@ describe('User Avatar Link Component', function () { expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual(''); }); - it('should render as a child element', function () { - expect(this.userAvatarLink.$el.querySelector('span')).toBeDefined(); - }); - it('should render username prop in ', function () { expect(this.userAvatarLink.$el.querySelector('span').innerText.trim()).toEqual(this.propsData.username); }); -- cgit v1.2.1 From be73ede362ca4f9700202c42f9eba74d58199042 Mon Sep 17 00:00:00 2001 From: Nick Brown Date: Fri, 27 Oct 2017 08:50:51 +0000 Subject: Semi-linear history merge is now available in CE. Already mentioned in the list above this one. --- doc/user/project/merge_requests/index.md | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 6289fcf3c2b..4b2e042251b 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -34,7 +34,6 @@ With **[GitLab Enterprise Edition][ee]**, you can also: - View the deployment process across projects with [Multi-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html#multi-project-pipeline-graphs) (available only in GitLab Enterprise Edition Premium) - Request [approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers (available in GitLab Enterprise Edition Starter) - [Squash and merge](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html) for a cleaner commit history (available in GitLab Enterprise Edition Starter) -- Enable [semi-linear history merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/index.html#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch (available in GitLab Enterprise Edition Starter) - Analise the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) (available in GitLab Enterprise Edition Starter) ## Use cases -- cgit v1.2.1 From b7835587e507ae9bc8e49c738370f6ef5b9f64fe Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 19 Oct 2017 13:03:41 -0500 Subject: Change default disabled merge request widget message to "Merge is not allowed yet" Fix https://gitlab.com/gitlab-org/gitlab-ce/issues/39188 --- .../components/states/mr_widget_ready_to_merge.js | 5 ++- ...39188-change-default-disabled-merge-message.yml | 5 +++ .../states/mr_widget_ready_to_merge_spec.js | 46 ++++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/39188-change-default-disabled-merge-message.yml diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js index b8a96b23012..be37dd87de9 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js @@ -286,6 +286,7 @@ export default { Remove source branch @@ -311,8 +312,8 @@ export default { diff --git a/changelogs/unreleased/39188-change-default-disabled-merge-message.yml b/changelogs/unreleased/39188-change-default-disabled-merge-message.yml new file mode 100644 index 00000000000..7de65f5c3f6 --- /dev/null +++ b/changelogs/unreleased/39188-change-default-disabled-merge-message.yml @@ -0,0 +1,5 @@ +--- +title: Update default disabled merge request widget message to reflect a general failure +merge_request: 14960 +author: +type: changed diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index d7019ea408b..df3d29ee1f9 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -43,6 +43,10 @@ describe('MRWidgetReadyToMerge', () => { vm = createComponent(); }); + afterEach(() => { + vm.$destroy(); + }); + describe('props', () => { it('should have props', () => { const { mr, service } = readyToMergeComponent.props; @@ -495,6 +499,48 @@ describe('MRWidgetReadyToMerge', () => { }); }); + describe('Merge controls', () => { + describe('when allowed to merge', () => { + beforeEach(() => { + vm = createComponent({ + mr: { isMergeAllowed: true }, + }); + }); + + it('shows remove source branch checkbox', () => { + expect(vm.$el.querySelector('.js-remove-source-branch-checkbox')).toBeDefined(); + }); + + it('shows modify commit message button', () => { + expect(vm.$el.querySelector('.js-modify-commit-message-button')).toBeDefined(); + }); + + it('does not show message about needing to resolve items', () => { + expect(vm.$el.querySelector('.js-resolve-mr-widget-items-message')).toBeNull(); + }); + }); + + describe('when not allowed to merge', () => { + beforeEach(() => { + vm = createComponent({ + mr: { isMergeAllowed: false }, + }); + }); + + it('does not show remove source branch checkbox', () => { + expect(vm.$el.querySelector('.js-remove-source-branch-checkbox')).toBeNull(); + }); + + it('does not show modify commit message button', () => { + expect(vm.$el.querySelector('.js-modify-commit-message-button')).toBeNull(); + }); + + it('shows message to resolve all items before being allowed to merge', () => { + expect(vm.$el.querySelector('.js-resolve-mr-widget-items-message')).toBeDefined(); + }); + }); + }); + describe('Commit message area', () => { it('when using merge commits, should show "Modify commit message" button', () => { const customVm = createComponent({ -- cgit v1.2.1 From fed51d1eaacc0d9e327cb2b67b19848d6cafd666 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 27 Oct 2017 09:05:03 +0000 Subject: Remove groups_select from global namespace & simplifies the code --- app/assets/javascripts/dispatcher.js | 4 +- app/assets/javascripts/groups_select.js | 185 +++++++++------------ app/assets/javascripts/main.js | 1 - .../projects/members/share_with_group_spec.rb | 2 +- 4 files changed, 78 insertions(+), 114 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index f20162c48e9..970e83c0ecb 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -13,7 +13,7 @@ import GroupLabelSubscription from './group_label_subscription'; /* global LineHighlighter */ import BuildArtifacts from './build_artifacts'; import CILintEditor from './ci_lint_editor'; -/* global GroupsSelect */ +import groupsSelect from './groups_select'; /* global Search */ /* global Admin */ /* global NamespaceSelects */ @@ -414,7 +414,7 @@ import Diff from './diff'; break; case 'projects:project_members:index': memberExpirationDate('.js-access-expiration-date-groups'); - new GroupsSelect(); + groupsSelect(); memberExpirationDate(); new Members(); new UsersSelect(); diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index 90ca70289ab..a69a0bde17b 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -1,121 +1,86 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, - camelcase, one-var-declaration-per-line, quotes, object-shorthand, - prefer-arrow-callback, comma-dangle, consistent-return, yoda, - prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, - promise/catch-or-return */ import Api from './api'; import { normalizeCRLFHeaders } from './lib/utils/common_utils'; -var slice = [].slice; +export default function groupsSelect() { + // Needs to be accessible in rspec + window.GROUP_SELECT_PER_PAGE = 20; + $('.ajax-groups-select').each(function setAjaxGroupsSelect2() { + const $select = $(this); + const allAvailable = $select.data('all-available'); + const skipGroups = $select.data('skip-groups') || []; + $select.select2({ + placeholder: 'Search for a group', + multiple: $select.hasClass('multiselect'), + minimumInputLength: 0, + ajax: { + url: Api.buildUrl(Api.groupsPath), + dataType: 'json', + quietMillis: 250, + transport(params) { + return $.ajax(params) + .then((data, status, xhr) => { + const results = data || []; -window.GroupsSelect = (function() { - function GroupsSelect() { - $('.ajax-groups-select').each((function(_this) { - const self = _this; - - return function(i, select) { - var all_available, skip_groups; - const $select = $(select); - all_available = $select.data('all-available'); - skip_groups = $select.data('skip-groups') || []; - - $select.select2({ - placeholder: "Search for a group", - multiple: $select.hasClass('multiselect'), - minimumInputLength: 0, - ajax: { - url: Api.buildUrl(Api.groupsPath), - dataType: 'json', - quietMillis: 250, - transport: function (params) { - $.ajax(params).then((data, status, xhr) => { - const results = data || []; - - const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders()); - const currentPage = parseInt(headers['X-PAGE'], 10) || 0; - const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0; - const more = currentPage < totalPages; - - return { - results, - pagination: { - more, - }, - }; - }).then(params.success).fail(params.error); - }, - data: function (search, page) { - return { - search, - page, - per_page: GroupsSelect.PER_PAGE, - all_available, - }; - }, - results: function (data, page) { - if (data.length) return { results: [] }; - - const groups = data.length ? data : data.results || []; - const more = data.pagination ? data.pagination.more : false; - const results = groups.filter(group => skip_groups.indexOf(group.id) === -1); + const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders()); + const currentPage = parseInt(headers['X-PAGE'], 10) || 0; + const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0; + const more = currentPage < totalPages; return { results, - page, - more, + pagination: { + more, + }, }; - }, - }, - initSelection: function(element, callback) { - var id; - id = $(element).val(); - if (id !== "") { - return Api.group(id, callback); - } - }, - formatResult: function() { - var args; - args = 1 <= arguments.length ? slice.call(arguments, 0) : []; - return self.formatResult.apply(self, args); - }, - formatSelection: function() { - var args; - args = 1 <= arguments.length ? slice.call(arguments, 0) : []; - return self.formatSelection.apply(self, args); - }, - dropdownCssClass: "ajax-groups-dropdown select2-infinite", - // we do not want to escape markup since we are displaying html in results - escapeMarkup: function(m) { - return m; - } - }); - - self.dropdown = document.querySelector('.select2-infinite .select2-results'); - - $select.on('select2-loaded', self.forceOverflow.bind(self)); - }; - })(this)); - } - - GroupsSelect.prototype.formatResult = function(group) { - var avatar; - if (group.avatar_url) { - avatar = group.avatar_url; - } else { - avatar = gon.default_avatar_url; - } - return "
" + group.full_name + "
" + group.full_path + "
"; - }; - - GroupsSelect.prototype.formatSelection = function(group) { - return group.full_name; - }; + }) + .then(params.success) + .fail(params.error); + }, + data(search, page) { + return { + search, + page, + per_page: window.GROUP_SELECT_PER_PAGE, + all_available: allAvailable, + }; + }, + results(data, page) { + if (data.length) return { results: [] }; - GroupsSelect.prototype.forceOverflow = function (e) { - this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight)}px`; - }; + const groups = data.length ? data : data.results || []; + const more = data.pagination ? data.pagination.more : false; + const results = groups.filter(group => skipGroups.indexOf(group.id) === -1); - GroupsSelect.PER_PAGE = 20; + return { + results, + page, + more, + }; + }, + }, + // eslint-disable-next-line consistent-return + initSelection(element, callback) { + const id = $(element).val(); + if (id !== '') { + return Api.group(id, callback); + } + }, + formatResult(object) { + return `
${object.full_name}
${object.full_path}
`; + }, + formatSelection(object) { + return object.full_name; + }, + dropdownCssClass: 'ajax-groups-dropdown select2-infinite', + // we do not want to escape markup since we are displaying html in results + escapeMarkup(m) { + return m; + }, + }); - return GroupsSelect; -})(); + $select.on('select2-loaded', () => { + const dropdown = document.querySelector('.select2-infinite .select2-results'); + dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`; + }); + }); +} diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 52715fba43f..38403fdaf6e 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -55,7 +55,6 @@ import './gl_dropdown'; import './gl_field_error'; import './gl_field_errors'; import './gl_form'; -import './groups_select'; import './header'; import './importer_status'; import './issuable_index'; diff --git a/spec/features/projects/members/share_with_group_spec.rb b/spec/features/projects/members/share_with_group_spec.rb index 3b368f8e25d..63b5df5a8f5 100644 --- a/spec/features/projects/members/share_with_group_spec.rb +++ b/spec/features/projects/members/share_with_group_spec.rb @@ -149,7 +149,7 @@ feature 'Project > Members > Share with Group', :js do create(:group).add_owner(master) visit project_settings_members_path(project) - execute_script 'GroupsSelect.PER_PAGE = 1;' + execute_script 'GROUP_SELECT_PER_PAGE = 1;' open_select2 '#link_group_id' end -- cgit v1.2.1 From 3411fef1df22295cc68b1d39576917dd533da580 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Fri, 13 Oct 2017 10:37:31 +0200 Subject: Cache commits on the repository model Now, when requesting a commit from the Repository model, the results are not cached. This means we're fetching the same commit by oid multiple times during the same request. To prevent us from doing this, we now cache results. Caching is done only based on object id (aka SHA). Given we cache on the Repository model, results are scoped to the associated project, eventhough the change of two repositories having the same oids for different commits is small. --- app/models/ci/pipeline.rb | 4 +-- app/models/project.rb | 6 +++- app/models/repository.rb | 38 ++++++++++++++-------- changelogs/unreleased/zj-commit-cache.yml | 5 +++ lib/gitlab/git/commit.rb | 3 +- .../projects/pipelines_controller_spec.rb | 2 +- spec/models/merge_request_spec.rb | 4 +-- spec/models/repository_spec.rb | 20 ++++++++++++ 8 files changed, 61 insertions(+), 21 deletions(-) create mode 100644 changelogs/unreleased/zj-commit-cache.yml diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index cf3ce3c9e54..ca65e81f27a 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -249,9 +249,7 @@ module Ci end def commit - @commit ||= project.commit(sha) - rescue - nil + @commit ||= project.commit_by(oid: sha) end def branch? diff --git a/app/models/project.rb b/app/models/project.rb index 4689b588906..7185b4d44fc 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -540,6 +540,10 @@ class Project < ActiveRecord::Base repository.commit(ref) end + def commit_by(oid:) + repository.commit_by(oid: oid) + end + # ref can't be HEAD, can only be branch/tag name or SHA def latest_successful_builds_for(ref = default_branch) latest_pipeline = pipelines.latest_successful_for(ref) @@ -553,7 +557,7 @@ class Project < ActiveRecord::Base def merge_base_commit(first_commit_id, second_commit_id) sha = repository.merge_base(first_commit_id, second_commit_id) - repository.commit(sha) if sha + commit_by(oid: sha) if sha end def saved? diff --git a/app/models/repository.rb b/app/models/repository.rb index 4324ea46aac..1d1987ec322 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -76,6 +76,7 @@ class Repository @full_path = full_path @disk_path = disk_path || full_path @project = project + @commit_cache = {} end def ==(other) @@ -103,18 +104,17 @@ class Repository def commit(ref = 'HEAD') return nil unless exists? + return ref if ref.is_a?(::Commit) - commit = - if ref.is_a?(Gitlab::Git::Commit) - ref - else - Gitlab::Git::Commit.find(raw_repository, ref) - end + find_commit(ref) + end - commit = ::Commit.new(commit, @project) if commit - commit - rescue Rugged::OdbError, Rugged::TreeError - nil + # Finding a commit by the passed SHA + # Also takes care of caching, based on the SHA + def commit_by(oid:) + return @commit_cache[oid] if @commit_cache.key?(oid) + + @commit_cache[oid] = find_commit(oid) end def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil) @@ -231,7 +231,7 @@ class Repository # branches or tags, but we want to keep some of these commits around, for # example if they have comments or CI builds. def keep_around(sha) - return unless sha && commit(sha) + return unless sha && commit_by(oid: sha) return if kept_around?(sha) @@ -1069,6 +1069,18 @@ class Repository private + # TODO Generice finder, later split this on finders by Ref or Oid + # gitlab-org/gitlab-ce#39239 + def find_commit(oid_or_ref) + commit = if oid_or_ref.is_a?(Gitlab::Git::Commit) + oid_or_ref + else + Gitlab::Git::Commit.find(raw_repository, oid_or_ref) + end + + ::Commit.new(commit, @project) if commit + end + def blob_data_at(sha, path) blob = blob_at(sha, path) return unless blob @@ -1107,12 +1119,12 @@ class Repository def last_commit_for_path_by_gitaly(sha, path) c = raw_repository.gitaly_commit_client.last_commit_for_path(sha, path) - commit(c) + commit_by(oid: c) end def last_commit_for_path_by_rugged(sha, path) sha = last_commit_id_for_path_by_shelling_out(sha, path) - commit(sha) + commit_by(oid: sha) end def last_commit_id_for_path_by_shelling_out(sha, path) diff --git a/changelogs/unreleased/zj-commit-cache.yml b/changelogs/unreleased/zj-commit-cache.yml new file mode 100644 index 00000000000..e3afe0ea7ef --- /dev/null +++ b/changelogs/unreleased/zj-commit-cache.yml @@ -0,0 +1,5 @@ +--- +title: Cache commits fetched from the repository +merge_request: +author: +type: performance diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index 1957c254c28..23ae37ff71e 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -72,7 +72,8 @@ module Gitlab decorate(repo, commit) if commit rescue Rugged::ReferenceError, Rugged::InvalidError, Rugged::ObjectError, - Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository + Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository, + Rugged::OdbError, Rugged::TreeError nil end diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index f1ad5dd84ff..1604a2da485 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -46,7 +46,7 @@ describe Projects::PipelinesController do context 'when performing gitaly calls', :request_store do it 'limits the Gitaly requests' do - expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(10) + expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(8) end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 73e038b61ed..2b8aa7cf9c3 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -86,7 +86,7 @@ describe MergeRequest do context 'when the target branch does not exist' do before do - project.repository.raw_repository.delete_branch(subject.target_branch) + project.repository.rm_branch(subject.author, subject.target_branch) end it 'returns nil' do @@ -1388,7 +1388,7 @@ describe MergeRequest do context 'when the target branch does not exist' do before do - subject.project.repository.raw_repository.delete_branch(subject.target_branch) + subject.project.repository.rm_branch(subject.author, subject.target_branch) end it 'returns nil' do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 39d188f18af..aa376d46a67 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -2238,4 +2238,24 @@ describe Repository do end end end + + describe 'commit cache' do + set(:project) { create(:project, :repository) } + + it 'caches based on SHA' do + # Gets the commit oid, and warms the cache + oid = project.commit.id + + expect(Gitlab::Git::Commit).not_to receive(:find).once + + project.commit_by(oid: oid) + end + + it 'caches nil values' do + expect(Gitlab::Git::Commit).to receive(:find).once + + project.commit_by(oid: '1' * 40) + project.commit_by(oid: '1' * 40) + end + end end -- cgit v1.2.1 From 43f0bca64825ac6f2863c810f19bf78f919c5e61 Mon Sep 17 00:00:00 2001 From: Ahmad Sherif Date: Thu, 26 Oct 2017 18:40:32 +0200 Subject: Avoid using Rugged in Gitlab::Git::Wiki#preview_slug --- lib/gitlab/git/wiki.rb | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb index e7b2f52a552..f01d5c96fc8 100644 --- a/lib/gitlab/git/wiki.rb +++ b/lib/gitlab/git/wiki.rb @@ -8,6 +8,7 @@ module Gitlab { name: name, email: email, message: message } end end + PageBlob = Struct.new(:name) def self.default_ref 'master' @@ -80,7 +81,15 @@ module Gitlab end def preview_slug(title, format) - gollum_wiki.preview_page(title, '', format).url_path + # Adapted from gollum gem (Gollum::Wiki#preview_page) to avoid + # using Rugged through a Gollum::Wiki instance + page_class = Gollum::Page + page = page_class.new(nil) + ext = page_class.format_to_ext(format.to_sym) + name = page_class.cname(title) + '.' + ext + blob = PageBlob.new(name) + page.populate(blob) + page.url_path end private -- cgit v1.2.1 From 48851ff308032105234b781c61bfae8f312aced7 Mon Sep 17 00:00:00 2001 From: Brett Walker Date: Fri, 27 Oct 2017 15:20:46 +0200 Subject: add changelog entry --- changelogs/unreleased/39366-email-confirmation-fails.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/39366-email-confirmation-fails.yml diff --git a/changelogs/unreleased/39366-email-confirmation-fails.yml b/changelogs/unreleased/39366-email-confirmation-fails.yml new file mode 100644 index 00000000000..7f6a753f8c8 --- /dev/null +++ b/changelogs/unreleased/39366-email-confirmation-fails.yml @@ -0,0 +1,5 @@ +--- +title: 'Fix #39366 : E-mail confirmation fails: NoMethodError: undefined method "username"' +merge_request: 15010 +author: +type: fixed -- cgit v1.2.1 From 8acb6b7680f8edb78cda20b03c1728c4ab211155 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 27 Oct 2017 22:29:36 +0800 Subject: Merging EE doc into CE --- doc/development/ee_features.md | 382 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 382 insertions(+) create mode 100644 doc/development/ee_features.md diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md new file mode 100644 index 00000000000..932a44f65e4 --- /dev/null +++ b/doc/development/ee_features.md @@ -0,0 +1,382 @@ +# Guidelines for implementing Enterprise Edition feature + +- **Write the code and the tests.**: As with any code, EE features should have + good test coverage to prevent regressions. +- **Write documentation.**: Add documentation to the `doc/` directory. Describe + the feature and include screenshots, if applicable. +- **Submit a MR to the `www-gitlab-com` project.**: Add the new feature to the + [EE features list][ee-features-list]. + +## Act as CE when unlicensed + +Since the implementation of [GitLab CE features to work with unlicensed EE instance][ee-as-ce] +GitLab Enterprise Edition should work like GitLab Community Edition +when no license is active. So EE features always should be guarded by +`project.feature_available?` or `group.feature_available?` (or +`License.feature_available?` if it is a system-wide feature). + +CE specs should remain untouched as much as possible and extra specs +should be added for EE. Licensed features can be stubbed using the +spec helper `stub_licensed_features` in `EE::LicenseHelpers`. + +[ee-as-ce]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2500 + +## Separation of EE code + +We want a [single code base][] eventually, but before we reach the goal, +we still need to merge changes from GitLab CE to EE. To help us get there, +we should make sure that we no longer edit CE files in place in order to +implement EE features. + +Instead, all EE codes should be put inside the `ee/` top-level directory, and +tests should be put inside `spec/ee/`. We don't use `ee/spec` for now due to +technical limitation. The rest of codes should be as close as to the CE files. + +[single code base]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2952#note_41016454 + +### EE-only features + +If the feature being developed is not present in any form in CE, we don't +need to put the codes under `EE` namespace. For example, an EE model could +go into: `ee/app/models/awesome.rb` using `Awesome` as the class name. This +is applied not only to models. Here's a list of other examples: + +- `ee/app/controllers/foos_controller.rb` +- `ee/app/finders/foos_finder.rb` +- `ee/app/helpers/foos_helper.rb` +- `ee/app/mailers/foos_mailer.rb` +- `ee/app/models/foo.rb` +- `ee/app/policies/foo_policy.rb` +- `ee/app/serializers/foo_entity.rb` +- `ee/app/serializers/foo_serializer.rb` +- `ee/app/services/foo/create_service.rb` +- `ee/app/validators/foo_attr_validator.rb` +- `ee/app/workers/foo_worker.rb` + +### EE features based on CE features + +For features that build on existing CE features, write a module in the +`EE` namespace and `prepend` it in the CE class. This makes conflicts +less likely to happen during CE to EE merges because only one line is +added to the CE class - the `prepend` line. + +Since the module would require an `EE` namespace, the file should also be +put in an `ee/` sub-directory. For example, we want to extend the user model +in EE, so we have a module called `::EE::User` put inside +`ee/app/models/ee/user.rb`. + +This is also not just applied to models. Here's a list of other examples: + +- `ee/app/controllers/ee/foos_controller.rb` +- `ee/app/finders/ee/foos_finder.rb` +- `ee/app/helpers/ee/foos_helper.rb` +- `ee/app/mailers/ee/foos_mailer.rb` +- `ee/app/models/ee/foo.rb` +- `ee/app/policies/ee/foo_policy.rb` +- `ee/app/serializers/ee/foo_entity.rb` +- `ee/app/serializers/ee/foo_serializer.rb` +- `ee/app/services/ee/foo/create_service.rb` +- `ee/app/validators/ee/foo_attr_validator.rb` +- `ee/app/workers/ee/foo_worker.rb` + +#### Overriding CE methods + +To override a method present in the CE codebase, use `prepend`. It +lets you override a method in a class with a method from a module, while +still having access the class's implementation with `super`. + +There are a few gotchas with it: + +- you should always add a `raise NotImplementedError unless defined?(super)` + guard clause in the "overrider" method to ensure that if the method gets + renamed in CE, the EE override won't be silently forgotten. +- when the "overrider" would add a line in the middle of the CE + implementation, you should refactor the CE method and split it in + smaller methods. Or create a "hook" method that is empty in CE, + and with the EE-specific implementation in EE. +- when the original implementation contains a guard clause (e.g. + `return unless condition`), we cannot easily extend the behaviour by + overriding the method, because we can't know when the overridden method + (i.e. calling `super` in the overriding method) would want to stop early. + In this case, we shouldn't just override it, but update the original method + to make it call the other method we want to extend, like a [template method + pattern](https://en.wikipedia.org/wiki/Template_method_pattern). + For example, given this base: + ``` ruby + class Base + def execute + return unless enabled? + + # ... + # ... + end + end + ``` + Instead of just overriding `Base#execute`, we should update it and extract + the behaviour into another method: + ``` ruby + class Base + def execute + return unless enabled? + + do_something + end + + private + + def do_something + # ... + # ... + end + end + ``` + Then we're free to override that `do_something` without worrying about the + guards: + ``` ruby + module EE::Base + def do_something + # Follow the above pattern to call super and extend it + end + end + ``` + This would require updating CE first, or make sure this is back ported to CE. + +When prepending, place them in the `ee/` specific sub-directory, and +wrap class or module in `module EE` to avoid naming conflicts. + +For example to override the CE implementation of +`ApplicationController#after_sign_out_path_for`: + +```ruby +def after_sign_out_path_for(resource) + current_application_settings.after_sign_out_path.presence || new_user_session_path +end +``` + +Instead of modifying the method in place, you should add `prepend` to +the existing file: + +```ruby +class ApplicationController < ActionController::Base + prepend EE::ApplicationController + # ... + + def after_sign_out_path_for(resource) + current_application_settings.after_sign_out_path.presence || new_user_session_path + end + + # ... +end +``` + +And create a new file in the `ee/` sub-directory with the altered +implementation: + +```ruby +module EE + class ApplicationController + def after_sign_out_path_for(resource) + raise NotImplementedError unless defined?(super) + + if Gitlab::Geo.secondary? + Gitlab::Geo.primary_node.oauth_logout_url(@geo_logout_state) + else + super + end + end + end +end +``` + +#### Use self-descriptive wrapper methods + +When it's not possible/logical to modify the implementation of a +method. Wrap it in a self-descriptive method and use that method. + +For example, in CE only an `admin` is allowed to access all private +projects/groups, but in EE also an `auditor` has full private +access. It would be incorrect to override the implementation of +`User#admin?`, so instead add a method `full_private_access?` to +`app/models/users.rb`. The implementation in CE will be: + +```ruby +def full_private_access? + admin? +end +``` + +In EE, the implementation `ee/app/models/ee/users.rb` would be: + +```ruby +def full_private_access? + raise NotImplementedError unless defined?(super) + super || auditor? +end +``` + +In `lib/gitlab/visibility_level.rb` this method is used to return the +allowed visibilty levels: + +```ruby +def levels_for_user(user = nil) + if user.full_private_access? + [PRIVATE, INTERNAL, PUBLIC] + elsif # ... +end +``` + +See [CE MR][ce-mr-full-private] and [EE MR][ee-mr-full-private] for +full implementation details. + +[ce-mr-full-private]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12373 +[ee-mr-full-private]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2199 + +### Code in `app/controllers/` + +In controllers, the most common type of conflict is with `before_action` that +has a list of actions in CE but EE adds some actions to that list. + +The same problem often occurs for `params.require` / `params.permit` calls. + +**Mitigations** + +Separate CE and EE actions/keywords. For instance for `params.require` in +`ProjectsController`: + +```ruby +def project_params + params.require(:project).permit(project_params_attributes) +end + +# Always returns an array of symbols, created however best fits the use case. +# It _should_ be sorted alphabetically. +def project_params_attributes + %i[ + description + name + path + ] +end + +``` + +In the `EE::ProjectsController` module: + +```ruby +def project_params_attributes + super + project_params_attributes_ee +end + +def project_params_attributes_ee + %i[ + approvals_before_merge + approver_group_ids + approver_ids + ... + ] +end +``` + +### Code in `app/models/` + +EE-specific models should `extend EE::Model`. + +For example, if EE has a specific `Tanuki` model, you would +place it in `ee/app/models/ee/tanuki.rb`. + +### Code in `app/views/` + +It's a very frequent problem that EE is adding some specific view code in a CE +view. For instance the approval code in the project's settings page. + +**Mitigations** + +Blocks of code that are EE-specific should be moved to partials. This +avoids conflicts with big chunks of HAML code that that are not fun to +resolve when you add the indentation to the equation. + +EE-specific views should be placed in `ee/app/views/ee/`, using extra +sub-directories if appropriate. + +### Code in `lib/` + +Place EE-specific logic in the top-level `EE` module namespace. Namespace the +class beneath the `EE` module just as you would normally. + +For example, if CE has LDAP classes in `lib/gitlab/ldap/` then you would place +EE-specific LDAP classes in `ee/lib/ee/gitlab/ldap`. + +### Code in `spec/` + +When you're testing EE-only features, avoid adding examples to the +existing CE specs. Also do no change existing CE examples, since they +should remain working as-is when EE is running without a license. + +Instead place EE specs in the `spec/ee/spec` folder. + +## JavaScript code in `assets/javascripts/` + +To separate EE-specific JS-files we can also move the files into an `ee` folder. + +For example there can be an +`app/assets/javascripts/protected_branches/protected_branches_bundle.js` and an +EE counterpart +`ee/app/assets/javascripts/protected_branches/protected_branches_bundle.js`. + +That way we can create a separate webpack bundle in `webpack.config.js`: + +```javascript + protected_branches: '~/protected_branches', + ee_protected_branches: 'ee/protected_branches/protected_branches_bundle.js', +``` + +With the separate bundle in place, we can decide which bundle to load inside the +view, using the `page_specific_javascript_bundle_tag` helper. + +```haml +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('protected_branches') +``` + +## SCSS code in `assets/stylesheets` + +To separate EE-specific styles in SCSS files, if a component you're adding styles for +is limited to only EE, it is better to have a separate SCSS file in appropriate directory +within `app/assets/stylesheets`. + +In some cases, this is not entirely possible or creating dedicated SCSS file is an overkill, +e.g. a text style of some component is different for EE. In such cases, +styles are usually kept in stylesheet that is common for both CE and EE, and it is wise +to isolate such ruleset from rest of CE rules (along with adding comment describing the same) +to avoid conflicts during CE to EE merge. + +#### Bad +```scss +.section-body { + .section-title { + background: $gl-header-color; + } + + &.ee-section-body { + .section-title { + background: $gl-header-color-cyan; + } + } +} +``` + +#### Good +```scss +.section-body { + .section-title { + background: $gl-header-color; + } +} + +/* EE-specific styles */ +.section-body.ee-section-body { + .section-title { + background: $gl-header-color-cyan; + } +} +``` -- cgit v1.2.1 From a2894b7ad2b26ff65d36b9c87b79c60ff4ddda59 Mon Sep 17 00:00:00 2001 From: Brett Walker Date: Fri, 27 Oct 2017 16:32:48 +0200 Subject: use a delegate for `username` to be more future friendly --- app/controllers/confirmations_controller.rb | 3 +-- app/models/email.rb | 2 ++ spec/models/email_spec.rb | 8 ++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index 8ca01a6e2c6..bc0948cd3fb 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -15,8 +15,7 @@ class ConfirmationsController < Devise::ConfirmationsController if signed_in?(:user) after_sign_in(resource) else - username = (resource_name == :email ? resource.user.username : resource.username) - Gitlab::AppLogger.info("Email Confirmed: username=#{username} email=#{resource.email} ip=#{request.remote_ip}") + Gitlab::AppLogger.info("Email Confirmed: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip}") flash[:notice] += " Please sign in." new_session_path(:user) end diff --git a/app/models/email.rb b/app/models/email.rb index 384f38f2db7..2da8b050149 100644 --- a/app/models/email.rb +++ b/app/models/email.rb @@ -14,6 +14,8 @@ class Email < ActiveRecord::Base devise :confirmable self.reconfirmable = false # currently email can't be changed, no need to reconfirm + delegate :username, to: :user + def email=(value) write_attribute(:email, value.downcase.strip) end diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb index b32dd31ae6d..4676612ad52 100644 --- a/spec/models/email_spec.rb +++ b/spec/models/email_spec.rb @@ -40,4 +40,12 @@ describe Email do expect(user.emails.confirmed.count).to eq 1 end end + + describe 'delegation' do + let(:user) { create(:user) } + + it 'delegates to :user' do + expect(build(:email, user: user).username).to eq user.username + end + end end -- cgit v1.2.1 From 34254e1d58991dadb461fea2b62d3d93b17fbb73 Mon Sep 17 00:00:00 2001 From: Brett Walker Date: Fri, 27 Oct 2017 17:35:40 +0200 Subject: remove extra whitespace --- spec/models/email_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb index 4676612ad52..47eb0717c0c 100644 --- a/spec/models/email_spec.rb +++ b/spec/models/email_spec.rb @@ -40,10 +40,10 @@ describe Email do expect(user.emails.confirmed.count).to eq 1 end end - + describe 'delegation' do let(:user) { create(:user) } - + it 'delegates to :user' do expect(build(:email, user: user).username).to eq user.username end -- cgit v1.2.1 From 57d7ed05d96928f7e33135e7397bdd6b3b0d25e0 Mon Sep 17 00:00:00 2001 From: "Lin Jen-Shin (godfat)" Date: Fri, 27 Oct 2017 15:55:08 +0000 Subject: Fetch the merged branches at once --- app/controllers/projects/branches_controller.rb | 2 ++ app/models/repository.rb | 21 +++++++++++----- app/views/projects/branches/_branch.html.haml | 5 ++-- app/views/projects/branches/index.html.haml | 2 +- changelogs/unreleased/use-git-branch-merged.yml | 5 ++++ lib/gitlab/git/branch.rb | 8 +++++++ lib/gitlab/git/repository.rb | 11 +++++++++ spec/lib/gitlab/git/branch_spec.rb | 32 +++++++++++++++++++++++++ spec/lib/gitlab/git/repository_spec.rb | 26 ++++++++++++++++++++ spec/models/repository_spec.rb | 18 ++++++++++++++ 10 files changed, 121 insertions(+), 9 deletions(-) create mode 100644 changelogs/unreleased/use-git-branch-merged.yml diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 7f03ce07dec..f28df83d5a5 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -15,6 +15,8 @@ class Projects::BranchesController < Projects::ApplicationController respond_to do |format| format.html do @refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name)) + @merged_branch_names = + repository.merged_branch_names(@branches.map(&:name)) # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37429 Gitlab::GitalyClient.allow_n_plus_1_calls do @max_commits = @branches.reduce(0) do |memo, branch| diff --git a/app/models/repository.rb b/app/models/repository.rb index 54388d1c8b4..44a1e9ce529 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -902,18 +902,27 @@ class Repository end end - def merged_to_root_ref?(branch_name) - branch_commit = commit(branch_name) - root_ref_commit = commit(root_ref) + def merged_to_root_ref?(branch_or_name, pre_loaded_merged_branches = nil) + branch = Gitlab::Git::Branch.find(self, branch_or_name) + + if branch + root_ref_sha = commit(root_ref).sha + same_head = branch.target == root_ref_sha + merged = + if pre_loaded_merged_branches + pre_loaded_merged_branches.include?(branch.name) + else + ancestor?(branch.target, root_ref_sha) + end - if branch_commit - same_head = branch_commit.id == root_ref_commit.id - !same_head && ancestor?(branch_commit.id, root_ref_commit.id) + !same_head && merged else nil end end + delegate :merged_branch_names, to: :raw_repository + def merge_base(first_commit_id, second_commit_id) first_commit_id = commit(first_commit_id).try(:id) || first_commit_id second_commit_id = commit(second_commit_id).try(:id) || second_commit_id diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 49101d1efa4..6e02ae6c9cc 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -1,3 +1,4 @@ +- merged = local_assigns.fetch(:merged, false) - commit = @repository.commit(branch.dereferenced_target) - bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0 - diverging_commit_counts = @repository.diverging_commit_counts(branch) @@ -12,7 +13,7 @@   - if branch.name == @repository.root_ref %span.label.label-primary default - - elsif @repository.merged_to_root_ref? branch.name + - elsif merged %span.label.label-info.has-tooltip{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } } = s_('Branches|merged') @@ -47,7 +48,7 @@ target: "#modal-delete-branch", delete_path: project_branch_path(@project, branch.name), branch_name: branch.name, - is_merged: ("true" if @repository.merged_to_root_ref?(branch.name)) } } + is_merged: ("true" if merged) } } = icon("trash-o") - else %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled", diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 7d9645d79e6..aade310236e 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -38,7 +38,7 @@ - if @branches.any? %ul.content-list.all-branches - @branches.each do |branch| - = render "projects/branches/branch", branch: branch + = render "projects/branches/branch", branch: branch, merged: @repository.merged_to_root_ref?(branch, @merged_branch_names) = paginate @branches, theme: 'gitlab' - else .nothing-here-block diff --git a/changelogs/unreleased/use-git-branch-merged.yml b/changelogs/unreleased/use-git-branch-merged.yml new file mode 100644 index 00000000000..24ec226250c --- /dev/null +++ b/changelogs/unreleased/use-git-branch-merged.yml @@ -0,0 +1,5 @@ +--- +title: Improve branch listing page performance +merge_request: 14729 +author: +type: performance diff --git a/lib/gitlab/git/branch.rb b/lib/gitlab/git/branch.rb index c53882787f1..3487e099381 100644 --- a/lib/gitlab/git/branch.rb +++ b/lib/gitlab/git/branch.rb @@ -3,6 +3,14 @@ module Gitlab module Git class Branch < Ref + def self.find(repo, branch_name) + if branch_name.is_a?(Gitlab::Git::Branch) + branch_name + else + repo.find_branch(branch_name) + end + end + def initialize(repository, name, target, target_commit) super(repository, name, target, target_commit) end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 408616d174b..fc8af38d4d9 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -511,6 +511,10 @@ module Gitlab gitaly_commit_client.ancestor?(from, to) end + def merged_branch_names(branch_names = []) + Set.new(git_merged_branch_names(branch_names)) + end + # Return an array of Diff objects that represent the diff # between +from+ and +to+. See Diff::filter_diff_options for the allowed # diff options. The +options+ hash can also include :break_rewrites to @@ -1190,6 +1194,13 @@ module Gitlab sort_branches(branches, sort_by) end + def git_merged_branch_names(branch_names = []) + lines = run_git(['branch', '--merged', root_ref] + branch_names) + .first.lines + + lines.map(&:strip) + end + def log_using_shell?(options) options[:path].present? || options[:disable_walk] || diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb index 318a7b7a332..708870060e7 100644 --- a/spec/lib/gitlab/git/branch_spec.rb +++ b/spec/lib/gitlab/git/branch_spec.rb @@ -7,6 +7,38 @@ describe Gitlab::Git::Branch, seed_helper: true do it { is_expected.to be_kind_of Array } + describe '.find' do + subject { described_class.find(repository, branch) } + + before do + allow(repository).to receive(:find_branch).with(branch) + .and_call_original + end + + context 'when finding branch via branch name' do + let(:branch) { 'master' } + + it 'returns a branch object' do + expect(subject).to be_a(described_class) + expect(subject.name).to eq(branch) + + expect(repository).to have_received(:find_branch).with(branch) + end + end + + context 'when the branch is already a branch' do + let(:commit) { repository.commit('master') } + let(:branch) { described_class.new(repository, 'master', commit.sha, commit) } + + it 'returns a branch object' do + expect(subject).to be_a(described_class) + expect(subject).to eq(branch) + + expect(repository).not_to have_received(:find_branch).with(branch) + end + end + end + describe '#size' do subject { super().size } it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) } diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index e3d320631bc..b161d25b96c 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1135,6 +1135,32 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#merged_branch_names' do + context 'when branch names are passed' do + it 'only returns the names we are asking' do + names = repository.merged_branch_names(%w[merge-test]) + + expect(names).to contain_exactly('merge-test') + end + + it 'does not return unmerged branch names' do + names = repository.merged_branch_names(%w[feature]) + + expect(names).to be_empty + end + end + + context 'when no branch names are specified' do + it 'returns all merged branch names' do + names = repository.merged_branch_names + + expect(names).to include('merge-test') + expect(names).to include('fix-mode') + expect(names).not_to include('feature') + end + 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 455d5e8a656..d7c07676911 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -299,6 +299,24 @@ describe Repository do it { is_expected.to be_falsey } end + + context 'when pre-loaded merged branches are provided' do + using RSpec::Parameterized::TableSyntax + + where(:branch, :pre_loaded, :expected) do + 'not-merged-branch' | ['branch-merged'] | false + 'branch-merged' | ['not-merged-branch'] | false + 'branch-merged' | ['branch-merged'] | true + 'not-merged-branch' | ['not-merged-branch'] | false + 'master' | ['master'] | false + end + + with_them do + subject { repository.merged_to_root_ref?(branch, pre_loaded) } + + it { is_expected.to eq(expected) } + end + end end describe '#can_be_merged?' do -- cgit v1.2.1 From a6c52c4e593850eb270e5bfbd021cdfb56e6eed6 Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira Date: Fri, 27 Oct 2017 22:11:21 +0200 Subject: Make merge_jid handling less stateful in MergeService --- app/models/merge_request.rb | 4 +- app/services/merge_requests/merge_service.rb | 9 +--- app/workers/stuck_merge_jobs_worker.rb | 2 +- spec/models/merge_request_spec.rb | 6 +++ spec/services/merge_requests/merge_service_spec.rb | 49 ---------------------- spec/workers/stuck_merge_jobs_worker_spec.rb | 9 +++- 6 files changed, 18 insertions(+), 61 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index c3fae16d109..c952ab862d7 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -396,7 +396,9 @@ class MergeRequest < ActiveRecord::Base end def merge_ongoing? - !!merge_jid && !merged? && Gitlab::SidekiqStatus.running?(merge_jid) + # While the MergeRequest is locked, it should present itself as 'merge ongoing'. + # The unlocking process is handled by StuckMergeJobsWorker scheduled in Cron. + locked? || !!merge_jid && !merged? && Gitlab::SidekiqStatus.running?(merge_jid) end def closed_without_fork? diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 8c5821aa870..156e7b2f078 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -82,16 +82,9 @@ module MergeRequests @merge_request.can_remove_source_branch?(branch_deletion_user) end - # Logs merge error message and cleans `MergeRequest#merge_jid`. - # def handle_merge_error(log_message:, save_message_on_model: false) Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{log_message}") - - if save_message_on_model - @merge_request.update(merge_error: log_message, merge_jid: nil) - else - clean_merge_jid - end + @merge_request.update(merge_error: log_message) if save_message_on_model end def merge_request_info diff --git a/app/workers/stuck_merge_jobs_worker.rb b/app/workers/stuck_merge_jobs_worker.rb index 7843179d77c..a396c0f27b2 100644 --- a/app/workers/stuck_merge_jobs_worker.rb +++ b/app/workers/stuck_merge_jobs_worker.rb @@ -23,7 +23,7 @@ class StuckMergeJobsWorker merge_requests = MergeRequest.where(id: completed_ids) merge_requests.where.not(merge_commit_sha: nil).update_all(state: :merged) - merge_requests.where(merge_commit_sha: nil).update_all(state: :opened) + merge_requests.where(merge_commit_sha: nil).update_all(state: :opened, merge_jid: nil) Rails.logger.info("Updated state of locked merge jobs. JIDs: #{completed_jids.join(', ')}") end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 2b8aa7cf9c3..476a2697605 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1460,6 +1460,12 @@ describe MergeRequest do end describe '#merge_ongoing?' do + it 'returns true when the merge request is locked' do + merge_request = build_stubbed(:merge_request, state: :locked) + + expect(merge_request.merge_ongoing?).to be(true) + end + it 'returns true when merge_id, MR is not merged and it has no running job' do merge_request = build_stubbed(:merge_request, state: :open, merge_jid: 'foo') allow(Gitlab::SidekiqStatus).to receive(:running?).with('foo') { true } diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index d1043f99b5a..ac196e92601 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -12,55 +12,6 @@ describe MergeRequests::MergeService do end describe '#execute' do - context 'MergeRequest#merge_jid' do - let(:service) do - described_class.new(project, user, commit_message: 'Awesome message') - end - - before do - merge_request.update_column(:merge_jid, 'hash-123') - end - - it 'is cleaned when no error is raised' do - service.execute(merge_request) - - expect(merge_request.reload.merge_jid).to be_nil - end - - it 'is cleaned when expected error is raised' do - allow(service).to receive(:commit).and_raise(described_class::MergeError) - - service.execute(merge_request) - - expect(merge_request.reload.merge_jid).to be_nil - end - - it 'is cleaned when merge request is not mergeable' do - allow(merge_request).to receive(:mergeable?).and_return(false) - - service.execute(merge_request) - - expect(merge_request.reload.merge_jid).to be_nil - end - - it 'is cleaned when no source is found' do - allow(merge_request).to receive(:diff_head_sha).and_return(nil) - - service.execute(merge_request) - - expect(merge_request.reload.merge_jid).to be_nil - end - - it 'is not cleaned when unexpected error is raised' do - service = described_class.new(project, user, commit_message: 'Awesome message') - allow(service).to receive(:commit).and_raise(StandardError) - - expect { service.execute(merge_request) }.to raise_error(StandardError) - - expect(merge_request.reload.merge_jid).to be_present - end - end - context 'valid params' do let(:service) { described_class.new(project, user, commit_message: 'Awesome message') } diff --git a/spec/workers/stuck_merge_jobs_worker_spec.rb b/spec/workers/stuck_merge_jobs_worker_spec.rb index a5ad78393c9..f8b55e873df 100644 --- a/spec/workers/stuck_merge_jobs_worker_spec.rb +++ b/spec/workers/stuck_merge_jobs_worker_spec.rb @@ -12,8 +12,13 @@ describe StuckMergeJobsWorker do worker.perform - expect(mr_with_sha.reload).to be_merged - expect(mr_without_sha.reload).to be_opened + mr_with_sha.reload + mr_without_sha.reload + + expect(mr_with_sha).to be_merged + expect(mr_without_sha).to be_opened + expect(mr_with_sha.merge_jid).to be_present + expect(mr_without_sha.merge_jid).to be_nil end it 'updates merge request to opened when locked but has not been merged' do -- cgit v1.2.1 From 306d030240b4678a94cf8bf8af11c07100abfa7c Mon Sep 17 00:00:00 2001 From: Brett Walker Date: Sat, 28 Oct 2017 10:49:14 +0200 Subject: more readable changelog --- changelogs/unreleased/39366-email-confirmation-fails.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/unreleased/39366-email-confirmation-fails.yml b/changelogs/unreleased/39366-email-confirmation-fails.yml index 7f6a753f8c8..a5568670c70 100644 --- a/changelogs/unreleased/39366-email-confirmation-fails.yml +++ b/changelogs/unreleased/39366-email-confirmation-fails.yml @@ -1,5 +1,5 @@ --- -title: 'Fix #39366 : E-mail confirmation fails: NoMethodError: undefined method "username"' +title: 'Fix bug preventing secondary emails from being confirmed' merge_request: 15010 author: type: fixed -- cgit v1.2.1 From 7e60c764282c84d4acea517903705a4c4e3a61f2 Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira Date: Fri, 27 Oct 2017 22:41:44 +0200 Subject: Add changelog --- changelogs/unreleased/make-merge-jid-handling-less-stateful.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/make-merge-jid-handling-less-stateful.yml diff --git a/changelogs/unreleased/make-merge-jid-handling-less-stateful.yml b/changelogs/unreleased/make-merge-jid-handling-less-stateful.yml new file mode 100644 index 00000000000..fe945e822fd --- /dev/null +++ b/changelogs/unreleased/make-merge-jid-handling-less-stateful.yml @@ -0,0 +1,5 @@ +--- +title: Fix widget of locked merge requests not being presented +merge_request: +author: +type: fixed -- cgit v1.2.1 From 7dbf114f595f8ff3ff41339d81d41b7722b97d2a Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Fri, 27 Oct 2017 16:09:11 +0200 Subject: Use the correct project visibility in system hooks --- app/services/system_hooks_service.rb | 2 +- changelogs/unreleased/bvl-fix-system-hook-project-visibility.yml | 5 +++++ spec/services/system_hooks_service_spec.rb | 6 ++++++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/bvl-fix-system-hook-project-visibility.yml diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index a1c2f8d0180..5d275967821 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -83,7 +83,7 @@ class SystemHooksService project_id: model.id, owner_name: owner.name, owner_email: owner.respond_to?(:email) ? owner.email : "", - project_visibility: Project.visibility_levels.key(model.visibility_level_value).downcase + project_visibility: model.visibility.downcase } end diff --git a/changelogs/unreleased/bvl-fix-system-hook-project-visibility.yml b/changelogs/unreleased/bvl-fix-system-hook-project-visibility.yml new file mode 100644 index 00000000000..a17ed51c9b8 --- /dev/null +++ b/changelogs/unreleased/bvl-fix-system-hook-project-visibility.yml @@ -0,0 +1,5 @@ +--- +title: Use the correct visibility attribute for projects in system hooks +merge_request: 15065 +author: +type: fixed diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb index 8b5d9187785..8f7aea533dc 100644 --- a/spec/services/system_hooks_service_spec.rb +++ b/spec/services/system_hooks_service_spec.rb @@ -63,6 +63,12 @@ describe SystemHooksService do :group_id, :user_id, :user_username, :user_name, :user_email, :group_access ) end + + it 'includes the correct project visibility level' do + data = event_data(project, :create) + + expect(data[:project_visibility]).to eq('private') + end end context 'event names' do -- cgit v1.2.1 From cd784a80d7c779edc468d6255ffdeb8ea692661a Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira Date: Mon, 30 Oct 2017 09:10:09 +0000 Subject: [CE backport] Saved configuration for issue board --- .../javascripts/boards/filtered_search_boards.js | 9 +- .../javascripts/boards/stores/boards_store.js | 8 +- .../javascripts/filtered_search/dropdown_utils.js | 10 ++ .../filtered_search/filtered_search_manager.js | 10 +- .../filtered_search_visual_tokens.js | 15 +-- app/assets/javascripts/labels_select.js | 13 ++- app/assets/javascripts/milestone_select.js | 17 +++- app/assets/javascripts/users_select.js | 7 +- .../vue_shared/components/loading_icon.vue | 8 +- .../vue_shared/components/popup_dialog.vue | 101 +++++++++++++-------- app/assets/stylesheets/framework/common.scss | 3 + app/assets/stylesheets/framework/dropdowns.scss | 1 + app/assets/stylesheets/framework/modal.scss | 8 ++ .../framework/tw_bootstrap_variables.scss | 33 +++++++ app/assets/stylesheets/pages/repo.scss | 13 --- app/helpers/boards_helper.rb | 11 --- 16 files changed, 174 insertions(+), 93 deletions(-) diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index 3f083655f95..184665f395c 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -11,7 +11,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { // Issue boards is slightly different, we handle all the requests async // instead or reloading the page, we just re-fire the list ajax requests this.isHandledAsync = true; - this.cantEdit = cantEdit; + this.cantEdit = cantEdit.filter(i => typeof i === 'string'); + this.cantEditWithValue = cantEdit.filter(i => typeof i === 'object'); } updateObject(path) { @@ -42,7 +43,9 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { this.filteredSearchInput.dispatchEvent(new Event('input')); } - canEdit(tokenName) { - return this.cantEdit.indexOf(tokenName) === -1; + canEdit(tokenName, tokenValue) { + if (this.cantEdit.includes(tokenName)) return false; + return this.cantEditWithValue.findIndex(token => token.name === tokenName && + token.value === tokenValue) === -1; } } diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index ea82958e80d..798d7e0d147 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -14,16 +14,18 @@ gl.issueBoards.BoardsStore = { }, state: {}, detail: { - issue: {} + issue: {}, }, moving: { issue: {}, - list: {} + list: {}, }, create () { this.state.lists = []; this.filter.path = getUrlParamsArray().join('&'); - this.detail = { issue: {} }; + this.detail = { + issue: {}, + }; }, addList (listObj, defaultAvatar) { const list = new List(listObj, defaultAvatar); diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js index 8d711e3213c..cf8a9b0402b 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -147,6 +147,16 @@ class DropdownUtils { return dataValue !== null; } + static getVisualTokenValues(visualToken) { + const tokenName = visualToken && visualToken.querySelector('.name').textContent.trim(); + let tokenValue = visualToken && visualToken.querySelector('.value') && visualToken.querySelector('.value').textContent.trim(); + if (tokenName === 'label' && tokenValue) { + // remove leading symbol and wrapping quotes + tokenValue = tokenValue.replace(/^~("|')?(.*)/, '$2').replace(/("|')$/, ''); + } + return { tokenName, tokenValue }; + } + // Determines the full search query (visual tokens + input) static getSearchQuery(untilInput = false) { const container = FilteredSearchContainer.container; diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 7b233842d5a..69c57f923b6 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -185,8 +185,8 @@ class FilteredSearchManager { if (e.keyCode === 8 || e.keyCode === 46) { const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); - const sanitizedTokenName = lastVisualToken && lastVisualToken.querySelector('.name').textContent.trim(); - const canEdit = sanitizedTokenName && this.canEdit && this.canEdit(sanitizedTokenName); + const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken); + const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue); if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) { this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial(); gl.FilteredSearchVisualTokens.removeLastTokenPartial(); @@ -336,8 +336,8 @@ class FilteredSearchManager { let canClearToken = t.classList.contains('js-visual-token'); if (canClearToken) { - const tokenKey = t.querySelector('.name').textContent.trim(); - canClearToken = this.canEdit && this.canEdit(tokenKey); + const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(t); + canClearToken = this.canEdit && this.canEdit(tokenName, tokenValue); } if (canClearToken) { @@ -469,7 +469,7 @@ class FilteredSearchManager { } hasFilteredSearch = true; - const canEdit = this.canEdit && this.canEdit(sanitizedKey); + const canEdit = this.canEdit && this.canEdit(sanitizedKey, sanitizedValue); gl.FilteredSearchVisualTokens.addFilterVisualToken( sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`, diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index d2f92929b8a..6139e81fe6d 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -38,21 +38,14 @@ class FilteredSearchVisualTokens { } static createVisualTokenElementHTML(canEdit = true) { - let removeTokenMarkup = ''; - if (canEdit) { - removeTokenMarkup = ` -
- -
- `; - } - return ` -
+
- ${removeTokenMarkup} +
+ +
`; diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 84602cf9207..1e52963b1dd 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -8,7 +8,7 @@ import CreateLabelDropdown from './create_label'; (function() { this.LabelsSelect = (function() { - function LabelsSelect(els) { + function LabelsSelect(els, options = {}) { var _this, $els; _this = this; @@ -58,6 +58,7 @@ import CreateLabelDropdown from './create_label'; labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <%- label.title %> <% }); %>'); labelNoneHTMLTemplate = 'None'; } + const handleClick = options.handleClick; $sidebarLabelTooltip.tooltip(); @@ -316,9 +317,9 @@ import CreateLabelDropdown from './create_label'; }, multiSelect: $dropdown.hasClass('js-multiselect'), vue: $dropdown.hasClass('js-issue-board-sidebar'), - clicked: function(options) { - const { $el, e, isMarking } = options; - const label = options.selectedObj; + clicked: function(clickEvent) { + const { $el, e, isMarking } = clickEvent; + const label = clickEvent.selectedObj; var isIssueIndex, isMRIndex, page, boardsModel; var fadeOutLoader = () => { @@ -391,6 +392,10 @@ import CreateLabelDropdown from './create_label'; .then(fadeOutLoader) .catch(fadeOutLoader); } + else if (handleClick) { + e.preventDefault(); + handleClick(label); + } else { if ($dropdown.hasClass('js-multiselect')) { diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index e7d5325a509..74e5a4f1cea 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -5,7 +5,7 @@ import _ from 'underscore'; (function() { this.MilestoneSelect = (function() { - function MilestoneSelect(currentProject, els) { + function MilestoneSelect(currentProject, els, options = {}) { var _this, $els; if (currentProject != null) { _this = this; @@ -136,19 +136,26 @@ import _ from 'underscore'; }, opened: function(e) { const $el = $(e.currentTarget); - if ($dropdown.hasClass('js-issue-board-sidebar')) { + if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) { selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault; } $('a.is-active', $el).removeClass('is-active'); $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active'); }, vue: $dropdown.hasClass('js-issue-board-sidebar'), - clicked: function(options) { - const { $el, e } = options; - let selected = options.selectedObj; + clicked: function(clickEvent) { + const { $el, e } = clickEvent; + let selected = clickEvent.selectedObj; var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore; if (!selected) return; + + if (options.handleClick) { + e.preventDefault(); + options.handleClick(selected); + return; + } + page = $('body').attr('data-page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = (page === page && page === 'projects:merge_requests:index'); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index a0883b32593..759cc9925f4 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -6,7 +6,7 @@ import _ from 'underscore'; // TODO: remove eventHub hack after code splitting refactor window.emitSidebarEvent = window.emitSidebarEvent || $.noop; -function UsersSelect(currentUser, els) { +function UsersSelect(currentUser, els, options = {}) { var $els; this.users = this.users.bind(this); this.user = this.user.bind(this); @@ -20,6 +20,8 @@ function UsersSelect(currentUser, els) { } } + const { handleClick } = options; + $els = $(els); if (!els) { @@ -442,6 +444,9 @@ function UsersSelect(currentUser, els) { } if ($el.closest('.add-issues-modal').length) { gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id; + } else if (handleClick) { + e.preventDefault(); + handleClick(user, isMarking); } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { return Issuable.filterResults($dropdown.closest('form')); } else if ($dropdown.hasClass('js-filter-submit')) { diff --git a/app/assets/javascripts/vue_shared/components/loading_icon.vue b/app/assets/javascripts/vue_shared/components/loading_icon.vue index 15581d5c2a0..494fe4468d9 100644 --- a/app/assets/javascripts/vue_shared/components/loading_icon.vue +++ b/app/assets/javascripts/vue_shared/components/loading_icon.vue @@ -18,6 +18,12 @@ required: false, default: false, }, + + class: { + type: String, + required: false, + default: '', + }, }, computed: { @@ -25,7 +31,7 @@ return this.inline ? 'span' : 'div'; }, cssClass() { - return `fa-${this.size}x`; + return `fa-${this.size}x ${this.class}`.trim(); }, }, }; diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/popup_dialog.vue index 9e8c10bdc1a..fc6421fecb9 100644 --- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue +++ b/app/assets/javascripts/vue_shared/components/popup_dialog.vue @@ -5,17 +5,27 @@ export default { props: { title: { type: String, - required: true, + required: false, }, text: { type: String, required: false, }, + hideFooter: { + type: Boolean, + required: false, + default: false, + }, kind: { type: String, required: false, default: 'primary', }, + modalDialogClass: { + type: String, + required: false, + default: '', + }, closeKind: { type: String, required: false, @@ -30,6 +40,11 @@ export default { type: String, required: true, }, + submitDisabled: { + type: Boolean, + required: false, + default: false, + }, }, computed: { @@ -57,43 +72,57 @@ export default { diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 96f9dda26c4..1cfd7ef01a8 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -4,6 +4,9 @@ .cred { color: $common-red; } .cgreen { color: $common-green; } .cdark { color: $common-gray-dark; } +.text-secondary { + color: $gl-text-color-secondary; +} /** COMMON CLASSES **/ .prepend-top-0 { margin-top: 0; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index a9d804e735d..63697fd38a7 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -37,6 +37,7 @@ .dropdown-menu-nav { @include set-visible; display: block; + min-height: 40px; @media (max-width: $screen-xs-max) { width: 100%; diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 1cebd02df48..d218fb6d702 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -42,3 +42,11 @@ body.modal-open { width: 98%; } } + +.modal.popup-dialog { + display: block; +} + +.modal-body { + background-color: $modal-body-bg; +} diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index 3ea77eb7a43..a23131e0818 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -164,3 +164,36 @@ $pre-border-color: $border-color; $table-bg-accent: $gray-light; $zindex-popover: 900; + +//== Modals +// +//## + +//** Padding applied to the modal body +$modal-inner-padding: $gl-padding; + +//** Padding applied to the modal title +$modal-title-padding: $gl-padding; +//** Modal title line-height +// $modal-title-line-height: $line-height-base + +//** Background color of modal content area +$modal-content-bg: $gray-light; +$modal-body-bg: $white-light; +//** Modal content border color +// $modal-content-border-color: rgba(0,0,0,.2) +//** Modal content border color **for IE8** +// $modal-content-fallback-border-color: #999 + +//** Modal backdrop background color +// $modal-backdrop-bg: #000 +//** Modal backdrop opacity +// $modal-backdrop-opacity: .5 +//** Modal header border color +// $modal-header-border-color: #e5e5e5 +//** Modal footer border color +// $modal-footer-border-color: $modal-header-border-color + +// $modal-lg: 900px +// $modal-md: 600px +// $modal-sm: 300px diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 6a363b1710e..e8c7f8a8fc0 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -7,19 +7,6 @@ background: $black-transparent; } -.modal.popup-dialog { - display: block; - background-color: $black-transparent; - z-index: 2100; - - @media (min-width: $screen-md-min) { - .modal-dialog { - width: 600px; - margin: 30px auto; - } - } -} - .project-refs-form, .project-refs-target-form { display: inline-block; diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index 7112c6ee470..c4a621160af 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -20,17 +20,6 @@ module BoardsHelper project_issues_path(@project) end - def current_board_json - board = @board || @boards.first - - board.to_json( - only: [:id, :name, :milestone_id], - include: { - milestone: { only: [:title] } - } - ) - end - def board_base_url project_boards_path(@project) end -- cgit v1.2.1 From 8275e34e41cc6a6e20c2c4fbfa5dccd9c8e498b6 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Mon, 30 Oct 2017 10:25:28 +0100 Subject: Ci::Build tag is a trait instead of an own factory Minor annoyance of mine, and there were a couple of things wrong, for example: 1. Switching on a property is just a trait 2. It didn't inherrit from its parent Find and replace through the code based fixed all occurances. --- spec/factories/ci/builds.rb | 2 +- spec/requests/api/runner_spec.rb | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index c2b59239af9..cf38066dedc 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -119,7 +119,7 @@ FactoryGirl.define do finished_at nil end - factory :ci_build_tag do + trait :tag do tag true end diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 5fd76fce7df..47f4ccd4887 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -385,7 +385,7 @@ describe API::Runner do end context 'when job is made for tag' do - let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } it 'sets branch as ref_type' do request_job @@ -436,8 +436,8 @@ describe API::Runner do end context 'when project and pipeline have multiple jobs' do - let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } - let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } + let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) } before do @@ -458,7 +458,7 @@ describe API::Runner do end context 'when pipeline have jobs with artifacts' do - let!(:job) { create(:ci_build_tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:job) { create(:ci_build, :tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) } before do @@ -478,8 +478,8 @@ describe API::Runner do end context 'when explicit dependencies are defined' do - let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } - let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } + let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } let!(:test_job) do create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'deploy', stage: 'deploy', stage_idx: 1, @@ -502,8 +502,8 @@ describe API::Runner do end context 'when dependencies is an empty array' do - let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } - let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } + let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } let!(:empty_dependencies_job) do create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'empty_dependencies_job', stage: 'deploy', stage_idx: 1, -- cgit v1.2.1 From c4124fce48e83db603d69a83bb912ae5d0d54f65 Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira Date: Mon, 30 Oct 2017 11:21:23 +0100 Subject: Move locked check to a guard-clause --- app/models/merge_request.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index c952ab862d7..07352db5d2d 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -398,7 +398,9 @@ class MergeRequest < ActiveRecord::Base def merge_ongoing? # While the MergeRequest is locked, it should present itself as 'merge ongoing'. # The unlocking process is handled by StuckMergeJobsWorker scheduled in Cron. - locked? || !!merge_jid && !merged? && Gitlab::SidekiqStatus.running?(merge_jid) + return true if locked? + + !!merge_jid && !merged? && Gitlab::SidekiqStatus.running?(merge_jid) end def closed_without_fork? -- cgit v1.2.1