summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWinnie Hellmann <winnie@gitlab.com>2017-11-23 14:35:50 +0000
committerWinnie Hellmann <winnie@gitlab.com>2017-11-23 14:35:50 +0000
commite71a9461894d432bcf8d38228446584d294d2303 (patch)
tree94fb2f14ca9e5a116ec706e8afc77394e303e7d4
parenteb2643f2a9eb71c0f463bc8c7ee63358fdbfd6c3 (diff)
parentbbad7b8c5c37d8534a9836016c75425bd2728e41 (diff)
downloadgitlab-ce-e71a9461894d432bcf8d38228446584d294d2303.tar.gz
Merge branch '10-2-stable-patch-2' into '10-2-stable'
Prepare 10.2.2 release See merge request gitlab-org/gitlab-ce!15559
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js52
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue (renamed from app/assets/javascripts/boards/components/board_card.js)40
-rw-r--r--app/assets/javascripts/boards/components/board_list.js2
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js11
-rw-r--r--app/assets/javascripts/boards/models/issue.js13
-rw-r--r--app/assets/javascripts/boards/services/board_service.js10
-rw-r--r--app/assets/javascripts/dispatcher.js3
-rw-r--r--app/assets/javascripts/environments/components/environment.vue36
-rw-r--r--app/assets/javascripts/init_issuable_sidebar.js1
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue6
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description.vue8
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue8
-rw-r--r--app/assets/javascripts/main.js1
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue6
-rw-r--r--app/assets/javascripts/subscription.js45
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue10
-rw-r--r--app/assets/stylesheets/framework/blank.scss9
-rw-r--r--app/controllers/application_controller.rb6
-rw-r--r--app/controllers/boards/issues_controller.rb1
-rw-r--r--app/models/ci/pipeline.rb5
-rw-r--r--app/models/issue.rb7
-rw-r--r--app/models/note.rb15
-rw-r--r--app/models/repository.rb6
-rw-r--r--app/models/system_note_metadata.rb10
-rw-r--r--app/services/merge_requests/merge_service.rb12
-rw-r--r--app/services/milestones/promote_service.rb23
-rw-r--r--app/services/system_note_service.rb4
-rw-r--r--app/views/layouts/_mailer.html.haml4
-rw-r--r--app/views/projects/environments/show.html.haml17
-rw-r--r--app/views/projects/tags/index.html.haml2
-rw-r--r--app/views/shared/boards/components/sidebar/_notifications.html.haml10
-rw-r--r--changelogs/unreleased/1870-impersonation-stuck-on-password-change.yml5
-rw-r--r--changelogs/unreleased/39167-async-boards-sidebar.yml5
-rw-r--r--changelogs/unreleased/39461-notes-api-for-issues-no-longer-returns-label-additions-removals.yml5
-rw-r--r--changelogs/unreleased/40292-bitbucket-import-hashed-storage.yml5
-rw-r--r--changelogs/unreleased/40377-blank-states.yml5
-rw-r--r--changelogs/unreleased/bvl-dont-move-projects-using-hashed-storage.yml5
-rw-r--r--changelogs/unreleased/fix-ci-pipelines-index.yml5
-rw-r--r--changelogs/unreleased/issue_40337.yml5
-rw-r--r--changelogs/unreleased/osw-merge-process-logs.yml5
-rw-r--r--changelogs/unreleased/reduce-queries-for-artifacts-button.yml5
-rw-r--r--db/migrate/20171121144800_ci_pipelines_index_on_project_id_ref_status_id.rb35
-rw-r--r--db/post_migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb1
-rw-r--r--db/schema.rb4
-rw-r--r--lib/api/notes.rb4
-rw-r--r--lib/gitlab/bitbucket_import/importer.rb4
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb12
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb8
-rw-r--r--spec/controllers/application_controller_spec.rb4
-rw-r--r--spec/features/admin/admin_users_spec.rb23
-rw-r--r--spec/features/boards/sidebar_spec.rb22
-rw-r--r--spec/features/issuables/shortcuts_issuable_spec.rb46
-rw-r--r--spec/fixtures/api/schemas/issue.json2
-rw-r--r--spec/javascripts/boards/board_card_spec.js29
-rw-r--r--spec/javascripts/boards/issue_spec.js13
-rw-r--r--spec/javascripts/vue_shared/components/loading_button_spec.js1
-rw-r--r--spec/javascripts/vue_shared/components/markdown/toolbar_spec.js37
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb33
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb16
-rw-r--r--spec/models/ci/pipeline_spec.rb4
-rw-r--r--spec/models/note_spec.rb31
-rw-r--r--spec/models/repository_spec.rb25
-rw-r--r--spec/services/milestones/promote_service_spec.rb36
-rw-r--r--spec/spec_helper.rb1
-rw-r--r--spec/support/selection_helper.rb6
-rw-r--r--vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml4
70 files changed, 683 insertions, 168 deletions
diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js
index ef4093b59e3..20d23162940 100644
--- a/app/assets/javascripts/boards/boards_bundle.js
+++ b/app/assets/javascripts/boards/boards_bundle.js
@@ -1,12 +1,13 @@
/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
-/* global BoardService */
import _ from 'underscore';
import Vue from 'vue';
import VueResource from 'vue-resource';
import Flash from '../flash';
+import { __ } from '../locale';
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
+import sidebarEventHub from '../sidebar/event_hub';
import './models/issue';
import './models/label';
import './models/list';
@@ -14,7 +15,7 @@ import './models/milestone';
import './models/assignee';
import './stores/boards_store';
import './stores/modal_store';
-import './services/board_service';
+import BoardService from './services/board_service';
import './mixins/modal_mixins';
import './mixins/sortable_default_options';
import './filters/due_date_filters';
@@ -77,11 +78,16 @@ $(() => {
});
Store.rootPath = this.boardsEndpoint;
- // Listen for updateTokens event
eventHub.$on('updateTokens', this.updateTokens);
+ eventHub.$on('newDetailIssue', this.updateDetailIssue);
+ eventHub.$on('clearDetailIssue', this.clearDetailIssue);
+ sidebarEventHub.$on('toggleSubscription', this.toggleSubscription);
},
beforeDestroy() {
eventHub.$off('updateTokens', this.updateTokens);
+ eventHub.$off('newDetailIssue', this.updateDetailIssue);
+ eventHub.$off('clearDetailIssue', this.clearDetailIssue);
+ sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
},
mounted () {
this.filterManager = new FilteredSearchBoards(Store.filter, true);
@@ -112,6 +118,46 @@ $(() => {
methods: {
updateTokens() {
this.filterManager.updateTokens();
+ },
+ updateDetailIssue(newIssue) {
+ const sidebarInfoEndpoint = newIssue.sidebarInfoEndpoint;
+ if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
+ newIssue.setFetchingState('subscriptions', true);
+ BoardService.getIssueInfo(sidebarInfoEndpoint)
+ .then(res => res.json())
+ .then((data) => {
+ newIssue.setFetchingState('subscriptions', false);
+ newIssue.updateData({
+ subscribed: data.subscribed,
+ });
+ })
+ .catch(() => {
+ newIssue.setFetchingState('subscriptions', false);
+ Flash(__('An error occurred while fetching sidebar data'));
+ });
+ }
+
+ Store.detail.issue = newIssue;
+ },
+ clearDetailIssue() {
+ Store.detail.issue = {};
+ },
+ toggleSubscription(id) {
+ const issue = Store.detail.issue;
+ if (issue.id === id && issue.toggleSubscriptionEndpoint) {
+ issue.setFetchingState('subscriptions', true);
+ BoardService.toggleIssueSubscription(issue.toggleSubscriptionEndpoint)
+ .then(() => {
+ issue.setFetchingState('subscriptions', false);
+ issue.updateData({
+ subscribed: !issue.subscribed,
+ });
+ })
+ .catch(() => {
+ issue.setFetchingState('subscriptions', false);
+ Flash(__('An error occurred when toggling the notification subscription'));
+ });
+ }
}
},
});
diff --git a/app/assets/javascripts/boards/components/board_card.js b/app/assets/javascripts/boards/components/board_card.vue
index 079fb6438b9..0b220a56e0b 100644
--- a/app/assets/javascripts/boards/components/board_card.js
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -1,25 +1,11 @@
+<script>
import './issue_card_inner';
+import eventHub from '../eventhub';
const Store = gl.issueBoards.BoardsStore;
export default {
name: 'BoardsIssueCard',
- template: `
- <li class="card"
- :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }"
- :index="index"
- :data-issue-id="issue.id"
- @mousedown="mouseDown"
- @mousemove="mouseMove"
- @mouseup="showIssue($event)">
- <issue-card-inner
- :list="list"
- :issue="issue"
- :issue-link-base="issueLinkBase"
- :root-path="rootPath"
- :update-filters="true" />
- </li>
- `,
components: {
'issue-card-inner': gl.issueBoards.IssueCardInner,
},
@@ -56,12 +42,30 @@ export default {
this.showDetail = false;
if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
- Store.detail.issue = {};
+ eventHub.$emit('clearDetailIssue');
} else {
- Store.detail.issue = this.issue;
+ eventHub.$emit('newDetailIssue', this.issue);
Store.detail.list = this.list;
}
}
},
},
};
+</script>
+
+<template>
+ <li class="card"
+ :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }"
+ :index="index"
+ :data-issue-id="issue.id"
+ @mousedown="mouseDown"
+ @mousemove="mouseMove"
+ @mouseup="showIssue($event)">
+ <issue-card-inner
+ :list="list"
+ :issue="issue"
+ :issue-link-base="issueLinkBase"
+ :root-path="rootPath"
+ :update-filters="true" />
+ </li>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js
index 6159680f1e6..29aeb8e84aa 100644
--- a/app/assets/javascripts/boards/components/board_list.js
+++ b/app/assets/javascripts/boards/components/board_list.js
@@ -1,6 +1,6 @@
/* global Sortable */
import boardNewIssue from './board_new_issue';
-import boardCard from './board_card';
+import boardCard from './board_card.vue';
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 9ae5e270a4b..faa76da964f 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -5,12 +5,13 @@
import Vue from 'vue';
import Flash from '../../flash';
import eventHub from '../../sidebar/event_hub';
-import AssigneeTitle from '../../sidebar/components/assignees/assignee_title';
-import Assignees from '../../sidebar/components/assignees/assignees';
+import assigneeTitle from '../../sidebar/components/assignees/assignee_title';
+import assignees from '../../sidebar/components/assignees/assignees';
import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue';
import IssuableContext from '../../issuable_context';
import LabelsSelect from '../../labels_select';
+import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
const Store = gl.issueBoards.BoardsStore;
@@ -117,11 +118,11 @@ gl.issueBoards.BoardSidebar = Vue.extend({
new DueDateSelectors();
new LabelsSelect();
new Sidebar();
- gl.Subscription.bindAll('.subscription');
},
components: {
+ assigneeTitle,
+ assignees,
removeBtn: gl.issueBoards.RemoveIssueBtn,
- 'assignee-title': AssigneeTitle,
- assignees: Assignees,
+ subscriptions,
},
});
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index 407db176446..10f85c1d676 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -17,6 +17,11 @@ class ListIssue {
this.assignees = [];
this.selected = false;
this.position = obj.relative_position || Infinity;
+ this.isFetching = {
+ subscriptions: true,
+ };
+ this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
+ this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
if (obj.milestone) {
this.milestone = new ListMilestone(obj.milestone);
@@ -73,6 +78,14 @@ class ListIssue {
return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id));
}
+ updateData(newData) {
+ Object.assign(this, newData);
+ }
+
+ setFetchingState(key, value) {
+ this.isFetching[key] = value;
+ }
+
update (url) {
const data = {
issue: {
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
index 97e80afa3f8..fa7ddd25e1f 100644
--- a/app/assets/javascripts/boards/services/board_service.js
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -2,7 +2,7 @@
import Vue from 'vue';
-class BoardService {
+export default class BoardService {
constructor ({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) {
this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, {
issues: {
@@ -88,6 +88,14 @@ class BoardService {
return this.issues.bulkUpdate(data);
}
+
+ static getIssueInfo(endpoint) {
+ return Vue.http.get(endpoint);
+ }
+
+ static toggleIssueSubscription(endpoint) {
+ return Vue.http.post(endpoint);
+ }
}
window.BoardService = BoardService;
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 44606989395..721db847d3f 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -317,7 +317,6 @@ import Diff from './diff';
break;
case 'projects:merge_requests:show':
new Diff();
- shortcut_handler = new ShortcutsIssuable(true);
new ZenMode();
initIssuableSidebar();
@@ -327,6 +326,8 @@ import Diff from './diff';
window.mergeRequest = new MergeRequest({
action: mrShowNode.dataset.mrAction,
});
+
+ shortcut_handler = new ShortcutsIssuable(true);
break;
case 'dashboard:activity':
new gl.Activities();
diff --git a/app/assets/javascripts/environments/components/environment.vue b/app/assets/javascripts/environments/components/environment.vue
index c039ae85cfb..ffb7757bed8 100644
--- a/app/assets/javascripts/environments/components/environment.vue
+++ b/app/assets/javascripts/environments/components/environment.vue
@@ -227,25 +227,27 @@ export default {
/>
<div
- class="blank-state blank-state-no-icon"
+ class="blank-state-row"
v-if="!isLoading && state.environments.length === 0">
- <h2 class="blank-state-title js-blank-state-title">
- You don't have any environments right now.
- </h2>
- <p class="blank-state-text">
- Environments are places where code gets deployed, such as staging or production.
- <br />
- <a :href="helpPagePath">
- Read more about environments
+ <div class="blank-state-center">
+ <h2 class="blank-state-title js-blank-state-title">
+ You don't have any environments right now.
+ </h2>
+ <p class="blank-state-text">
+ Environments are places where code gets deployed, such as staging or production.
+ <br />
+ <a :href="helpPagePath">
+ Read more about environments
+ </a>
+ </p>
+
+ <a
+ v-if="canCreateEnvironmentParsed"
+ :href="newEnvironmentPath"
+ class="btn btn-create js-new-environment-button">
+ New environment
</a>
- </p>
-
- <a
- v-if="canCreateEnvironmentParsed"
- :href="newEnvironmentPath"
- class="btn btn-create js-new-environment-button">
- New environment
- </a>
+ </div>
</div>
<div
diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js
index 1191e0b895e..ada693afc46 100644
--- a/app/assets/javascripts/init_issuable_sidebar.js
+++ b/app/assets/javascripts/init_issuable_sidebar.js
@@ -14,7 +14,6 @@ export default () => {
});
new LabelsSelect();
new IssuableContext(sidebarOptions.currentUser);
- gl.Subscription.bindAll('.subscription');
new DueDateSelectors();
window.sidebar = new Sidebar();
};
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index e8ac8d3b5bb..4e39d483b31 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -102,6 +102,11 @@ export default {
required: false,
default: 'issue',
},
+ canAttachFile: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
const store = new Store({
@@ -234,6 +239,7 @@ export default {
:project-path="projectPath"
:project-namespace="projectNamespace"
:show-delete-button="showDeleteButton"
+ :can-attach-file="canAttachFile"
/>
<div v-else>
<title-component
diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue
index 0aa1b2c2e31..4d2ef409bad 100644
--- a/app/assets/javascripts/issue_show/components/fields/description.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description.vue
@@ -17,6 +17,11 @@
type: String,
required: true,
},
+ canAttachFile: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
components: {
markdownField,
@@ -36,7 +41,8 @@
</label>
<markdown-field
:markdown-preview-path="markdownPreviewPath"
- :markdown-docs-path="markdownDocsPath">
+ :markdown-docs-path="markdownDocsPath"
+ :can-attach-file="canAttachFile">
<textarea
id="issue-description"
class="note-textarea js-gfm-input js-autosize markdown-area"
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
index 8bb5c86d567..d61776d480d 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -41,6 +41,11 @@
required: false,
default: true,
},
+ canAttachFile: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
components: {
lockedWarning,
@@ -83,7 +88,8 @@
<description-field
:form-state="formState"
:markdown-preview-path="markdownPreviewPath"
- :markdown-docs-path="markdownDocsPath" />
+ :markdown-docs-path="markdownDocsPath"
+ :can-attach-file="canAttachFile" />
<edit-actions
:form-state="formState"
:can-destroy="canDestroy"
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 31c5cfc5e55..21b23d97952 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -88,7 +88,6 @@ import './right_sidebar';
import './search';
import './search_autocomplete';
import './smart_interval';
-import './subscription';
import './subscription_select';
import initBreadcrumbs from './breadcrumb';
diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index 3da60e88474..55ab83dfc0b 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -202,9 +202,11 @@
/>
<div
- class="blank-state blank-state-no-icon"
+ class="blank-state-row"
v-if="shouldRenderNoPipelinesMessage">
- <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
+ <div class="blank-state-center">
+ <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
+ </div>
</div>
<div
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
index 4ad3d469f25..25acc099699 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
@@ -3,6 +3,7 @@ import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub';
import Flash from '../../../flash';
+import { __ } from '../../../locale';
import subscriptions from './subscriptions.vue';
export default {
@@ -21,7 +22,7 @@ export default {
onToggleSubscription() {
this.mediator.toggleSubscription()
.catch(() => {
- Flash('Error occurred when toggling the notification subscription');
+ Flash(__('Error occurred when toggling the notification subscription'));
});
},
},
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
index a3a8213d63a..940e1764f3d 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -14,6 +14,10 @@ export default {
type: Boolean,
required: false,
},
+ id: {
+ type: Number,
+ required: false,
+ },
},
components: {
loadingButton,
@@ -32,7 +36,7 @@ export default {
},
methods: {
toggleSubscription() {
- eventHub.$emit('toggleSubscription');
+ eventHub.$emit('toggleSubscription', this.id);
},
},
};
diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js
deleted file mode 100644
index bb4d68fcd49..00000000000
--- a/app/assets/javascripts/subscription.js
+++ /dev/null
@@ -1,45 +0,0 @@
-class Subscription {
- constructor(containerElm) {
- this.containerElm = containerElm;
-
- const subscribeButton = containerElm.querySelector('.js-subscribe-button');
- if (subscribeButton) {
- // remove class so we don't bind twice
- subscribeButton.classList.remove('js-subscribe-button');
- subscribeButton.addEventListener('click', this.toggleSubscription.bind(this));
- }
- }
-
- toggleSubscription(event) {
- const button = event.currentTarget;
- const buttonSpan = button.querySelector('span');
- if (!buttonSpan || button.classList.contains('disabled')) {
- return;
- }
- button.classList.add('disabled');
-
- const isSubscribed = buttonSpan.innerHTML.trim().toLowerCase() !== 'subscribe';
- const toggleActionUrl = this.containerElm.dataset.url;
-
- $.post(toggleActionUrl, () => {
- button.classList.remove('disabled');
-
- // hack to allow this to work with the issue boards Vue object
- if (document.querySelector('html').classList.contains('issue-boards-page')) {
- gl.issueBoards.boardStoreIssueSet(
- 'subscribed',
- !gl.issueBoards.BoardsStore.detail.issue.subscribed,
- );
- } else {
- buttonSpan.innerHTML = isSubscribed ? 'Subscribe' : 'Unsubscribe';
- }
- });
- }
-
- static bindAll(selector) {
- [].forEach.call(document.querySelectorAll(selector), elm => new Subscription(elm));
- }
-}
-
-window.gl = window.gl || {};
-window.gl.Subscription = Subscription;
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js
index 4998a47b691..eeb990908f6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js
@@ -14,7 +14,7 @@ export default {
statusObj() {
return {
group: this.status,
- icon: `icon_status_${this.status}`,
+ icon: `status_${this.status}`,
};
},
},
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index a873e00d0f3..ee50ce27c3d 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -25,6 +25,11 @@
type: String,
required: false,
},
+ canAttachFile: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -129,6 +134,7 @@
<markdown-toolbar
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
+ :can-attach-file="canAttachFile"
/>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index 65fe7bbd94e..ea2509d2839 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -9,6 +9,11 @@
type: String,
required: false,
},
+ canAttachFile: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
};
</script>
@@ -41,7 +46,10 @@
are supported
</template>
</div>
- <span class="uploading-container">
+ <span
+ v-if="canAttachFile"
+ class="uploading-container"
+ >
<span class="uploading-progress-container hide">
<i
class="fa fa-file-image-o toolbar-button-icon"
diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss
index 10f9e9b70b0..9982a5779af 100644
--- a/app/assets/stylesheets/framework/blank.scss
+++ b/app/assets/stylesheets/framework/blank.scss
@@ -56,6 +56,12 @@
}
}
+.blank-state-center {
+ padding-top: 20px;
+ padding-bottom: 20px;
+ text-align: center;
+}
+
.blank-state {
padding: 20px;
border: 1px solid $border-color;
@@ -66,7 +72,10 @@
align-items: center;
padding: 50px 30px;
}
+}
+.blank-state,
+.blank-state-center {
.blank-state-icon {
svg {
display: block;
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 3be7aee69bc..1f0f9cb562e 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -212,7 +212,11 @@ class ApplicationController < ActionController::Base
end
def check_password_expiration
- if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user?
+ return if session[:impersonator_id] || current_user&.ldap_user?
+
+ password_expires_at = current_user&.password_expires_at
+
+ if password_expires_at && password_expires_at < Time.now
return redirect_to new_profile_password_path
end
end
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index 737656b3dcc..f8049b20b9f 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -84,6 +84,7 @@ module Boards
resource.as_json(
only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position],
labels: true,
+ sidebar_endpoints: true,
include: {
project: { only: [:id, :path] },
assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 797a12d4e63..ec12dd7c210 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -505,7 +505,10 @@ module Ci
end
def latest_builds_with_artifacts
- @latest_builds_with_artifacts ||= builds.latest.with_artifacts
+ # We purposely cast the builds to an Array here. Because we always use the
+ # rows if there are more than 0 this prevents us from having to run two
+ # queries: one to get the count and one to get the rows.
+ @latest_builds_with_artifacts ||= builds.latest.with_artifacts.to_a
end
private
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 3b3c7fb7f8b..a0f845cf0ed 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -246,7 +246,12 @@ class Issue < ActiveRecord::Base
def as_json(options = {})
super(options).tap do |json|
- json[:subscribed] = subscribed?(options[:user], project) if options.key?(:user) && options[:user]
+ if options.key?(:sidebar_endpoints) && project
+ url_helper = Gitlab::Routing.url_helpers
+
+ json.merge!(issue_sidebar_endpoint: url_helper.project_issue_path(project, self, format: :json, serializer: 'sidebar'),
+ toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self))
+ end
if options.key?(:labels)
json[:labels] = labels.as_json(
diff --git a/app/models/note.rb b/app/models/note.rb
index f9676361072..50c9caf8529 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -110,6 +110,7 @@ class Note < ActiveRecord::Base
includes(:author, :noteable, :updated_by,
project: [:project_members, { group: [:group_members] }])
end
+ scope :with_metadata, -> { includes(:system_note_metadata) }
after_initialize :ensure_discussion_id
before_validation :nullify_blank_type, :nullify_blank_line_code
@@ -169,7 +170,13 @@ class Note < ActiveRecord::Base
end
def cross_reference?
- system? && matches_cross_reference_regex?
+ return unless system?
+
+ if force_cross_reference_regex_check?
+ matches_cross_reference_regex?
+ else
+ SystemNoteService.cross_reference?(note)
+ end
end
def diff_note?
@@ -382,4 +389,10 @@ class Note < ActiveRecord::Base
def set_discussion_id
self.discussion_id ||= discussion_class.discussion_id(self)
end
+
+ def force_cross_reference_regex_check?
+ return unless system?
+
+ SystemNoteMetadata::TYPES_WITH_CROSS_REFERENCES.include?(system_note_metadata&.action)
+ end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 3a6afc117a6..8c76a3a5dc5 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -217,11 +217,7 @@ class Repository
def branch_exists?(branch_name)
return false unless raw_repository
- @branch_exists_memo ||= Hash.new do |hash, key|
- hash[key] = raw_repository.branch_exists?(key)
- end
-
- @branch_exists_memo[branch_name]
+ branch_names.include?(branch_name)
end
def ref_exists?(ref)
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 1f9f8d7286b..29035480371 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -1,4 +1,14 @@
class SystemNoteMetadata < ActiveRecord::Base
+ # These notes's action text might contain a reference that is external.
+ # We should always force a deep validation upon references that are found
+ # in this note type.
+ # Other notes can always be safely shown as all its references are
+ # in the same project (i.e. with the same permissions)
+ TYPES_WITH_CROSS_REFERENCES = %w[
+ commit cross_reference
+ close duplicate
+ ].freeze
+
ICON_TYPES = %w[
commit description merge confidential visible label assignee cross_reference
title time_tracking branch milestone discussion task moved
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 1da4dbd9e96..cedfcb50e09 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -10,6 +10,8 @@ module MergeRequests
attr_reader :merge_request, :source
+ delegate :merge_jid, :state, to: :@merge_request
+
def execute(merge_request)
if project.merge_requests_ff_only_enabled && !self.is_a?(FfMergeService)
FfMergeService.new(project, current_user, params).execute(merge_request)
@@ -27,6 +29,7 @@ module MergeRequests
success
end
end
+ log_info("Merge process finished on JID #{merge_jid} with state #{state}")
rescue MergeError => e
handle_merge_error(log_message: e.message, save_message_on_model: true)
end
@@ -49,7 +52,9 @@ module MergeRequests
def commit
message = params[:commit_message] || merge_request.merge_commit_message
+ log_info("Git merge started on JID #{merge_jid}")
commit_id = repository.merge(current_user, source, merge_request, message)
+ log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}")
raise MergeError, 'Conflicts detected during merge' unless commit_id
@@ -63,7 +68,9 @@ module MergeRequests
end
def after_merge
+ log_info("Post merge started on JID #{merge_jid} with state #{state}")
MergeRequests::PostMergeService.new(project, current_user).execute(merge_request)
+ log_info("Post merge finished on JID #{merge_jid} with state #{state}")
if delete_source_branch?
DeleteBranchService.new(@merge_request.source_project, branch_deletion_user)
@@ -92,6 +99,11 @@ module MergeRequests
@merge_request.update(merge_error: log_message) if save_message_on_model
end
+ def log_info(message)
+ @logger ||= Rails.logger
+ @logger.info("#{merge_request_info} - #{message}")
+ end
+
def merge_request_info
merge_request.to_reference(full: true)
end
diff --git a/app/services/milestones/promote_service.rb b/app/services/milestones/promote_service.rb
index bd9cfd4e0ea..2187f26d1ed 100644
--- a/app/services/milestones/promote_service.rb
+++ b/app/services/milestones/promote_service.rb
@@ -6,14 +6,14 @@ module Milestones
check_project_milestone!(milestone)
Milestone.transaction do
- # Destroy all milestones with same title across projects
- destroy_old_milestones(milestone)
-
group_milestone = clone_project_milestone(milestone)
move_children_to_group_milestone(group_milestone)
- # Just to be safe
+ # Destroy all milestones with same title across projects
+ destroy_old_milestones(milestone)
+
+ # Rollback if milestone is not valid
unless group_milestone.valid?
raise_error(group_milestone.errors.full_messages.to_sentence)
end
@@ -35,7 +35,7 @@ module Milestones
end
def move_children_to_group_milestone(group_milestone)
- milestone_ids_for_merge(group_milestone).in_groups_of(100) do |milestone_ids|
+ milestone_ids_for_merge(group_milestone).in_groups_of(100, false) do |milestone_ids|
update_children(group_milestone, milestone_ids)
end
end
@@ -49,7 +49,12 @@ module Milestones
create_service = CreateService.new(group, current_user, params)
- create_service.execute
+ milestone = create_service.execute
+
+ # milestone won't be valid here because of duplicated title
+ milestone.save(validate: false)
+
+ milestone
end
def update_children(group_milestone, milestone_ids)
@@ -65,12 +70,12 @@ module Milestones
@group ||= parent.group || raise_error('Project does not belong to a group.')
end
- def destroy_old_milestones(group_milestone)
- Milestone.where(id: milestone_ids_for_merge(group_milestone)).destroy_all
+ def destroy_old_milestones(milestone)
+ Milestone.where(id: milestone_ids_for_merge(milestone)).destroy_all
end
def group_project_ids
- @group_project_ids ||= group.projects.map(&:id)
+ @group_project_ids ||= group.projects.pluck(:id)
end
def raise_error(message)
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index e946218824c..fe71a405565 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -583,6 +583,10 @@ module SystemNoteService
create_note(NoteSummary.new(issuable, issuable.project, author, body, action: action))
end
+ def cross_reference?(note_text)
+ note_text =~ /\A#{cross_reference_note_prefix}/i
+ end
+
private
def notes_for_mentioner(mentioner, noteable, notes)
diff --git a/app/views/layouts/_mailer.html.haml b/app/views/layouts/_mailer.html.haml
index 983ed22a506..b50537438a9 100644
--- a/app/views/layouts/_mailer.html.haml
+++ b/app/views/layouts/_mailer.html.haml
@@ -10,6 +10,10 @@
body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; }
+ .hidden {
+ display: none !important;
+ visibility: hidden !important;
+ }
/* iOS BLUE LINKS */
a[x-apple-data-detectors] {
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index d7859c9fbeb..add394a6356 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -19,14 +19,15 @@
.environments-container
- if @deployments.blank?
- .blank-state.blank-state-no-icon
- %h2.blank-state-title
- You don't have any deployments right now.
- %p.blank-state-text
- Define environments in the deploy stage(s) in
- %code .gitlab-ci.yml
- to track deployments here.
- = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success"
+ .blank-state-row
+ .blank-state-center
+ %h2.blank-state-title
+ You don't have any deployments right now.
+ %p.blank-state-text
+ Define environments in the deploy stage(s) in
+ %code .gitlab-ci.yml
+ to track deployments here.
+ = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success"
- else
.table-holder
.ci-table.environments{ role: 'grid' }
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index fd3b8c01b83..da364b58e36 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -1,6 +1,6 @@
- @no_container = true
- @sort ||= sort_value_recently_updated
-- page_title _('TagsPage|Tags')
+- page_title s_('TagsPage|Tags')
- add_to_breadcrumbs("Repository", project_tree_path(@project))
.flex-list{ class: container_class }
diff --git a/app/views/shared/boards/components/sidebar/_notifications.html.haml b/app/views/shared/boards/components/sidebar/_notifications.html.haml
index 9b989c23cab..333dd1a00b4 100644
--- a/app/views/shared/boards/components/sidebar/_notifications.html.haml
+++ b/app/views/shared/boards/components/sidebar/_notifications.html.haml
@@ -1,7 +1,5 @@
- if current_user
- .block.light.subscription{ ":data-url" => "'#{build_issue_link_base}/' + issue.iid + '/toggle_subscription'" }
- %span.issuable-header-text.hide-collapsed.pull-left
- Notifications
- %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
- %span
- {{issue.subscribed ? 'Unsubscribe' : 'Subscribe'}}
+ .block.subscriptions
+ %subscriptions{ ":loading" => "issue.isFetching && issue.isFetching.subscriptions",
+ ":subscribed" => "issue.subscribed",
+ ":id" => "issue.id" }
diff --git a/changelogs/unreleased/1870-impersonation-stuck-on-password-change.yml b/changelogs/unreleased/1870-impersonation-stuck-on-password-change.yml
new file mode 100644
index 00000000000..b217cb44bf7
--- /dev/null
+++ b/changelogs/unreleased/1870-impersonation-stuck-on-password-change.yml
@@ -0,0 +1,5 @@
+---
+title: Impersonation no longer gets stuck on password change.
+merge_request: 15497
+author:
+type: fixed
diff --git a/changelogs/unreleased/39167-async-boards-sidebar.yml b/changelogs/unreleased/39167-async-boards-sidebar.yml
new file mode 100644
index 00000000000..dc77f1ad451
--- /dev/null
+++ b/changelogs/unreleased/39167-async-boards-sidebar.yml
@@ -0,0 +1,5 @@
+---
+title: Update Issue Boards to fetch the notification subscription status asynchronously
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/39461-notes-api-for-issues-no-longer-returns-label-additions-removals.yml b/changelogs/unreleased/39461-notes-api-for-issues-no-longer-returns-label-additions-removals.yml
new file mode 100644
index 00000000000..36c2f789eeb
--- /dev/null
+++ b/changelogs/unreleased/39461-notes-api-for-issues-no-longer-returns-label-additions-removals.yml
@@ -0,0 +1,5 @@
+---
+title: Label addition/removal are not going to be redacted wrongfully in the API.
+merge_request: 15080
+author:
+type: fixed
diff --git a/changelogs/unreleased/40292-bitbucket-import-hashed-storage.yml b/changelogs/unreleased/40292-bitbucket-import-hashed-storage.yml
new file mode 100644
index 00000000000..e5879f89156
--- /dev/null
+++ b/changelogs/unreleased/40292-bitbucket-import-hashed-storage.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bitbucket wiki import with hashed storage enabled
+merge_request: 15490
+author:
+type: fixed
diff --git a/changelogs/unreleased/40377-blank-states.yml b/changelogs/unreleased/40377-blank-states.yml
new file mode 100644
index 00000000000..7635602c68c
--- /dev/null
+++ b/changelogs/unreleased/40377-blank-states.yml
@@ -0,0 +1,5 @@
+---
+title: Fix blank states using old css
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/bvl-dont-move-projects-using-hashed-storage.yml b/changelogs/unreleased/bvl-dont-move-projects-using-hashed-storage.yml
new file mode 100644
index 00000000000..e0895cb5d48
--- /dev/null
+++ b/changelogs/unreleased/bvl-dont-move-projects-using-hashed-storage.yml
@@ -0,0 +1,5 @@
+---
+title: Don't move repositories and attachments for projects using hashed storage
+merge_request: 15479
+author:
+type: other
diff --git a/changelogs/unreleased/fix-ci-pipelines-index.yml b/changelogs/unreleased/fix-ci-pipelines-index.yml
new file mode 100644
index 00000000000..772093fbef0
--- /dev/null
+++ b/changelogs/unreleased/fix-ci-pipelines-index.yml
@@ -0,0 +1,5 @@
+---
+title: Update composite pipelines index to include "id"
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/issue_40337.yml b/changelogs/unreleased/issue_40337.yml
new file mode 100644
index 00000000000..0cd6c5f46a9
--- /dev/null
+++ b/changelogs/unreleased/issue_40337.yml
@@ -0,0 +1,5 @@
+---
+title: Fix promoting milestone updating all issuables without milestone
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/osw-merge-process-logs.yml b/changelogs/unreleased/osw-merge-process-logs.yml
new file mode 100644
index 00000000000..d2bb0e09834
--- /dev/null
+++ b/changelogs/unreleased/osw-merge-process-logs.yml
@@ -0,0 +1,5 @@
+---
+title: Add logs for monitoring the merge process
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/reduce-queries-for-artifacts-button.yml b/changelogs/unreleased/reduce-queries-for-artifacts-button.yml
new file mode 100644
index 00000000000..f2d469b5a80
--- /dev/null
+++ b/changelogs/unreleased/reduce-queries-for-artifacts-button.yml
@@ -0,0 +1,5 @@
+---
+title: Use arrays in Pipeline#latest_builds_with_artifacts
+merge_request:
+author:
+type: performance
diff --git a/db/migrate/20171121144800_ci_pipelines_index_on_project_id_ref_status_id.rb b/db/migrate/20171121144800_ci_pipelines_index_on_project_id_ref_status_id.rb
new file mode 100644
index 00000000000..5a8ae6e4b57
--- /dev/null
+++ b/db/migrate/20171121144800_ci_pipelines_index_on_project_id_ref_status_id.rb
@@ -0,0 +1,35 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CiPipelinesIndexOnProjectIdRefStatusId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ TABLE = :ci_pipelines
+ OLD_COLUMNS = %i[project_id ref status].freeze
+ NEW_COLUMNS = %i[project_id ref status id].freeze
+
+ def up
+ unless index_exists?(TABLE, NEW_COLUMNS)
+ add_concurrent_index(TABLE, NEW_COLUMNS)
+ end
+
+ if index_exists?(TABLE, OLD_COLUMNS)
+ remove_concurrent_index(TABLE, OLD_COLUMNS)
+ end
+ end
+
+ def down
+ unless index_exists?(TABLE, OLD_COLUMNS)
+ add_concurrent_index(TABLE, OLD_COLUMNS)
+ end
+
+ if index_exists?(TABLE, NEW_COLUMNS)
+ remove_concurrent_index(TABLE, NEW_COLUMNS)
+ end
+ end
+end
diff --git a/db/post_migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb b/db/post_migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb
index 4758c694563..28cd0f70cc2 100644
--- a/db/post_migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb
+++ b/db/post_migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb
@@ -74,7 +74,6 @@ class MigrateGcpClustersToNewClustersArchitectures < ActiveRecord::Migration
encrypted_access_token_iv: gcp_cluster.encrypted_gcp_token_iv
},
platform_kubernetes_attributes: {
- cluster_id: gcp_cluster.id,
api_url: api_url(gcp_cluster.endpoint),
ca_cert: gcp_cluster.ca_cert,
namespace: gcp_cluster.project_namespace,
diff --git a/db/schema.rb b/db/schema.rb
index c60cb729b75..66aa505bb04 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20171106101200) do
+ActiveRecord::Schema.define(version: 20171121144800) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -373,7 +373,7 @@ ActiveRecord::Schema.define(version: 20171106101200) do
add_index "ci_pipelines", ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id", using: :btree
add_index "ci_pipelines", ["pipeline_schedule_id"], name: "index_ci_pipelines_on_pipeline_schedule_id", using: :btree
- add_index "ci_pipelines", ["project_id", "ref", "status"], name: "index_ci_pipelines_on_project_id_and_ref_and_status", using: :btree
+ add_index "ci_pipelines", ["project_id", "ref", "status", "id"], name: "index_ci_pipelines_on_project_id_and_ref_and_status_and_id", using: :btree
add_index "ci_pipelines", ["project_id", "sha"], name: "index_ci_pipelines_on_project_id_and_sha", using: :btree
add_index "ci_pipelines", ["project_id"], name: "index_ci_pipelines_on_project_id", using: :btree
add_index "ci_pipelines", ["status"], name: "index_ci_pipelines_on_status", using: :btree
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 0b9ab4eeb05..ceaaeca4046 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -33,7 +33,7 @@ module API
# paginate() only works with a relation. This could lead to a
# mismatch between the pagination headers info and the actual notes
# array returned, but this is really a edge-case.
- paginate(noteable.notes)
+ paginate(noteable.notes.with_metadata)
.reject { |n| n.cross_reference_not_visible_for?(current_user) }
present notes, with: Entities::Note
else
@@ -50,7 +50,7 @@ module API
end
get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
noteable = find_project_noteable(noteables_str, params[:noteable_id])
- note = noteable.notes.find(params[:note_id])
+ note = noteable.notes.with_metadata.find(params[:note_id])
can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user)
if can_read_note
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
index 033ecd15749..d48ae17aeaf 100644
--- a/lib/gitlab/bitbucket_import/importer.rb
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -61,9 +61,9 @@ module Gitlab
def import_wiki
return if project.wiki.repository_exists?
- path_with_namespace = "#{project.full_path}.wiki"
+ disk_path = project.wiki.disk_path
import_url = project.import_url.sub(/\.git\z/, ".git/wiki")
- gitlab_shell.import_repository(project.repository_storage_path, path_with_namespace, import_url)
+ gitlab_shell.import_repository(project.repository_storage_path, disk_path, import_url)
rescue StandardError => e
errors << { type: :wiki, errors: e.message }
end
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb
index 5481024db8e..7e492938eac 100644
--- a/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb
@@ -68,6 +68,11 @@ module Gitlab
has_one :route, as: :source
self.table_name = 'projects'
+ HASHED_STORAGE_FEATURES = {
+ repository: 1,
+ attachments: 2
+ }.freeze
+
def repository_storage_path
Gitlab.config.repositories.storages[repository_storage]['path']
end
@@ -76,6 +81,13 @@ module Gitlab
def self.name
'Project'
end
+
+ def hashed_storage?(feature)
+ raise ArgumentError, "Invalid feature" unless HASHED_STORAGE_FEATURES.include?(feature)
+ return false unless respond_to?(:storage_version)
+
+ self.storage_version && self.storage_version >= HASHED_STORAGE_FEATURES[feature]
+ end
end
end
end
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb
index 75a75f61953..d32616862f0 100644
--- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb
@@ -22,9 +22,11 @@ module Gitlab
end
def move_project_folders(project, old_full_path, new_full_path)
- move_repository(project, old_full_path, new_full_path)
- move_repository(project, "#{old_full_path}.wiki", "#{new_full_path}.wiki")
- move_uploads(old_full_path, new_full_path)
+ unless project.hashed_storage?(:repository)
+ move_repository(project, old_full_path, new_full_path)
+ move_repository(project, "#{old_full_path}.wiki", "#{new_full_path}.wiki")
+ end
+ move_uploads(old_full_path, new_full_path) unless project.hashed_storage?(:attachments)
move_pages(old_full_path, new_full_path)
end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index b73ca0c2346..768c7e99c96 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -6,6 +6,10 @@ describe ApplicationController do
describe '#check_password_expiration' do
let(:controller) { described_class.new }
+ before do
+ allow(controller).to receive(:session).and_return({})
+ end
+
it 'redirects if the user is over their password expiry' do
user.password_expires_at = Time.new(2002)
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index b47f9055d29..a69b428d117 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -167,19 +167,36 @@ describe "Admin::Users" do
it 'sees impersonation log out icon' do
icon = first('.fa.fa-user-secret')
- expect(icon).not_to eql nil
+ expect(icon).not_to be nil
end
it 'logs out of impersonated user back to original user' do
find(:css, 'li.impersonation a').click
- expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(current_user.username)
+ expect(page.find(:css, '.header-user .profile-link')['data-user']).to eq(current_user.username)
end
it 'is redirected back to the impersonated users page in the admin after stopping' do
find(:css, 'li.impersonation a').click
- expect(current_path).to eql "/admin/users/#{another_user.username}"
+ expect(current_path).to eq("/admin/users/#{another_user.username}")
+ end
+ end
+
+ context 'when impersonating a user with an expired password' do
+ before do
+ another_user.update(password_expires_at: Time.now - 5.minutes)
+ click_link 'Impersonate'
+ end
+
+ it 'does not redirect to password change page' do
+ expect(current_path).to eq('/')
+ end
+
+ it 'is redirected back to the impersonated users page in the admin after stopping' do
+ find(:css, 'li.impersonation a').click
+
+ expect(current_path).to eq("/admin/users/#{another_user.username}")
end
end
end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 9137ab82ff4..205900615c4 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -331,11 +331,29 @@ describe 'Issue Boards', :js do
context 'subscription' do
it 'changes issue subscription' do
click_card(card)
+ wait_for_requests
- page.within('.subscription') do
+ page.within('.subscriptions') do
click_button 'Subscribe'
wait_for_requests
- expect(page).to have_content("Unsubscribe")
+
+ expect(page).to have_content('Unsubscribe')
+ end
+ end
+
+ it 'has "Unsubscribe" button when already subscribed' do
+ create(:subscription, user: user, project: project, subscribable: issue2, subscribed: true)
+ visit project_board_path(project, board)
+ wait_for_requests
+
+ click_card(card)
+ wait_for_requests
+
+ page.within('.subscriptions') do
+ click_button 'Unsubscribe'
+ wait_for_requests
+
+ expect(page).to have_content('Subscribe')
end
end
end
diff --git a/spec/features/issuables/shortcuts_issuable_spec.rb b/spec/features/issuables/shortcuts_issuable_spec.rb
new file mode 100644
index 00000000000..e25fd1a6249
--- /dev/null
+++ b/spec/features/issuables/shortcuts_issuable_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+feature 'Blob shortcuts', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public, :repository) }
+ let(:issue) { create(:issue, project: project, author: user) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:note_text) { 'I got this!' }
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ end
+
+ describe 'pressing "r"' do
+ describe 'On an Issue' do
+ before do
+ create(:note, noteable: issue, project: project, note: note_text)
+ visit project_issue_path(project, issue)
+ wait_for_requests
+ end
+
+ it 'quotes the selected text' do
+ select_element('.note-text')
+ find('body').native.send_key('r')
+
+ expect(find('.js-main-target-form .js-vue-comment-form').value).to include(note_text)
+ end
+ end
+
+ describe 'On a Merge Request' do
+ before do
+ create(:note, noteable: merge_request, project: project, note: note_text)
+ visit project_merge_request_path(project, merge_request)
+ wait_for_requests
+ end
+
+ it 'quotes the selected text' do
+ select_element('.note-text')
+ find('body').native.send_key('r')
+
+ expect(find('.js-main-target-form #note_note').value).to include(note_text)
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index a55ecaa5697..b579e32c9aa 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -13,6 +13,8 @@
"confidential": { "type": "boolean" },
"due_date": { "type": ["date", "null"] },
"relative_position": { "type": "integer" },
+ "issue_sidebar_endpoint": { "type": "string" },
+ "toggle_subscription_endpoint": { "type": "string" },
"project": {
"id": { "type": "integer" },
"path": { "type": "string" }
diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js
index 83b13b06dc1..8f607899b20 100644
--- a/spec/javascripts/boards/board_card_spec.js
+++ b/spec/javascripts/boards/board_card_spec.js
@@ -9,10 +9,11 @@
import Vue from 'vue';
import '~/boards/models/assignee';
+import eventHub from '~/boards/eventhub';
import '~/boards/models/list';
import '~/boards/models/label';
import '~/boards/stores/boards_store';
-import boardCard from '~/boards/components/board_card';
+import boardCard from '~/boards/components/board_card.vue';
import './mock_data';
describe('Board card', () => {
@@ -157,33 +158,35 @@ describe('Board card', () => {
});
it('sets detail issue to card issue on mouse up', () => {
+ spyOn(eventHub, '$emit');
+
triggerEvent('mousedown');
triggerEvent('mouseup');
- expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue);
+ expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', vm.issue);
expect(gl.issueBoards.BoardsStore.detail.list).toEqual(vm.list);
});
it('adds active class if detail issue is set', (done) => {
- triggerEvent('mousedown');
- triggerEvent('mouseup');
-
- setTimeout(() => {
- expect(vm.$el.classList.contains('is-active')).toBe(true);
- done();
- }, 0);
+ vm.detailIssue.issue = vm.issue;
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.$el.classList.contains('is-active')).toBe(true);
+ })
+ .then(done)
+ .catch(done.fail);
});
it('resets detail issue to empty if already set', () => {
- triggerEvent('mousedown');
- triggerEvent('mouseup');
+ spyOn(eventHub, '$emit');
- expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue);
+ gl.issueBoards.BoardsStore.detail.issue = vm.issue;
triggerEvent('mousedown');
triggerEvent('mouseup');
- expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({});
+ expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue');
});
});
});
diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js
index 022d286d5df..ccde657789a 100644
--- a/spec/javascripts/boards/issue_spec.js
+++ b/spec/javascripts/boards/issue_spec.js
@@ -133,6 +133,19 @@ describe('Issue model', () => {
expect(relativePositionIssue.position).toBe(1);
});
+ it('updates data', () => {
+ issue.updateData({ subscribed: true });
+ expect(issue.subscribed).toBe(true);
+ });
+
+ it('sets fetching state', () => {
+ expect(issue.isFetching.subscriptions).toBe(true);
+
+ issue.setFetchingState('subscriptions', false);
+
+ expect(issue.isFetching.subscriptions).toBe(false);
+ });
+
describe('update', () => {
it('passes assignee ids when there are assignees', (done) => {
spyOn(Vue.http, 'patch').and.callFake((url, data) => {
diff --git a/spec/javascripts/vue_shared/components/loading_button_spec.js b/spec/javascripts/vue_shared/components/loading_button_spec.js
index c1eabdede00..49bf8ee6f7c 100644
--- a/spec/javascripts/vue_shared/components/loading_button_spec.js
+++ b/spec/javascripts/vue_shared/components/loading_button_spec.js
@@ -98,7 +98,6 @@ describe('LoadingButton', function () {
it('does not call given callback when disabled because of loading', () => {
vm = mountComponent(LoadingButton, {
loading: true,
- indeterminate: true,
});
spyOn(vm, '$emit');
diff --git a/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js b/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js
new file mode 100644
index 00000000000..818ef0af3c2
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js
@@ -0,0 +1,37 @@
+import Vue from 'vue';
+import toolbar from '~/vue_shared/components/markdown/toolbar.vue';
+import mountComponent from '../../../helpers/vue_mount_component_helper';
+
+describe('toolbar', () => {
+ let vm;
+ const Toolbar = Vue.extend(toolbar);
+ const props = {
+ markdownDocsPath: '',
+ };
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('user can attach file', () => {
+ beforeEach(() => {
+ vm = mountComponent(Toolbar, props);
+ });
+
+ it('should render uploading-container', () => {
+ expect(vm.$el.querySelector('.uploading-container')).not.toBeNull();
+ });
+ });
+
+ describe('user cannot attach file', () => {
+ beforeEach(() => {
+ vm = mountComponent(Toolbar, Object.assign({}, props, {
+ canAttachFile: false,
+ }));
+ });
+
+ it('should not render uploading-container', () => {
+ expect(vm.$el.querySelector('.uploading-container')).toBeNull();
+ });
+ });
+});
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index a66347ead76..a6a1d9e619f 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -54,11 +54,13 @@ describe Gitlab::BitbucketImport::Importer do
create(
:project,
import_source: project_identifier,
+ import_url: "https://bitbucket.org/#{project_identifier}.git",
import_data_attributes: { credentials: data }
)
end
let(:importer) { described_class.new(project) }
+ let(:gitlab_shell) { double }
let(:issues_statuses_sample_data) do
{
@@ -67,6 +69,10 @@ describe Gitlab::BitbucketImport::Importer do
}
end
+ before do
+ allow(importer).to receive(:gitlab_shell) { gitlab_shell }
+ end
+
context 'issues statuses' do
before do
# HACK: Bitbucket::Representation.const_get('Issue') seems to return ::Issue without this
@@ -110,15 +116,36 @@ describe Gitlab::BitbucketImport::Importer do
end
it 'maps statuses to open or closed' do
+ allow(importer).to receive(:import_wiki)
+
importer.execute
expect(project.issues.where(state: "closed").size).to eq(5)
expect(project.issues.where(state: "opened").size).to eq(2)
end
- it 'calls import_wiki' do
- expect(importer).to receive(:import_wiki)
- importer.execute
+ describe 'wiki import' do
+ it 'is skipped when the wiki exists' do
+ expect(project.wiki).to receive(:repository_exists?) { true }
+ expect(importer.gitlab_shell).not_to receive(:import_repository)
+
+ importer.execute
+
+ expect(importer.errors).to be_empty
+ end
+
+ it 'imports to the project disk_path' do
+ expect(project.wiki).to receive(:repository_exists?) { false }
+ expect(importer.gitlab_shell).to receive(:import_repository).with(
+ project.repository_storage_path,
+ project.wiki.disk_path,
+ project.import_url + '/wiki'
+ )
+
+ importer.execute
+
+ expect(importer.errors).to be_empty
+ end
end
end
end
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
index 8922370b0a0..e850b5cd6a4 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
@@ -87,6 +87,14 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr
subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0')
end
+ it 'does not move the repositories when hashed storage is enabled' do
+ project.update!(storage_version: Project::HASHED_STORAGE_FEATURES[:repository])
+
+ expect(subject).not_to receive(:move_repository)
+
+ subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0')
+ end
+
it 'moves uploads' do
expect(subject).to receive(:move_uploads)
.with('known-parent/the-path', 'known-parent/the-path0')
@@ -94,6 +102,14 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr
subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0')
end
+ it 'does not move uploads when hashed storage is enabled for attachments' do
+ project.update!(storage_version: Project::HASHED_STORAGE_FEATURES[:attachments])
+
+ expect(subject).not_to receive(:move_uploads)
+
+ subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0')
+ end
+
it 'moves pages' do
expect(subject).to receive(:move_pages)
.with('known-parent/the-path', 'known-parent/the-path0')
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index b89b0e555d9..3a19a0753e2 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -1502,6 +1502,10 @@ describe Ci::Pipeline, :mailer do
create(:ci_build, :success, :artifacts, pipeline: pipeline)
end
+ it 'returns an Array' do
+ expect(pipeline.latest_builds_with_artifacts).to be_an_instance_of(Array)
+ end
+
it 'returns the latest builds' do
expect(pipeline.latest_builds_with_artifacts).to eq([build])
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 1ecb50586c7..6e7e8c4c570 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -231,6 +231,37 @@ describe Note do
end
end
+ describe '#cross_reference?' do
+ it 'falsey for user-generated notes' do
+ note = create(:note, system: false)
+
+ expect(note.cross_reference?).to be_falsy
+ end
+
+ context 'when the note might contain cross references' do
+ SystemNoteMetadata::TYPES_WITH_CROSS_REFERENCES.each do |type|
+ let(:note) { create(:note, :system) }
+ let!(:metadata) { create(:system_note_metadata, note: note, action: type) }
+
+ it 'delegates to the cross-reference regex' do
+ expect(note).to receive(:matches_cross_reference_regex?).and_return(false)
+
+ note.cross_reference?
+ end
+ end
+ end
+
+ context 'when the note cannot contain cross references' do
+ let(:commit_note) { build(:note, note: 'mentioned in 1312312313 something else.', system: true) }
+ let(:label_note) { build(:note, note: 'added ~2323232323', system: true) }
+
+ it 'scan for a `mentioned in` prefix' do
+ expect(commit_note.cross_reference?).to be_truthy
+ expect(label_note.cross_reference?).to be_falsy
+ end
+ end
+ end
+
describe 'clear_blank_line_code!' do
it 'clears a blank line code before validation' do
note = build(:note, line_code: ' ')
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 8a6aa767ce6..e9e6abb0d5f 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1166,6 +1166,31 @@ describe Repository do
end
end
+ describe '#branch_exists?' do
+ it 'uses branch_names' do
+ allow(repository).to receive(:branch_names).and_return(['foobar'])
+
+ expect(repository.branch_exists?('foobar')).to eq(true)
+ expect(repository.branch_exists?('master')).to eq(false)
+ end
+ end
+
+ describe '#branch_names', :use_clean_rails_memory_store_caching do
+ let(:fake_branch_names) { ['foobar'] }
+
+ it 'gets cached across Repository instances' do
+ allow(repository.raw_repository).to receive(:branch_names).once.and_return(fake_branch_names)
+
+ expect(repository.branch_names).to eq(fake_branch_names)
+
+ fresh_repository = Project.find(project.id).repository
+ expect(fresh_repository.object_id).not_to eq(repository.object_id)
+
+ expect(fresh_repository.raw_repository).not_to receive(:branch_names)
+ expect(fresh_repository.branch_names).to eq(fake_branch_names)
+ end
+ end
+
describe '#update_autocrlf_option' do
describe 'when autocrlf is not already set to :input' do
before do
diff --git a/spec/services/milestones/promote_service_spec.rb b/spec/services/milestones/promote_service_spec.rb
index 9f2df6d6d19..a0a2843b676 100644
--- a/spec/services/milestones/promote_service_spec.rb
+++ b/spec/services/milestones/promote_service_spec.rb
@@ -25,6 +25,18 @@ describe Milestones::PromoteService do
expect { service.execute(milestone) }.to raise_error(described_class::PromoteMilestoneError)
end
+
+ it 'does not promote milestone and update issuables if promoted milestone is not valid' do
+ issue = create(:issue, milestone: milestone, project: project)
+ merge_request = create(:merge_request, milestone: milestone, source_project: project)
+ allow_any_instance_of(Milestone).to receive(:valid?).and_return(false)
+
+ expect { service.execute(milestone) }.to raise_error(described_class::PromoteMilestoneError)
+
+ expect(milestone.reload).to be_persisted
+ expect(issue.reload.milestone).to eq(milestone)
+ expect(merge_request.reload.milestone).to eq(milestone)
+ end
end
context 'without duplicated milestone titles across projects' do
@@ -34,6 +46,16 @@ describe Milestones::PromoteService do
expect(promoted_milestone).to be_group_milestone
end
+ it 'does not update issuables without milestone with the new promoted milestone' do
+ issue_without_milestone = create(:issue, project: project, milestone: nil)
+ merge_request_without_milestone = create(:merge_request, milestone: nil, source_project: project)
+
+ service.execute(milestone)
+
+ expect(issue_without_milestone.reload.milestone).to be_nil
+ expect(merge_request_without_milestone.reload.milestone).to be_nil
+ end
+
it 'sets issuables with new promoted milestone' do
issue = create(:issue, milestone: milestone, project: project)
merge_request = create(:merge_request, milestone: milestone, source_project: project)
@@ -59,6 +81,20 @@ describe Milestones::PromoteService do
expect(Milestone.exists?(milestone_2.id)).to be_falsy
end
+ it 'does not update issuables without milestone with the new promoted milestone' do
+ issue_without_milestone_1 = create(:issue, project: project, milestone: nil)
+ issue_without_milestone_2 = create(:issue, project: project_2, milestone: nil)
+ merge_request_without_milestone_1 = create(:merge_request, milestone: nil, source_project: project)
+ merge_request_without_milestone_2 = create(:merge_request, milestone: nil, source_project: project_2)
+
+ service.execute(milestone)
+
+ expect(issue_without_milestone_1.reload.milestone).to be_nil
+ expect(issue_without_milestone_2.reload.milestone).to be_nil
+ expect(merge_request_without_milestone_1.reload.milestone).to be_nil
+ expect(merge_request_without_milestone_2.reload.milestone).to be_nil
+ end
+
it 'sets all issuables with new promoted milestone' do
issue = create(:issue, milestone: milestone, project: project)
issue_2 = create(:issue, milestone: milestone_2, project: project_2)
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 7c8331f6c60..6310ea1b52b 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -50,6 +50,7 @@ RSpec.configure do |config|
config.include SearchHelpers, type: :feature
config.include CookieHelper, :js
config.include InputHelper, :js
+ config.include SelectionHelper, :js
config.include InspectRequests, :js
config.include WaitForRequests, :js
config.include LiveDebugger, :js
diff --git a/spec/support/selection_helper.rb b/spec/support/selection_helper.rb
new file mode 100644
index 00000000000..b4725b137b2
--- /dev/null
+++ b/spec/support/selection_helper.rb
@@ -0,0 +1,6 @@
+module SelectionHelper
+ def select_element(selector)
+ find(selector)
+ execute_script("let range = document.createRange(); let sel = window.getSelection(); range.selectNodeContents(document.querySelector('#{selector}')); sel.addRange(range);")
+ end
+end
diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
index 88261502d7f..f65770f6528 100644
--- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
@@ -218,8 +218,8 @@ production:
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume /tmp/cc:/tmp/cc"
- docker run ${cc_opts} codeclimate/codeclimate init
- docker run ${cc_opts} codeclimate/codeclimate analyze -f json > codeclimate.json
+ docker run ${cc_opts} codeclimate/codeclimate:0.69.0 init
+ docker run ${cc_opts} codeclimate/codeclimate:0.69.0 analyze -f json > codeclimate.json
}
function deploy() {