diff options
161 files changed, 1643 insertions, 779 deletions
diff --git a/.eslintrc b/.eslintrc index 57a08a06527..aba8112c5a9 100644 --- a/.eslintrc +++ b/.eslintrc @@ -14,7 +14,8 @@ "plugins": [ "filenames", "import", - "html" + "html", + "promise" ], "settings": { "html/html-extensions": [".html", ".html.raw", ".vue"], @@ -26,6 +27,7 @@ }, "rules": { "filenames/match-regex": [2, "^[a-z0-9_]+$"], - "no-multiple-empty-lines": ["error", { "max": 1 }] + "no-multiple-empty-lines": ["error", { "max": 1 }], + "promise/catch-or-return": "error" } } diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index f93208944a1..adb45b0606d 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -239,6 +239,9 @@ AwardsHandler if (menu) { menu.dispatchEvent(new CustomEvent('build-emoji-menu-finish')); } + }).catch((err) => { + emojiContentElement.insertAdjacentHTML('beforeend', '<p>We encountered an error while adding the remaining categories</p>'); + throw new Error(`Error occurred in addRemainingEmojiMenuCategories: ${err.message}`); }); }; diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index c9fe23aec75..4568b86f298 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -35,7 +35,7 @@ export default class BlobFileDropzone { this.removeFile(file); }); this.on('sending', function (file, xhr, formData) { - formData.append('target_branch', form.find('input[name="target_branch"]').val()); + formData.append('branch_name', form.find('input[name="branch_name"]').val()); formData.append('create_merge_request', form.find('.js-create-merge-request').val()); formData.append('commit_message', form.find('.js-commit-message').val()); }); diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index b749ef43cd3..b6dee8177d2 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -1,5 +1,6 @@ /* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */ /* global BoardService */ +/* global Flash */ import Vue from 'vue'; import VueResource from 'vue-resource'; @@ -93,7 +94,7 @@ $(() => { Store.addBlankState(); this.loading = false; - }); + }).catch(() => new Flash('An error occurred. Please try again.')); }, methods: { updateTokens() { diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js index adbd82cb687..b13386536bf 100644 --- a/app/assets/javascripts/boards/components/board_list.js +++ b/app/assets/javascripts/boards/components/board_list.js @@ -57,12 +57,15 @@ export default { }, loadNextPage() { const getIssues = this.list.nextPage(); + const loadingDone = () => { + this.list.loadingMore = false; + }; if (getIssues) { this.list.loadingMore = true; - getIssues.then(() => { - this.list.loadingMore = false; - }); + getIssues + .then(loadingDone) + .catch(loadingDone); } }, toggleForm() { diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js index fb0aac3c0e4..fdab317dc23 100644 --- a/app/assets/javascripts/boards/components/modal/index.js +++ b/app/assets/javascripts/boards/components/modal/index.js @@ -51,11 +51,13 @@ gl.issueBoards.IssuesModal = Vue.extend({ showAddIssuesModal() { if (this.showAddIssuesModal && !this.issues.length) { this.loading = true; + const loadingDone = () => { + this.loading = false; + }; this.loadIssues() - .then(() => { - this.loading = false; - }); + .then(loadingDone) + .catch(loadingDone); } else if (!this.showAddIssuesModal) { this.issues = []; this.selectedIssues = []; @@ -67,11 +69,13 @@ gl.issueBoards.IssuesModal = Vue.extend({ if (this.$el.tagName) { this.page = 1; this.filterLoading = true; + const loadingDone = () => { + this.filterLoading = false; + }; this.loadIssues(true) - .then(() => { - this.filterLoading = false; - }); + .then(loadingDone) + .catch(loadingDone); } }, deep: true, diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index 22f20305624..7e3bb79af1d 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -1,4 +1,5 @@ -/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var */ +/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var, + promise/catch-or-return */ window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 66384d9c038..ccb00099215 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -36,6 +36,9 @@ gl.issueBoards.BoardsStore = { .save() .then(() => { this.state.lists = _.sortBy(this.state.lists, 'position'); + }) + .catch(() => { + // https://gitlab.com/gitlab-org/gitlab-ce/issues/30821 }); this.removeBlankState(); }, diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js index 8fafd13c6c2..92f6fd654b3 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js +++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js @@ -64,6 +64,8 @@ const ResolveBtn = Vue.extend({ }); }, resolve: function () { + const errorFlashMsg = 'An error occurred when trying to resolve a comment. Please try again.'; + if (!this.canResolve) return; let promise; @@ -87,10 +89,12 @@ const ResolveBtn = Vue.extend({ CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by); this.discussion.updateHeadline(data); } else { - new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert'); + new Flash(errorFlashMsg); } this.updateTooltip(); + }).catch(() => { + new Flash(errorFlashMsg); }); } }, diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js index e1e2e3e93f9..4ea6ba8a73d 100644 --- a/app/assets/javascripts/diff_notes/services/resolve.js +++ b/app/assets/javascripts/diff_notes/services/resolve.js @@ -51,8 +51,10 @@ class ResolveServiceClass { discussion.updateHeadline(data); } else { - new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert'); + throw new Error('An error occurred when trying to resolve discussion.'); } + }).catch(() => { + new Flash('An error occurred when trying to resolve a discussion. Please try again.'); }); } diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 0faf757eaab..02a7df9b2a0 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -150,13 +150,13 @@ const ShortcutsBlob = require('./shortcuts_blob'); case 'projects:milestones:new': case 'projects:milestones:edit': case 'projects:milestones:update': + case 'groups:milestones:new': + case 'groups:milestones:edit': + case 'groups:milestones:update': new ZenMode(); new gl.DueDateSelectors(); new gl.GLForm($('.milestone-form')); break; - case 'groups:milestones:new': - new ZenMode(); - break; case 'projects:compare:show': new gl.Diff(); break; diff --git a/app/assets/javascripts/droplab/constants.js b/app/assets/javascripts/droplab/constants.js index a23d914772a..8883ed9aa14 100644 --- a/app/assets/javascripts/droplab/constants.js +++ b/app/assets/javascripts/droplab/constants.js @@ -2,10 +2,12 @@ const DATA_TRIGGER = 'data-dropdown-trigger'; const DATA_DROPDOWN = 'data-dropdown'; const SELECTED_CLASS = 'droplab-item-selected'; const ACTIVE_CLASS = 'droplab-item-active'; +const IGNORE_CLASS = 'droplab-item-ignore'; export { DATA_TRIGGER, DATA_DROPDOWN, SELECTED_CLASS, ACTIVE_CLASS, + IGNORE_CLASS, }; diff --git a/app/assets/javascripts/droplab/drop_down.js b/app/assets/javascripts/droplab/drop_down.js index 9588921ebcd..1fb4d63923c 100644 --- a/app/assets/javascripts/droplab/drop_down.js +++ b/app/assets/javascripts/droplab/drop_down.js @@ -1,7 +1,7 @@ /* eslint-disable */ import utils from './utils'; -import { SELECTED_CLASS } from './constants'; +import { SELECTED_CLASS, IGNORE_CLASS } from './constants'; var DropDown = function(list) { this.currentIndex = 0; @@ -36,6 +36,7 @@ Object.assign(DropDown.prototype, { clickEvent: function(e) { if (e.target.tagName === 'UL') return; + if (e.target.classList.contains(IGNORE_CLASS)) return; var selected = utils.closest(e.target, 'LI'); if (!selected) return; diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index c5fbbdaf465..b70d242269d 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -38,6 +38,9 @@ window.DropzoneInput = (function() { "opacity": 0, "display": "none" }); + + if (!project_uploads_path) return; + dropzone = form_dropzone.dropzone({ url: project_uploads_path, dictDefaultMessage: "", diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index db10b383913..dd95b6530f6 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -115,11 +115,13 @@ class DueDateSelect { this.$dropdown.trigger('loading.gl.dropdown'); this.$selectbox.hide(); this.$value.css('display', ''); + const fadeOutLoader = () => { + this.$loading.fadeOut(); + }; gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update')) - .then(() => { - this.$loading.fadeOut(); - }); + .then(fadeOutLoader) + .catch(fadeOutLoader); } submitSelectedDate(isDropdown) { diff --git a/app/assets/javascripts/environments/components/environment_external_url.js b/app/assets/javascripts/environments/components/environment_external_url.js deleted file mode 100644 index d79b916c360..00000000000 --- a/app/assets/javascripts/environments/components/environment_external_url.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Renders the external url link in environments table. - */ -export default { - props: { - externalUrl: { - type: String, - default: '', - }, - }, - - computed: { - title() { - return 'Open'; - }, - }, - - template: ` - <a - class="btn external-url has-tooltip" - data-container="body" - :href="externalUrl" - target="_blank" - rel="noopener noreferrer nofollow" - :title="title" - :aria-label="title"> - <i class="fa fa-external-link" aria-hidden="true"></i> - </a> - `, -}; diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue new file mode 100644 index 00000000000..eaeec2bc53c --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_external_url.vue @@ -0,0 +1,33 @@ +<script> +/** + * Renders the external url link in environments table. + */ +export default { + props: { + externalUrl: { + type: String, + required: true, + }, + }, + + computed: { + title() { + return 'Open'; + }, + }, +}; +</script> +<template> + <a + class="btn external-url has-tooltip" + data-container="body" + target="_blank" + rel="noopener noreferrer nofollow" + :title="title" + :aria-label="title" + :href="externalUrl"> + <i + class="fa fa-external-link" + aria-hidden="true" /> + </a> +</template> diff --git a/app/assets/javascripts/environments/components/environment_item.js b/app/assets/javascripts/environments/components/environment_item.js index d9b49287dec..0b174cf97da 100644 --- a/app/assets/javascripts/environments/components/environment_item.js +++ b/app/assets/javascripts/environments/components/environment_item.js @@ -1,11 +1,11 @@ import Timeago from 'timeago.js'; import '../../lib/utils/text_utility'; import ActionsComponent from './environment_actions'; -import ExternalUrlComponent from './environment_external_url'; -import StopComponent from './environment_stop'; -import RollbackComponent from './environment_rollback'; -import TerminalButtonComponent from './environment_terminal_button'; -import MonitoringButtonComponent from './environment_monitoring'; +import ExternalUrlComponent from './environment_external_url.vue'; +import StopComponent from './environment_stop.vue'; +import RollbackComponent from './environment_rollback.vue'; +import TerminalButtonComponent from './environment_terminal_button.vue'; +import MonitoringButtonComponent from './environment_monitoring.vue'; import CommitComponent from '../../vue_shared/components/commit'; import eventHub from '../event_hub'; diff --git a/app/assets/javascripts/environments/components/environment_monitoring.js b/app/assets/javascripts/environments/components/environment_monitoring.js deleted file mode 100644 index 064e2fc7434..00000000000 --- a/app/assets/javascripts/environments/components/environment_monitoring.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Renders the Monitoring (Metrics) link in environments table. - */ -export default { - props: { - monitoringUrl: { - type: String, - default: '', - required: true, - }, - }, - - computed: { - title() { - return 'Monitoring'; - }, - }, - - template: ` - <a - class="btn monitoring-url has-tooltip" - data-container="body" - :href="monitoringUrl" - target="_blank" - rel="noopener noreferrer nofollow" - :title="title" - :aria-label="title"> - <i class="fa fa-area-chart" aria-hidden="true"></i> - </a> - `, -}; diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue new file mode 100644 index 00000000000..4b030a27900 --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_monitoring.vue @@ -0,0 +1,33 @@ +<script> +/** + * Renders the Monitoring (Metrics) link in environments table. + */ +export default { + props: { + monitoringUrl: { + type: String, + required: true, + }, + }, + + computed: { + title() { + return 'Monitoring'; + }, + }, +}; +</script> +<template> + <a + class="btn monitoring-url has-tooltip" + data-container="body" + target="_blank" + rel="noopener noreferrer nofollow" + :href="monitoringUrl" + :title="title" + :aria-label="title"> + <i + class="fa fa-area-chart" + aria-hidden="true" /> + </a> +</template> diff --git a/app/assets/javascripts/environments/components/environment_rollback.js b/app/assets/javascripts/environments/components/environment_rollback.vue index 7cbfb651525..f139f24036f 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.js +++ b/app/assets/javascripts/environments/components/environment_rollback.vue @@ -1,3 +1,4 @@ +<script> /* global Flash */ /* eslint-disable no-new */ /** @@ -49,21 +50,25 @@ export default { }); }, }, +}; +</script> +<template> + <button + type="button" + class="btn" + @click="onClick" + :disabled="isLoading"> - template: ` - <button type="button" - class="btn" - @click="onClick" - :disabled="isLoading"> - - <span v-if="isLastDeployment"> - Re-deploy - </span> - <span v-else> - Rollback - </span> + <span v-if="isLastDeployment"> + Re-deploy + </span> + <span v-else> + Rollback + </span> - <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i> - </button> - `, -}; + <i + v-if="isLoading" + class="fa fa-spinner fa-spin" + aria-hidden="true" /> + </button> +</template> diff --git a/app/assets/javascripts/environments/components/environment_stop.js b/app/assets/javascripts/environments/components/environment_stop.vue index 9e5465c1785..11e9aff7b92 100644 --- a/app/assets/javascripts/environments/components/environment_stop.js +++ b/app/assets/javascripts/environments/components/environment_stop.vue @@ -1,3 +1,4 @@ +<script> /* global Flash */ /* eslint-disable no-new, no-alert */ /** @@ -50,17 +51,23 @@ export default { } }, }, - - template: ` - <button type="button" - class="btn stop-env-link has-tooltip" - data-container="body" - @click="onClick" - :disabled="isLoading" - :title="title" - :aria-label="title"> - <i class="fa fa-stop stop-env-icon" aria-hidden="true"></i> - <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i> - </button> - `, }; +</script> +<template> + <button + type="button" + class="btn stop-env-link has-tooltip" + data-container="body" + @click="onClick" + :disabled="isLoading" + :title="title" + :aria-label="title"> + <i + class="fa fa-stop stop-env-icon" + aria-hidden="true" /> + <i + v-if="isLoading" + class="fa fa-spinner fa-spin" + aria-hidden="true" /> + </button> +</template> diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js b/app/assets/javascripts/environments/components/environment_terminal_button.vue index 092a50a0d6f..c8c1f17d4d8 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.js +++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue @@ -1,3 +1,4 @@ +<script> /** * Renders a terminal button to open a web terminal. * Used in environments table. @@ -24,14 +25,15 @@ export default { return 'Terminal'; }, }, - - template: ` - <a class="btn terminal-button has-tooltip" - data-container="body" - :title="title" - :aria-label="title" - :href="terminalPath"> - ${terminalIconSvg} - </a> - `, }; +</script> +<template> + <a + class="btn terminal-button has-tooltip" + data-container="body" + :title="title" + :aria-label="title" + :href="terminalPath" + v-html="terminalIconSvg"> + </a> +</template> diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index a5eb33dd9de..68a832102a0 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -343,6 +343,8 @@ class FilteredSearchManager { const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery); this.recentSearchesService.save(resultantSearches); } + }).catch(() => { + // https://gitlab.com/gitlab-org/gitlab-ce/issues/30821 }); } diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index 10363c16bae..acfa4bd4c6b 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -1,4 +1,8 @@ -/* 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, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, + camelcase, one-var-declaration-per-line, quotes, object-shorthand, + prefer-arrow-callback, comma-dangle, consistent-return, yoda, + prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, + promise/catch-or-return */ /* global Api */ var slice = [].slice; diff --git a/app/assets/javascripts/issue_show/issue_title.vue b/app/assets/javascripts/issue_show/issue_title.vue index ba54178a310..00b0e56030a 100644 --- a/app/assets/javascripts/issue_show/issue_title.vue +++ b/app/assets/javascripts/issue_show/issue_title.vue @@ -34,17 +34,6 @@ export default { }; }, methods: { - fetch() { - this.poll.makeRequest(); - - Visibility.change(() => { - if (!Visibility.hidden()) { - this.poll.restart(); - } else { - this.poll.stop(); - } - }); - }, renderResponse(res) { const body = JSON.parse(res.body); this.triggerAnimation(body); @@ -71,7 +60,17 @@ export default { }, }, created() { - this.fetch(); + if (!Visibility.hidden()) { + this.poll.makeRequest(); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + this.poll.restart(); + } else { + this.poll.stop(); + } + }); }, }; </script> diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 443fb3e0ca9..9a60f5464df 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -332,6 +332,9 @@ vue: $dropdown.hasClass('js-issue-board-sidebar'), clicked: function(label, $el, e, isMarking) { var isIssueIndex, isMRIndex, page, boardsModel; + var fadeOutLoader = () => { + $loading.fadeOut(); + }; page = $('body').data('page'); isIssueIndex = page === 'projects:issues:index'; @@ -396,9 +399,8 @@ $loading.fadeIn(); gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update')) - .then(function () { - $loading.fadeOut(); - }); + .then(fadeOutLoader) + .catch(fadeOutLoader); } else { if ($dropdown.hasClass('js-multiselect')) { diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 36616d02bf6..be3c2c9fbb1 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -211,6 +211,14 @@ $(function () { } }); + if (bootstrapBreakpoint === 'xs') { + const $rightSidebar = $('aside.right-sidebar, .page-with-sidebar'); + + $rightSidebar + .removeClass('right-sidebar-expanded') + .addClass('right-sidebar-collapsed'); + } + // prevent default action for disabled buttons $('.btn').click(function(e) { if ($(this).hasClass('disabled')) { diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js index b0254b17dd2..42ecf0d6cb2 100644 --- a/app/assets/javascripts/merge_request_widget.js +++ b/app/assets/javascripts/merge_request_widget.js @@ -157,7 +157,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; $('.ci-widget-fetching').show(); return $.getJSON(this.opts.ci_status_url, (function(_this) { return function(data) { - var message, status, title; + var message, status, title, callback; _this.status = data.status; _this.hasCi = data.has_ci; _this.updateMergeButton(_this.status, _this.hasCi); @@ -179,6 +179,12 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; _this.opts.ci_sha = data.sha; _this.updateCommitUrls(data.sha); } + if (data.status === "success" || data.status === "failed") { + callback = function() { + return _this.getMergeStatus(); + }; + return setTimeout(callback, 2000); + } if (showNotification && data.status) { status = _this.ciLabelForStatus(data.status); if (status === "preparing") { diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 773fe3233a7..bebd0aa357e 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -164,6 +164,9 @@ .then(function () { $dropdown.trigger('loaded.gl.dropdown'); $loading.fadeOut(); + }) + .catch(() => { + $loading.fadeOut(); }); } else { selected = $selectbox.find('input[type="hidden"]').val(); diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js index d82a4eb9642..aff507abb91 100644 --- a/app/assets/javascripts/monitoring/prometheus_graph.js +++ b/app/assets/javascripts/monitoring/prometheus_graph.js @@ -71,6 +71,8 @@ class PrometheusGraph { this.transformData(metricsResponse); this.createGraph(); } + }).catch(() => { + new Flash('An error occurred when trying to load metrics. Please try again.'); }); } diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 15f7a813626..974fb0d83da 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -308,8 +308,10 @@ require('./task_list'); if (this.isNewNote(note)) { this.note_ids.push(note.id); - $notesList = $('ul.main-notes-list'); - $notesList.append(note.html).syntaxHighlight(); + + $notesList = window.$('ul.main-notes-list'); + Notes.animateAppendNote(note.html, $notesList); + // Update datetime format on the recent note gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false); this.collapseLongCommitList(); @@ -348,7 +350,7 @@ require('./task_list'); lineType = this.isParallelView() ? form.find('#line_type').val() : 'old'; diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line'); // is this the first note of discussion? - discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']"); + discussionContainer = window.$(`.notes[data-discussion-id="${note.discussion_id}"]`); if (!discussionContainer.length) { discussionContainer = form.closest('.discussion').find('.notes'); } @@ -370,14 +372,13 @@ require('./task_list'); row.find(contentContainerClass + ' .content').append($notes.closest('.content').children()); } } - // Init discussion on 'Discussion' page if it is merge request page - if ($('body').attr('data-page').indexOf('projects:merge_request') === 0 || !note.diff_discussion_html) { - $('ul.main-notes-list').append($(note.discussion_html).renderGFM()); + if (window.$('body').attr('data-page').indexOf('projects:merge_request') === 0 || !note.diff_discussion_html) { + Notes.animateAppendNote(note.discussion_html, window.$('ul.main-notes-list')); } } else { // append new note to all matching discussions - discussionContainer.append($(note.html).renderGFM()); + Notes.animateAppendNote(note.html, discussionContainer); } if (typeof gl.diffNotesCompileComponents !== 'undefined' && note.discussion_resolvable) { @@ -1063,6 +1064,13 @@ require('./task_list'); return $form; }; + Notes.animateAppendNote = function(noteHTML, $notesList) { + const $note = window.$(noteHTML); + + $note.addClass('fade-in').renderGFM(); + $notesList.append($note); + }; + return Notes; })(); }).call(window); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 3325a7d429c..30902767705 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -56,6 +56,9 @@ gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update')) .then(function () { $loading.fadeOut(); + }) + .catch(function () { + $loading.fadeOut(); }); }; diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index 90935b9616b..7c50b80fd2b 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -145,3 +145,17 @@ a { .dropdown-menu-nav a { transition: none; } + +@keyframes fadeIn { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +.fade-in { + animation: fadeIn $fade-in-duration 1; +} diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 2c33b235980..0fd7203e72b 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -40,6 +40,10 @@ line-height: 24px; } +.bold { + font-weight: 600; +} + .tab-content { overflow: visible; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 7767826b033..b87e712c763 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -564,3 +564,7 @@ color: $gl-text-color-secondary; } } + +.droplab-item-ignore { + pointer-events: none; +} diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 2071d4dcfce..0077ea41d3b 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -331,6 +331,14 @@ header { .dropdown-menu-nav { min-width: 140px; margin-top: -5px; + + .current-user { + padding: 5px 18px; + + .user-name { + display: block; + } + } } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 20ef9a774e4..3ef6ec3f912 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -458,6 +458,11 @@ $label-remove-border: rgba(0, 0, 0, .1); $label-border-radius: 100px; /* +* Animation +*/ +$fade-in-duration: 200ms; + +/* * Lint */ $lint-incorrect-color: $red-500; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 0bca3e93e4c..8d3d1a72b9b 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -210,10 +210,6 @@ } } - .bold { - font-weight: 600; - } - .light { font-weight: normal; } diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 9ac8197e45a..183eb00ef67 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -1,17 +1,29 @@ module CreatesCommit extend ActiveSupport::Concern + def set_start_branch_to_branch_name + branch_exists = @repository.find_branch(@branch_name) + @start_branch = @branch_name if branch_exists + end + def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) - set_commit_variables + if can?(current_user, :push_code, @project) + @project_to_commit_into = @project + @branch_name ||= @ref + else + @project_to_commit_into = current_user.fork_of(@project) + @branch_name ||= @project_to_commit_into.repository.next_branch('patch') + end + + @start_branch ||= @ref || @branch_name commit_params = @commit_params.merge( - start_project: @mr_target_project, - start_branch: @mr_target_branch, - target_branch: @mr_source_branch + start_project: @project, + start_branch: @start_branch, + branch_name: @branch_name ) - result = service.new( - @mr_source_project, current_user, commit_params).execute + result = service.new(@project_to_commit_into, current_user, commit_params).execute if result[:status] == :success update_flash_notice(success_notice) @@ -72,30 +84,30 @@ module CreatesCommit def new_merge_request_path new_namespace_project_merge_request_path( - @mr_source_project.namespace, - @mr_source_project, + @project_to_commit_into.namespace, + @project_to_commit_into, merge_request: { - source_project_id: @mr_source_project.id, - target_project_id: @mr_target_project.id, - source_branch: @mr_source_branch, - target_branch: @mr_target_branch + source_project_id: @project_to_commit_into.id, + target_project_id: @project.id, + source_branch: @branch_name, + target_branch: @start_branch } ) end def existing_merge_request_path - namespace_project_merge_request_path(@mr_target_project.namespace, @mr_target_project, @merge_request) + namespace_project_merge_request_path(@project.namespace, @project, @merge_request) end def merge_request_exists? return @merge_request if defined?(@merge_request) - @merge_request = MergeRequestsFinder.new(current_user, project_id: @mr_target_project.id).execute.opened. - find_by(source_branch: @mr_source_branch, target_branch: @mr_target_branch, source_project_id: @mr_source_project) + @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened. + find_by(source_project_id: @project_to_commit_into, source_branch: @branch_name, target_branch: @start_branch) end def different_project? - @mr_source_project != @mr_target_project + @project_to_commit_into != @project end def create_merge_request? @@ -103,22 +115,6 @@ module CreatesCommit # as the target branch in the same project, # we don't want to create a merge request. params[:create_merge_request].present? && - (different_project? || @mr_target_branch != @mr_source_branch) - end - - def set_commit_variables - if can?(current_user, :push_code, @project) - @mr_source_project = @project - @target_branch ||= @ref - else - @mr_source_project = current_user.fork_of(@project) - @target_branch ||= @mr_source_project.repository.next_branch('patch') - end - - # Merge request to this project - @mr_target_project = @project - @mr_target_branch ||= @ref || @target_branch - - @mr_source_branch = @target_branch + (different_project? || @start_branch != @branch_name) end end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index f1a93ccb3ad..e2f81b09adc 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -89,9 +89,4 @@ class Projects::ApplicationController < ApplicationController def builds_enabled return render_404 unless @project.feature_available?(:builds, current_user) end - - def update_ref - branch_exists = @repository.find_branch(@target_branch) - @ref = @target_branch if branch_exists - end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 73706bf8dae..9fce1db6742 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -25,10 +25,10 @@ class Projects::BlobController < Projects::ApplicationController end def create - update_ref + set_start_branch_to_branch_name create_commit(Files::CreateService, success_notice: "The file has been successfully created.", - success_path: -> { namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) }, + success_path: -> { namespace_project_blob_path(@project.namespace, @project, File.join(@branch_name, @file_path)) }, failure_view: :new, failure_path: namespace_project_new_blob_path(@project.namespace, @project, @ref)) end @@ -69,10 +69,10 @@ class Projects::BlobController < Projects::ApplicationController end def destroy - create_commit(Files::DestroyService, success_notice: "The file has been successfully deleted.", - success_path: -> { namespace_project_tree_path(@project.namespace, @project, @target_branch) }, - failure_view: :show, - failure_path: namespace_project_blob_path(@project.namespace, @project, @id)) + create_commit(Files::DeleteService, success_notice: "The file has been successfully deleted.", + success_path: -> { namespace_project_tree_path(@project.namespace, @project, @branch_name) }, + failure_view: :show, + failure_path: namespace_project_blob_path(@project.namespace, @project, @id)) end def diff @@ -127,16 +127,16 @@ class Projects::BlobController < Projects::ApplicationController def after_edit_path from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:from_merge_request_iid]) - if from_merge_request && @target_branch == @ref + if from_merge_request && @branch_name == @ref diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) + "##{hexdigest(@path)}" else - namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path)) + namespace_project_blob_path(@project.namespace, @project, File.join(@branch_name, @path)) end end def editor_variables - @target_branch = params[:target_branch] + @branch_name = params[:branch_name] @file_path = if action_name.to_s == 'create' diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index d25bbddd1bb..2b5f0383ac1 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -56,9 +56,7 @@ class Projects::CommitController < Projects::ApplicationController return render_404 if @start_branch.blank? - @target_branch = create_new_branch? ? @commit.revert_branch_name : @start_branch - - @mr_target_branch = @start_branch + @branch_name = create_new_branch? ? @commit.revert_branch_name : @start_branch create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully reverted.", success_path: -> { successful_change_path }, failure_path: failed_change_path) @@ -69,9 +67,7 @@ class Projects::CommitController < Projects::ApplicationController return render_404 if @start_branch.blank? - @target_branch = create_new_branch? ? @commit.cherry_pick_branch_name : @start_branch - - @mr_target_branch = @start_branch + @branch_name = create_new_branch? ? @commit.cherry_pick_branch_name : @start_branch create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked.", success_path: -> { successful_change_path }, failure_path: failed_change_path) @@ -84,7 +80,7 @@ class Projects::CommitController < Projects::ApplicationController end def successful_change_path - referenced_merge_request_url || namespace_project_commits_url(@project.namespace, @project, @target_branch) + referenced_merge_request_url || namespace_project_commits_url(@project.namespace, @project, @branch_name) end def failed_change_path diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index 637b61504d8..5e2182c883e 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -34,16 +34,16 @@ class Projects::TreeController < Projects::ApplicationController def create_dir return render_404 unless @commit_params.values.all? - update_ref + set_start_branch_to_branch_name create_commit(Files::CreateDirService, success_notice: "The directory has been successfully created.", - success_path: namespace_project_tree_path(@project.namespace, @project, File.join(@target_branch, @dir_name)), + success_path: namespace_project_tree_path(@project.namespace, @project, File.join(@branch_name, @dir_name)), failure_path: namespace_project_tree_path(@project.namespace, @project, @ref)) end private def assign_dir_vars - @target_branch = params[:target_branch] + @branch_name = params[:branch_name] @dir_name = File.join(@path, params[:dir_name]) @commit_params = { diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index ec57fec4f99..0b13dbf5f8d 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -165,11 +165,8 @@ module IssuablesHelper html.html_safe end - def cached_assigned_issuables_count(assignee, issuable_type, state) - cache_key = hexdigest(['assigned_issuables_count', assignee.id, issuable_type, state].join('-')) - Rails.cache.fetch(cache_key, expires_in: 2.minutes) do - assigned_issuables_count(assignee, issuable_type, state) - end + def assigned_issuables_count(issuable_type) + current_user.public_send("assigned_open_#{issuable_type}_count") end def issuable_filter_params @@ -192,10 +189,6 @@ module IssuablesHelper private - def assigned_issuables_count(assignee, issuable_type, state) - assignee.public_send("assigned_#{issuable_type}").public_send(state).count - end - def sidebar_gutter_collapsed? cookies[:collapsed_gutter] == 'true' end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 43669b6f356..5f97e6114ea 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -272,14 +272,14 @@ module ProjectsHelper end end - def add_special_file_path(project, file_name:, commit_message: nil, target_branch: nil, context: nil) + def add_special_file_path(project, file_name:, commit_message: nil, branch_name: nil, context: nil) namespace_project_new_blob_path( project.namespace, project, project.default_branch || 'master', file_name: file_name, commit_message: commit_message || "Add #{file_name.downcase}", - target_branch: target_branch, + branch_name: branch_name, context: context ) end diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 3a5d1b97c36..2fda98cae90 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -62,6 +62,14 @@ module SortingHelper } end + def branches_sort_options_hash + { + sort_value_name => sort_title_name, + sort_value_recently_updated => sort_title_recently_updated, + sort_value_oldest_updated => sort_title_oldest_updated + } + end + def sort_title_priority 'Priority' end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 4a76c679bad..f1dab60524e 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -35,7 +35,7 @@ module TreeHelper end def on_top_of_branch?(project = @project, ref = @ref) - project.repository.branch_names.include?(ref) + project.repository.branch_exists?(ref) end def can_edit_tree?(project = nil, ref = nil) diff --git a/app/models/identity.rb b/app/models/identity.rb index 3bacc450e6e..920a25932b4 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -7,6 +7,8 @@ class Identity < ActiveRecord::Base validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider } validates :user_id, uniqueness: { scope: :provider } + scope :with_extern_uid, ->(provider, extern_uid) { where(extern_uid: extern_uid, provider: provider) } + def ldap? provider.starts_with?('ldap') end diff --git a/app/models/user.rb b/app/models/user.rb index 2d85bf8df26..774d4caa806 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -99,9 +99,6 @@ class User < ActiveRecord::Base has_many :award_emoji, dependent: :destroy has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id - has_many :assigned_issues, dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue" - has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" - # Issues that a user owns are expected to be moved to the "ghost" user before # the user is destroyed. If the user owns any issues during deletion, this # should be treated as an exceptional condition. @@ -891,20 +888,20 @@ class User < ActiveRecord::Base @global_notification_setting end - def assigned_open_merge_request_count(force: false) - Rails.cache.fetch(['users', id, 'assigned_open_merge_request_count'], force: force) do - assigned_merge_requests.opened.count + def assigned_open_merge_requests_count(force: false) + Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force) do + MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count end end def assigned_open_issues_count(force: false) Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force) do - assigned_issues.opened.count + IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count end end def update_cache_counts - assigned_open_merge_request_count(force: true) + assigned_open_merge_requests_count(force: true) assigned_open_issues_count(force: true) end diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index 1297a792259..a48d6a976f0 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -1,69 +1,27 @@ module Commits - class ChangeService < ::BaseService - ValidationError = Class.new(StandardError) - ChangeError = Class.new(StandardError) + class ChangeService < Commits::CreateService + def initialize(*args) + super - def execute - @start_project = params[:start_project] || @project - @start_branch = params[:start_branch] - @target_branch = params[:target_branch] @commit = params[:commit] - - check_push_permissions - - commit - rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError, - ValidationError, ChangeError => ex - error(ex.message) end private - def commit - raise NotImplementedError - end - def commit_change(action) raise NotImplementedError unless repository.respond_to?(action) - validate_target_branch if different_branch? - repository.public_send( action, current_user, @commit, - @target_branch, + @branch_name, start_project: @start_project, start_branch_name: @start_branch) - - success rescue Repository::CreateTreeError error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically. - A #{action.to_s.dasherize} may have already been performed with this #{@commit.change_type_title(current_user)}, or a more recent commit may have updated some of its content." + This #{@commit.change_type_title(current_user)} may already have been #{action.to_s.dasherize}ed, or a more recent commit may have updated some of its content." raise ChangeError, error_msg end - - def check_push_permissions - allowed = ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(@target_branch) - - unless allowed - raise ValidationError.new('You are not allowed to push into this branch') - end - - true - end - - def validate_target_branch - result = ValidateNewBranchService.new(@project, current_user) - .execute(@target_branch) - - if result[:status] == :error - raise ChangeError, "There was an error creating the source branch: #{result[:message]}" - end - end - - def different_branch? - @start_branch != @target_branch || @start_project != @project - end end end diff --git a/app/services/commits/cherry_pick_service.rb b/app/services/commits/cherry_pick_service.rb index 605cca36f9c..320e229560d 100644 --- a/app/services/commits/cherry_pick_service.rb +++ b/app/services/commits/cherry_pick_service.rb @@ -1,6 +1,6 @@ module Commits class CherryPickService < ChangeService - def commit + def create_commit! commit_change(:cherry_pick) end end diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb new file mode 100644 index 00000000000..c58f04a252b --- /dev/null +++ b/app/services/commits/create_service.rb @@ -0,0 +1,74 @@ +module Commits + class CreateService < ::BaseService + ValidationError = Class.new(StandardError) + ChangeError = Class.new(StandardError) + + def initialize(*args) + super + + @start_project = params[:start_project] || @project + @start_branch = params[:start_branch] + @branch_name = params[:branch_name] + end + + def execute + validate! + + new_commit = create_commit! + + success(result: new_commit) + rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Repository::CommitError, GitHooksService::PreReceiveError => ex + error(ex.message) + end + + private + + def create_commit! + raise NotImplementedError + end + + def raise_error(message) + raise ValidationError, message + end + + def different_branch? + @start_branch != @branch_name || @start_project != @project + end + + def validate! + validate_permissions! + validate_on_branch! + validate_branch_existance! + + validate_new_branch_name! if different_branch? + end + + def validate_permissions! + allowed = ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(@branch_name) + + unless allowed + raise_error("You are not allowed to push into this branch") + end + end + + def validate_on_branch! + if !@start_project.empty_repo? && !@start_project.repository.branch_exists?(@start_branch) + raise_error('You can only create or edit files when you are on a branch') + end + end + + def validate_branch_existance! + if !project.empty_repo? && different_branch? && repository.branch_exists?(@branch_name) + raise_error("A branch called '#{@branch_name}' already exists. Switch to that branch in order to make changes") + end + end + + def validate_new_branch_name! + result = ValidateNewBranchService.new(project, current_user).execute(@branch_name) + + if result[:status] == :error + raise_error("Something went wrong when we tried to create '#{@branch_name}' for you: #{result[:message]}") + end + end + end +end diff --git a/app/services/commits/revert_service.rb b/app/services/commits/revert_service.rb index addd55cb32f..dc27399e047 100644 --- a/app/services/commits/revert_service.rb +++ b/app/services/commits/revert_service.rb @@ -1,6 +1,6 @@ module Commits class RevertService < ChangeService - def commit + def create_commit! commit_change(:revert) end end diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb index 1b5623baebe..3b611588466 100644 --- a/app/services/delete_merged_branches_service.rb +++ b/app/services/delete_merged_branches_service.rb @@ -8,9 +8,20 @@ class DeleteMergedBranchesService < BaseService branches = project.repository.branch_names branches = branches.select { |branch| project.repository.merged_to_root_ref?(branch) } + # Prevent deletion of branches relevant to open merge requests + branches -= merge_request_branch_names branches.each do |branch| DeleteBranchService.new(project, current_user).execute(branch) end end + + private + + def merge_request_branch_names + # reorder(nil) is necessary for SELECT DISTINCT because default scope adds an ORDER BY + source_names = project.origin_merge_requests.opened.reorder(nil).uniq.pluck(:source_branch) + target_names = project.merge_requests.opened.reorder(nil).uniq.pluck(:target_branch) + (source_names + target_names).uniq + end end diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index c8a60422bf4..38231f66009 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -1,79 +1,17 @@ module Files - class BaseService < ::BaseService - ValidationError = Class.new(StandardError) - - def execute - @start_project = params[:start_project] || @project - @start_branch = params[:start_branch] - @target_branch = params[:target_branch] + class BaseService < Commits::CreateService + def initialize(*args) + super + @author_email = params[:author_email] + @author_name = params[:author_name] @commit_message = params[:commit_message] - @file_path = params[:file_path] - @previous_path = params[:previous_path] - @file_content = if params[:file_content_encoding] == 'base64' - Base64.decode64(params[:file_content]) - else - params[:file_content] - end - @last_commit_sha = params[:last_commit_sha] - @author_email = params[:author_email] - @author_name = params[:author_name] - - # Validate parameters - validate - - # Create new branch if it different from start_branch - validate_target_branch if different_branch? - - result = commit - if result - success(result: result) - else - error('Something went wrong. Your changes were not committed') - end - rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError, ValidationError => ex - error(ex.message) - end - - private - - def different_branch? - @start_branch != @target_branch || @start_project != @project - end - - def file_has_changed? - return false unless @last_commit_sha && last_commit - - @last_commit_sha != last_commit.sha - end - - def raise_error(message) - raise ValidationError.new(message) - end - - def validate - allowed = ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(@target_branch) - - unless allowed - raise_error("You are not allowed to push into this branch") - end - - if !@start_project.empty_repo? && !@start_project.repository.branch_exists?(@start_branch) - raise ValidationError, 'You can only create or edit files when you are on a branch' - end - - if !project.empty_repo? && different_branch? && repository.branch_exists?(@branch_name) - raise ValidationError, "A branch called #{@branch_name} already exists. Switch to that branch in order to make changes" - end - end - def validate_target_branch - result = ValidateNewBranchService.new(project, current_user). - execute(@target_branch) + @file_path = params[:file_path] + @previous_path = params[:previous_path] - if result[:status] == :error - raise_error("Something went wrong when we tried to create #{@target_branch} for you: #{result[:message]}") - end + @file_content = params[:file_content] + @file_content = Base64.decode64(@file_content) if params[:file_content_encoding] == 'base64' end end end diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb index 083ffdc634c..8ecac6115bd 100644 --- a/app/services/files/create_dir_service.rb +++ b/app/services/files/create_dir_service.rb @@ -1,26 +1,15 @@ module Files class CreateDirService < Files::BaseService - def commit + def create_commit! repository.create_dir( current_user, @file_path, message: @commit_message, - branch_name: @target_branch, + branch_name: @branch_name, author_email: @author_email, author_name: @author_name, start_project: @start_project, start_branch_name: @start_branch) end - - def validate - super - - unless @file_path =~ Gitlab::Regex.file_path_regex - raise_error( - 'Your changes could not be committed, because the file path ' + - Gitlab::Regex.file_path_regex_message - ) - end - end end end diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index 65b5537fb68..00a8dcf0934 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -1,48 +1,16 @@ module Files class CreateService < Files::BaseService - def commit + def create_commit! repository.create_file( current_user, @file_path, @file_content, message: @commit_message, - branch_name: @target_branch, + branch_name: @branch_name, author_email: @author_email, author_name: @author_name, start_project: @start_project, start_branch_name: @start_branch) end - - def validate - super - - if @file_content.nil? - raise_error("You must provide content.") - end - - if @file_path =~ Gitlab::Regex.directory_traversal_regex - raise_error( - 'Your changes could not be committed, because the file name ' + - Gitlab::Regex.directory_traversal_regex_message - ) - end - - unless @file_path =~ Gitlab::Regex.file_path_regex - raise_error( - 'Your changes could not be committed, because the file name ' + - Gitlab::Regex.file_path_regex_message - ) - end - - unless project.empty_repo? - @file_path.slice!(0) if @file_path.start_with?('/') - - blob = repository.blob_at_branch(@start_branch, @file_path) - - if blob - raise_error('Your changes could not be committed because a file with the same name already exists') - end - end - end end end diff --git a/app/services/files/destroy_service.rb b/app/services/files/delete_service.rb index e294659bc98..7952e5c95d4 100644 --- a/app/services/files/destroy_service.rb +++ b/app/services/files/delete_service.rb @@ -1,11 +1,11 @@ module Files - class DestroyService < Files::BaseService - def commit + class DeleteService < Files::BaseService + def create_commit! repository.delete_file( current_user, @file_path, message: @commit_message, - branch_name: @target_branch, + branch_name: @branch_name, author_email: @author_email, author_name: @author_name, start_project: @start_project, diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb index 700f9f4f6f0..bfacc462847 100644 --- a/app/services/files/multi_service.rb +++ b/app/services/files/multi_service.rb @@ -1,14 +1,10 @@ module Files class MultiService < Files::BaseService - FileChangedError = Class.new(StandardError) - - ACTIONS = %w[create update delete move].freeze - - def commit + def create_commit! repository.multi_action( user: current_user, message: @commit_message, - branch_name: @target_branch, + branch_name: @branch_name, actions: params[:actions], author_email: @author_email, author_name: @author_name, @@ -19,122 +15,17 @@ module Files private - def validate + def validate! super - params[:actions].each_with_index do |action, index| - if ACTIONS.include?(action[:action].to_s) - action[:action] = action[:action].to_sym - else - raise_error("Unknown action type `#{action[:action]}`.") - end - - unless action[:file_path].present? - raise_error("You must specify a file_path.") - end - - action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/') - action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/') - - regex_check(action[:file_path]) - regex_check(action[:previous_path]) if action[:previous_path] - - if project.empty_repo? && action[:action] != :create - raise_error("No files to #{action[:action]}.") - end - - validate_file_exists(action) - - case action[:action] - when :create - validate_create(action) - when :update - validate_update(action) - when :delete - validate_delete(action) - when :move - validate_move(action, index) - end - end - end - - def validate_file_exists(action) - return if action[:action] == :create - - file_path = action[:file_path] - file_path = action[:previous_path] if action[:action] == :move - - blob = repository.blob_at_branch(params[:branch], file_path) - - unless blob - raise_error("File to be #{action[:action]}d `#{file_path}` does not exist.") + params[:actions].each do |action| + validate_action!(action) end end - def last_commit - Gitlab::Git::Commit.last_for_path(repository, @start_branch, @file_path) - end - - def regex_check(file) - if file =~ Gitlab::Regex.directory_traversal_regex - raise_error( - 'Your changes could not be committed, because the file name, `' + - file + - '` ' + - Gitlab::Regex.directory_traversal_regex_message - ) - end - - unless file =~ Gitlab::Regex.file_path_regex - raise_error( - 'Your changes could not be committed, because the file name, `' + - file + - '` ' + - Gitlab::Regex.file_path_regex_message - ) - end - end - - def validate_create(action) - return if project.empty_repo? - - if repository.blob_at_branch(params[:branch], action[:file_path]) - raise_error("Your changes could not be committed because a file with the name `#{action[:file_path]}` already exists.") - end - - if action[:content].nil? - raise_error("You must provide content.") - end - end - - def validate_update(action) - if action[:content].nil? - raise_error("You must provide content.") - end - - if file_has_changed? - raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.") - end - end - - def validate_delete(action) - end - - def validate_move(action, index) - if action[:previous_path].nil? - raise_error("You must supply the original file path when moving file `#{action[:file_path]}`.") - end - - blob = repository.blob_at_branch(params[:branch], action[:file_path]) - - if blob - raise_error("Move destination `#{action[:file_path]}` already exists.") - end - - if action[:content].nil? - blob = repository.blob_at_branch(params[:branch], action[:previous_path]) - blob.load_all_data!(repository) if blob.truncated? - params[:actions][index][:content] = blob.data + def validate_action!(action) + unless Gitlab::Git::Index::ACTIONS.include?(action[:action].to_s) + raise_error("Unknown action '#{action[:action]}'") end end end diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index fbbab97632e..f23a9f6d57c 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -2,10 +2,16 @@ module Files class UpdateService < Files::BaseService FileChangedError = Class.new(StandardError) - def commit + def initialize(*args) + super + + @last_commit_sha = params[:last_commit_sha] + end + + def create_commit! repository.update_file(current_user, @file_path, @file_content, message: @commit_message, - branch_name: @target_branch, + branch_name: @branch_name, previous_path: @previous_path, author_email: @author_email, author_name: @author_name, @@ -15,21 +21,23 @@ module Files private - def validate - super - - if @file_content.nil? - raise_error("You must provide content.") - end + def file_has_changed? + return false unless @last_commit_sha && last_commit - if file_has_changed? - raise FileChangedError.new("You are attempting to update a file that has changed since you started editing it.") - end + @last_commit_sha != last_commit.sha end def last_commit @last_commit ||= Gitlab::Git::Commit. last_for_path(@start_project.repository, @start_branch, @file_path) end + + def validate! + super + + if file_has_changed? + raise FileChangedError, "You are attempting to update a file that has changed since you started editing it." + end + end end end diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb index b7a244c2029..1711be7211c 100644 --- a/app/services/members/authorized_destroy_service.rb +++ b/app/services/members/authorized_destroy_service.rb @@ -9,7 +9,11 @@ module Members def execute return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user) - member.destroy + Member.transaction do + unassign_issues_and_merge_requests(member) + + member.destroy + end if member.request? && member.user != user notification_service.decline_access_request(member) @@ -17,5 +21,23 @@ module Members member end + + private + + def unassign_issues_and_merge_requests(member) + if member.is_a?(GroupMember) + IssuesFinder.new(user, group_id: member.source_id, assignee_id: member.user_id). + execute. + update_all(assignee_id: nil) + MergeRequestsFinder.new(user, group_id: member.source_id, assignee_id: member.user_id). + execute. + update_all(assignee_id: nil) + else + project = member.source + project.issues.opened.assigned_to(member.user).update_all(assignee_id: nil) + project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil) + member.user.update_cache_counts + end + end end end diff --git a/app/services/validate_new_branch_service.rb b/app/services/validate_new_branch_service.rb index 2f61be184ce..d232e85cd33 100644 --- a/app/services/validate_new_branch_service.rb +++ b/app/services/validate_new_branch_service.rb @@ -8,10 +8,7 @@ class ValidateNewBranchService < BaseService return error('Branch name is invalid') end - repository = project.repository - existing_branch = repository.find_branch(branch_name) - - if existing_branch + if project.repository.branch_exists?(branch_name) return error('Branch already exists') end diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index a9893dea68f..659d548df18 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -47,13 +47,13 @@ %li = link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('hashtag fw') - - issues_count = cached_assigned_issuables_count(current_user, :issues, :opened) + - issues_count = assigned_issuables_count(:issues) %span.badge.issues-count{ class: ('hidden' if issues_count.zero?) } = number_with_delimiter(issues_count) %li = link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = custom_icon('mr_bold') - - merge_requests_count = cached_assigned_issuables_count(current_user, :merge_requests, :opened) + - merge_requests_count = assigned_issuables_count(:merge_requests) %span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) } = number_with_delimiter(merge_requests_count) %li @@ -67,6 +67,11 @@ = icon('caret-down') .dropdown-menu-nav.dropdown-menu-align-right %ul + %li.current-user + .user-name.bold + = current_user.name + @#{current_user.username} + %li.divider %li = link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username } %li diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 444ecc414c0..ac222ad8c82 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -44,7 +44,7 @@ I %span Issues - .badge= number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened)) + .badge= number_with_delimiter(assigned_issuables_count(:issues)) = nav_link(path: 'dashboard#merge_requests') do = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do .shortcut-mappings @@ -53,7 +53,7 @@ M %span Merge Requests - .badge= number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened)) + .badge= number_with_delimiter(assigned_issuables_count(:merge_requests)) = nav_link(controller: 'dashboard/snippets') do = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do .shortcut-mappings diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 4b26f944733..4af62461151 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -9,7 +9,7 @@ - if @conflict .alert.alert-danger Someone edited the file the same time you did. Please check out - = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank", rel: 'noopener noreferrer' + = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@branch_name, @file_path)), target: "_blank", rel: 'noopener noreferrer' and make sure your changes will not unintentionally remove theirs. .editor-title-row %h3.page-title.blob-edit-page-title diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index bd1f2d96f56..91b86280e4c 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -15,16 +15,14 @@ .dropdown.inline> %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } %span.light - = projects_sort_options_hash[@sort] + = branches_sort_options_hash[@sort] = icon('chevron-down') - %ul.dropdown-menu.dropdown-menu-align-right - %li - = link_to filter_branches_path(sort: sort_value_name) do - = sort_title_name - = link_to filter_branches_path(sort: sort_value_recently_updated) do - = sort_title_recently_updated - = link_to filter_branches_path(sort: sort_value_oldest_updated) do - = sort_title_oldest_updated + %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable + %li.dropdown-header + Sort by + - branches_sort_options_hash.each do |value, title| + %li + = link_to title, filter_branches_path(sort: value), class: ("is-active" if @sort == value) - if can? current_user, :push_code, @project = link_to namespace_project_merged_branches_path(@project.namespace, @project), class: 'btn btn-inverted btn-remove has-tooltip', title: "Delete all branches that are merged into '#{@project.repository.root_ref}'", method: :delete, data: { confirm: "Deleting the merged branches cannot be undone. Are you sure?", container: 'body' } do diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index 2e54af698aa..766f119116f 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -13,9 +13,6 @@ Environment: = link_to @environment.name, environment_path(@environment) - .col-sm-6 - .nav-controls - = render 'projects/deployments/actions', deployment: @environment.last_deployment .prometheus-state .js-getting-started.hidden .row diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index bc426f1dc0c..0872a1a0503 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -19,6 +19,8 @@ = render 'projects/merge_requests/widget/open/conflicts' - elsif @merge_request.work_in_progress? = render 'projects/merge_requests/widget/open/wip' + - elsif @merge_request.merge_when_pipeline_succeeds? && @merge_request.merge_error.present? + = render 'projects/merge_requests/widget/open/error' - elsif @merge_request.merge_when_pipeline_succeeds? = render 'projects/merge_requests/widget/open/merge_when_pipeline_succeeds' - elsif !@merge_request.can_be_merged_by?(current_user) diff --git a/app/views/projects/merge_requests/widget/open/_error.html.haml b/app/views/projects/merge_requests/widget/open/_error.html.haml new file mode 100644 index 00000000000..bbdc053609f --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_error.html.haml @@ -0,0 +1,6 @@ +%h4 + = icon('exclamation-triangle') + This merge request failed to be merged automatically + +%p + = @merge_request.merge_error diff --git a/app/views/projects/notes/_comment_button.html.haml b/app/views/projects/notes/_comment_button.html.haml index 6bb55f04b6e..29cf5825292 100644 --- a/app/views/projects/notes/_comment_button.html.haml +++ b/app/views/projects/notes/_comment_button.html.haml @@ -16,7 +16,7 @@ %p Add a general comment to this #{noteable_name}. - %li.divider + %li.divider.droplab-item-ignore %li#discussion{ data: { value: 'DiscussionNote', 'submit-text' => 'Start discussion', 'close-text' => "Start discussion & close #{noteable_name}", 'reopen-text' => "Start discussion & reopen #{noteable_name}" } } %a{ href: '#' } diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index fd7bd21677c..d6c4195e2d0 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -70,7 +70,7 @@ = link_to 'Set up Koding', add_koding_stack_path(@project) - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present? %li.missing - = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', target_branch: 'auto-deploy', context: 'autodeploy') do + = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', branch_name: 'auto-deploy', context: 'autodeploy') do Set up auto deploy - if @repository.commit diff --git a/app/views/shared/_branch_switcher.html.haml b/app/views/shared/_branch_switcher.html.haml index 7799aff6b5b..69e3f3042a9 100644 --- a/app/views/shared/_branch_switcher.html.haml +++ b/app/views/shared/_branch_switcher.html.haml @@ -1,8 +1,8 @@ -- dropdown_toggle_text = @target_branch || tree_edit_branch -= hidden_field_tag 'target_branch', dropdown_toggle_text +- dropdown_toggle_text = @branch_name || tree_edit_branch += hidden_field_tag 'branch_name', dropdown_toggle_text .dropdown - = dropdown_toggle dropdown_toggle_text, { toggle: 'dropdown', selected: dropdown_toggle_text, field_name: 'target_branch', form_id: '.js-edit-blob-form', refs_url: namespace_project_branches_path(@project.namespace, @project) }, { toggle_class: 'js-project-branches-dropdown js-target-branch' } + = dropdown_toggle dropdown_toggle_text, { toggle: 'dropdown', selected: dropdown_toggle_text, field_name: 'branch_name', form_id: '.js-edit-blob-form', refs_url: namespace_project_branches_path(@project.namespace, @project) }, { toggle_class: 'js-project-branches-dropdown js-target-branch' } .dropdown-menu.dropdown-menu-selectable.dropdown-menu-paging.dropdown-menu-branches = render partial: 'shared/projects/blob/branch_page_default' = render partial: 'shared/projects/blob/branch_page_create' diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml index 3ac5e15d1c4..0b37fe3013b 100644 --- a/app/views/shared/_new_commit_form.html.haml +++ b/app/views/shared/_new_commit_form.html.haml @@ -1,11 +1,11 @@ = render 'shared/commit_message_container', placeholder: placeholder - if @project.empty_repo? - = hidden_field_tag 'target_branch', @ref + = hidden_field_tag 'branch_name', @ref - else - if can?(current_user, :push_code, @project) .form-group.branch - = label_tag 'target_branch', 'Target branch', class: 'control-label' + = label_tag 'branch_name', 'Target branch', class: 'control-label' .col-sm-10 = render 'shared/branch_switcher' @@ -16,7 +16,7 @@ = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}" Start a <strong>new merge request</strong> with these changes - else - = hidden_field_tag 'target_branch', @target_branch || tree_edit_branch + = hidden_field_tag 'branch_name', @branch_name || tree_edit_branch = hidden_field_tag 'create_merge_request', 1 = hidden_field_tag 'original_branch', @ref, class: 'js-original-branch' diff --git a/app/workers/system_hook_worker.rb b/app/workers/system_hook_worker.rb index baf2f12eeac..55d4e7d6dab 100644 --- a/app/workers/system_hook_worker.rb +++ b/app/workers/system_hook_worker.rb @@ -2,6 +2,8 @@ class SystemHookWorker include Sidekiq::Worker include DedicatedSidekiqQueue + sidekiq_options retry: 4 + def perform(hook_id, data, hook_name) SystemHook.find(hook_id).execute(data, hook_name) end diff --git a/changelogs/unreleased/29734-prometheus-monitoring-page-displays-button-to-control-manual-actions.yml b/changelogs/unreleased/29734-prometheus-monitoring-page-displays-button-to-control-manual-actions.yml new file mode 100644 index 00000000000..ca4a8889454 --- /dev/null +++ b/changelogs/unreleased/29734-prometheus-monitoring-page-displays-button-to-control-manual-actions.yml @@ -0,0 +1,4 @@ +--- +title: Remove pipeline controls for last deployment from Environment monitoring page +merge_request: 10769 +author: diff --git a/changelogs/unreleased/29977-style-comments-and-system-notes-real-time-updates.yml b/changelogs/unreleased/29977-style-comments-and-system-notes-real-time-updates.yml new file mode 100644 index 00000000000..c1640777e12 --- /dev/null +++ b/changelogs/unreleased/29977-style-comments-and-system-notes-real-time-updates.yml @@ -0,0 +1,4 @@ +--- +title: Added quick-update (fade-in) animation to newly rendered notes +merge_request: 10623 +author: diff --git a/changelogs/unreleased/30484-profile-dropdown-account-name.yml b/changelogs/unreleased/30484-profile-dropdown-account-name.yml new file mode 100644 index 00000000000..71aa1ce139b --- /dev/null +++ b/changelogs/unreleased/30484-profile-dropdown-account-name.yml @@ -0,0 +1,4 @@ +--- +title: Added profile name to user dropdown +merge_request: +author: diff --git a/changelogs/unreleased/fix-29125.yml b/changelogs/unreleased/fix-29125.yml new file mode 100644 index 00000000000..00b5e8c0a2a --- /dev/null +++ b/changelogs/unreleased/fix-29125.yml @@ -0,0 +1,4 @@ +--- +title: Display custom hook error messages when automatic merge is enabled +merge_request: +author: diff --git a/changelogs/unreleased/fix-link-prometheus-opening-outside-gitlab.yml b/changelogs/unreleased/fix-link-prometheus-opening-outside-gitlab.yml new file mode 100644 index 00000000000..e684a1f6684 --- /dev/null +++ b/changelogs/unreleased/fix-link-prometheus-opening-outside-gitlab.yml @@ -0,0 +1,4 @@ +--- +title: Removed target blank from the metrics action inside the environments list +merge_request: 10726 +author: diff --git a/changelogs/unreleased/group-milestone-date-fields-fix.yml b/changelogs/unreleased/group-milestone-date-fields-fix.yml new file mode 100644 index 00000000000..3cf3d3fa5ed --- /dev/null +++ b/changelogs/unreleased/group-milestone-date-fields-fix.yml @@ -0,0 +1,4 @@ +--- +title: Fixed group milestone date dropdowns not opening +merge_request: +author: diff --git a/changelogs/unreleased/hook_retries.yml b/changelogs/unreleased/hook_retries.yml new file mode 100644 index 00000000000..ee30399edbb --- /dev/null +++ b/changelogs/unreleased/hook_retries.yml @@ -0,0 +1,4 @@ +--- +title: Add retry to system hook worker +merge_request: 10801 +author: diff --git a/changelogs/unreleased/plantuml-filter-after-highlight.yml b/changelogs/unreleased/plantuml-filter-after-highlight.yml new file mode 100644 index 00000000000..f438bfd2bf7 --- /dev/null +++ b/changelogs/unreleased/plantuml-filter-after-highlight.yml @@ -0,0 +1,4 @@ +--- +title: Fix PlantUML integration in GFM +merge_request: 10651 +author: diff --git a/changelogs/unreleased/query-users-by-extern-uid.yml b/changelogs/unreleased/query-users-by-extern-uid.yml new file mode 100644 index 00000000000..39d1cf8d3f3 --- /dev/null +++ b/changelogs/unreleased/query-users-by-extern-uid.yml @@ -0,0 +1,4 @@ +--- +title: Implement search by extern_uid in Users API +merge_request: 10509 +author: Robin Bobbitt diff --git a/changelogs/unreleased/right-sidebar-closed-default-mobile.yml b/changelogs/unreleased/right-sidebar-closed-default-mobile.yml new file mode 100644 index 00000000000..cf0ec418f0e --- /dev/null +++ b/changelogs/unreleased/right-sidebar-closed-default-mobile.yml @@ -0,0 +1,4 @@ +--- +title: Set the issuable sidebar to remain closed for mobile devices +merge_request: +author: diff --git a/changelogs/unreleased/sh-add-index-to-system-note-metadata.yml b/changelogs/unreleased/sh-add-index-to-system-note-metadata.yml new file mode 100644 index 00000000000..6b226c53f30 --- /dev/null +++ b/changelogs/unreleased/sh-add-index-to-system-note-metadata.yml @@ -0,0 +1,4 @@ +--- +title: Add unique index for notes_id to system note metadata table +merge_request: +author: diff --git a/changelogs/unreleased/sh-issue-29247-fix.yml b/changelogs/unreleased/sh-issue-29247-fix.yml new file mode 100644 index 00000000000..b530e5da55b --- /dev/null +++ b/changelogs/unreleased/sh-issue-29247-fix.yml @@ -0,0 +1,5 @@ +--- +title: Don't delete a branch involved in an open merge request in "Delete all merged + branches" service +merge_request: +author: diff --git a/changelogs/unreleased/uassign_on_member_removing.yml b/changelogs/unreleased/uassign_on_member_removing.yml new file mode 100644 index 00000000000..cd60bdf5b3d --- /dev/null +++ b/changelogs/unreleased/uassign_on_member_removing.yml @@ -0,0 +1,4 @@ +--- +title: Unassign all Issues and Merge Requests when member leaves a team +merge_request: +author: diff --git a/db/migrate/20170419001229_add_index_to_system_note_metadata.rb b/db/migrate/20170419001229_add_index_to_system_note_metadata.rb new file mode 100644 index 00000000000..c68fd920fff --- /dev/null +++ b/db/migrate/20170419001229_add_index_to_system_note_metadata.rb @@ -0,0 +1,17 @@ +class AddIndexToSystemNoteMetadata < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def up + # MySQL automatically creates an index on a foreign-key constraint; PostgreSQL does not + add_concurrent_index :system_note_metadata, :note_id, unique: true if Gitlab::Database.postgresql? + end + + def down + remove_concurrent_index :system_note_metadata, :note_id, unique: true if Gitlab::Database.postgresql? + end +end diff --git a/db/schema.rb b/db/schema.rb index d46b7564958..f89e9adb7d6 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: 20170418103908) do +ActiveRecord::Schema.define(version: 20170419001229) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -1142,6 +1142,8 @@ ActiveRecord::Schema.define(version: 20170418103908) do t.datetime "updated_at", null: false end + add_index "system_note_metadata", ["note_id"], name: "index_system_note_metadata_on_note_id", unique: true, using: :btree + create_table "taggings", force: :cascade do |t| t.integer "tag_id" t.integer "taggable_id" diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md index 6515b1a264a..5c856835039 100644 --- a/doc/administration/integration/plantuml.md +++ b/doc/administration/integration/plantuml.md @@ -1,6 +1,6 @@ # PlantUML & GitLab -> [Introduced][ce-7810] in GitLab 8.16. +> [Introduced][ce-8537] in GitLab 8.16. When [PlantUML](http://plantuml.com) integration is enabled and configured in GitLab we are able to create simple diagrams in AsciiDoc and Markdown documents @@ -93,3 +93,5 @@ Some parameters can be added to the AsciiDoc block definition: - *height*: Height attribute added to the img tag. Markdown does not support any parameters and will always use PNG format. + +[ce-8537]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8537
\ No newline at end of file diff --git a/doc/api/users.md b/doc/api/users.md index a79d31d19fa..ff5bffa178f 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -132,6 +132,18 @@ For example: GET /users?username=jack_smith ``` +You can also lookup users by external UID and provider: + +``` +GET /users?extern_uid=:extern_uid&provider=:provider +``` + +For example: + +``` +GET /users?extern_uid=1234567&provider=github +``` + You can search for users who are external with: `/users?external=true` ## Single user diff --git a/doc/development/fe_guide/img/boards_diagram.png b/doc/development/fe_guide/img/boards_diagram.png Binary files differnew file mode 100644 index 00000000000..7a2cf972fd0 --- /dev/null +++ b/doc/development/fe_guide/img/boards_diagram.png diff --git a/doc/development/fe_guide/img/vue_arch.png b/doc/development/fe_guide/img/vue_arch.png Binary files differnew file mode 100644 index 00000000000..a67706c7c1e --- /dev/null +++ b/doc/development/fe_guide/img/vue_arch.png diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md index c7d4f2e9c23..e2a198f637f 100644 --- a/doc/development/fe_guide/index.md +++ b/doc/development/fe_guide/index.md @@ -27,6 +27,59 @@ For our currently-supported browsers, see our [requirements][requirements]. --- +## Development Process + +When you are assigned an issue please follow the next steps: + +### Divide a big feature into small Merge Requests +1. Big Merge Request are painful to review. In order to make this process easier we +must break a big feature into smaller ones and create a Merge Request for each step. +1. First step is to create a branch from `master`, let's call it `new-feature`. This branch +will be the recipient of all the smaller Merge Requests. Only this one will be merged to master. +1. Don't do any work on this one, let's keep it synced with master. +1. Create a new branch from `new-feature`, let's call it `new-feature-step-1`. We advise you +to clearly identify which step the branch represents. +1. Do the first part of the modifications in this branch. The target branch of this Merge Request +should be `new-feature`. +1. Once `new-feature-step-1` gets merged into `new-feature` we can continue our work. Create a new +branch from `new-feature`, let's call it `new-feature-step-2` and repeat the process done before. + +```shell +* master +|\ +| * new-feature +| |\ +| | * new-feature-step-1 +| |\ +| | * new-feature-step-2 +| |\ +| | * new-feature-step-3 +``` + +**Tips** +- Make sure `new-feature` branch is always synced with `master`: merge master frequently. +- Do the same for the feature branch you have opened. This can be accomplished by merging `master` into `new-feature` and `new-feature` into `new-feature-step-*` +- Avoid rewriting history. + +### Share your work early +1. Before writing code guarantee your vision of the architecture is aligned with +GitLab's architecture. +1. Add a diagram to the issue and ask a Frontend Architecture about it. + + ![Diagram of Issue Boards Architecture](img/boards_diagram.png) + +1. Don't take more than one week between starting work on a feature and +sharing a Merge Request with a reviewer or a maintainer. + +### Vue features +1. Follow the steps in [Vue.js Best Practices](vue.md) +1. Follow the style guide. +1. Only a handful of people are allowed to merge Vue related features. +Reach out to @jschatz, @iamphill, @fatihacet or @filipa early in this process. + + +--- + ## [Architecture](architecture.md) How we go about making fundamental design decisions in GitLab's frontend team or make changes to our frontend development guidelines. diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md index abd241c0bc8..ed656476a96 100644 --- a/doc/development/fe_guide/style_guide_js.md +++ b/doc/development/fe_guide/style_guide_js.md @@ -58,7 +58,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns. import Bar from './bar'; ``` -- **Never** disable the `no-undef` rule. Declare globals with `/* global Foo */` instead. +- **Never** disable the `no-undef` rule. Declare globals with `/* global Foo */` instead. - When declaring multiple globals, always use one `/* global [name] */` line per variable. @@ -183,6 +183,19 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns. parseInt('10', 10); ``` +#### CSS classes used for JavaScript +- If the class is being used in Javascript it needs to be prepend with `js-` + ```html + // bad + <button class="add-user"> + Add User + </button> + + // good + <button class="js-add-user"> + Add User + </button> + ``` ### Vue.js @@ -200,6 +213,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns. #### Naming - **Extensions**: Use `.vue` extension for Vue components. - **Reference Naming**: Use PascalCase for Vue components and camelCase for their instances: + ```javascript // bad import cardBoard from 'cardBoard'; @@ -217,6 +231,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns. cardBoard: CardBoard }; ``` + - **Props Naming:** - Avoid using DOM component prop names. - Use kebab-case instead of camelCase to provide props in templates. @@ -243,12 +258,18 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns. <component v-if="bar" param="baz" /> + <button class="btn">Click me</button> + // good <component v-if="bar" param="baz" /> + <button class="btn"> + Click me + </button> + // if props fit in one line then keep it on the same line <component bar="bar" /> ``` diff --git a/doc/development/fe_guide/testing.md b/doc/development/fe_guide/testing.md index a4631fd0073..66afbf4db4d 100644 --- a/doc/development/fe_guide/testing.md +++ b/doc/development/fe_guide/testing.md @@ -26,6 +26,10 @@ browser and you will not have access to certain APIs, such as [`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification), which will have to be stubbed. +### Writing tests +### Vue.js unit tests +See this [section][vue-test]. + ### Running frontend tests `rake karma` runs the frontend-only (JavaScript) tests. @@ -134,3 +138,4 @@ Scenario: Developer can approve merge request [jasmine-focus]: https://jasmine.github.io/2.5/focused_specs.html [jasmine-jquery]: https://github.com/velesin/jasmine-jquery [karma]: http://karma-runner.github.io/ +[vue-test]:https://docs.gitlab.com/ce/development/fe_guide/vue.html#testing-vue-components diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md index 3e3406e7d6a..45c8300d9de 100644 --- a/doc/development/fe_guide/vue.md +++ b/doc/development/fe_guide/vue.md @@ -19,13 +19,31 @@ We don't want to refactor all GitLab frontend code into Vue.js, here are some gu when not to use Vue.js: - Adding or changing static information; -- Features that highly depend on jQuery will be hard to work with Vue.js +- Features that highly depend on jQuery will be hard to work with Vue.js; +- Features without reactive data; As always, the Frontend Architectural Experts are available to help with any Vue or JavaScript questions. -## How to build a new feature with Vue.js +## Vue architecture -**Components, Stores and Services** +All new features built with Vue.js must follow a [Flux architecture][flux]. +The main goal we are trying to achieve is to have only one data flow and only one data entry. +In order to achieve this goal, each Vue bundle needs a Store - where we keep all the data -, +a Service - that we use to communicate with the server - and a main Vue component. + +Think of the Main Vue Component as the entry point of your application. This is the only smart +component that should exist in each Vue feature. +This component is responsible for: +1. Calling the Service to get data from the server +1. Calling the Store to store the data received +1. Mounting all the other components + + ![Vue Architecture](img/vue_arch.png) + +You can also read about this architecture in vue docs about [state management][state-management] +and about [one way data flow][one-way-data-flow]. + +### Components, Stores and Services In some features implemented with Vue.js, like the [issue board][issue-boards] or [environments table][environments-table] @@ -46,16 +64,17 @@ _For consistency purposes, we recommend you to follow the same structure._ Let's look into each of them: -**A `*_bundle.js` file** +### A `*_bundle.js` file This is the index file of your new feature. This is where the root Vue instance of the new feature should be. -The Store and the Service should be imported and initialized in this file and provided as a prop to the main component. +The Store and the Service should be imported and initialized in this file and +provided as a prop to the main component. Don't forget to follow [these steps.][page_specific_javascript] -**A folder for Components** +### A folder for Components This folder holds all components that are specific of this new feature. If you need to use or create a component that will probably be used somewhere @@ -70,29 +89,320 @@ in one table would not be a good use of this pattern. You can read more about components in Vue.js site, [Component System][component-system] -**A folder for the Store** +### A folder for the Store The Store is a class that allows us to manage the state in a single -source of truth. +source of truth. It is not aware of the service or the components. The concept we are trying to follow is better explained by Vue documentation itself, please read this guide: [State Management][state-management] -**A folder for the Service** +### A folder for the Service + +The Service is a class used only to communicate with the server. +It does not store or manipulate any data. It is not aware of the store or the components. +We use [vue-resource][vue-resource-repo] to communicate with the server. + +### End Result + +The following example shows an application: + +```javascript +// store.js +export default class Store { + + /** + * This is where we will iniatialize the state of our data. + * Usually in a small SPA you don't need any options when starting the store. In the case you do + * need guarantee it's an Object and it's documented. + * + * @param {Object} options + */ + constructor(options) { + this.options = options; + + // Create a state object to handle all our data in the same place + this.todos = []: + } + + setTodos(todos = []) { + this.todos = todos; + } + + addTodo(todo) { + this.todos.push(todo); + } + + removeTodo(todoID) { + const state = this.todos; + + const newState = state.filter((element) => {element.id !== todoID}); + + this.todos = newState; + } +} + +// service.js +import Vue from 'vue'; +import VueResource from 'vue-resource'; +import 'vue_shared/vue_resource_interceptor'; + +Vue.use(VueResource); + +export default class Service { + constructor(options) { + this.todos = Vue.resource(endpoint.todosEndpoint); + } + + getTodos() { + return this.todos.get(); + } + + addTodo(todo) { + return this.todos.put(todo); + } +} +// todo_component.vue +<script> +export default { + props: { + data: { + type: Object, + required: true, + }, + } +} +</script> +<template> + <div> + <h1> + Title: {{data.title}} + </h1> + <p> + {{data.text}} + </p> + </div> +</template> + +// todos_main_component.vue +<script> +import Store from 'store'; +import Service from 'service'; +import TodoComponent from 'todoComponent'; +export default { + /** + * Although most data belongs in the store, each component it's own state. + * We want to show a loading spinner while we are fetching the todos, this state belong + * in the component. + * + * We need to access the store methods through all methods of our component. + * We need to access the state of our store. + */ + data() { + const store = new Store(); + + return { + isLoading: false, + store: store, + todos: store.todos, + }; + }, + + components: { + todo: TodoComponent, + }, + + created() { + this.service = new Service('todos'); + + this.getTodos(); + }, + + methods: { + getTodos() { + this.isLoading = true; + + this.service.getTodos() + .then(response => response.json()) + .then((response) => { + this.store.setTodos(response); + this.isLoading = false; + }) + .catch(() => { + this.isLoading = false; + // Show an error + }); + }, + + addTodo(todo) { + this.service.addTodo(todo) + then(response => response.json()) + .then((response) => { + this.store.addTodo(response); + }) + .catch(() => { + // Show an error + }); + } + } +} +</script> +<template> + <div class="container"> + <div v-if="isLoading"> + <i + class="fa fa-spin fa-spinner" + aria-hidden="true" /> + </div> + + <div + v-if="!isLoading" + class="js-todo-list"> + <template v-for='todo in todos'> + <todo :data="todo" /> + </template> + + <button + @click="addTodo" + class="js-add-todo"> + Add Todo + </button> + </div> + <div> +</template> + +// bundle.js +import todoComponent from 'todos_main_component.vue'; + +new Vue({ + el: '.js-todo-app', + components: { + todoComponent, + }, + render: createElement => createElement('todo-component' { + props: { + someProp: [], + } + }), +}); -The Service is used only to communicate with the server. -It does not store or manipulate any data. -We use [vue-resource][vue-resource-repo] to -communicate with the server. +``` -The [issue boards service][issue-boards-service] -is a good example of this pattern. +The [issue boards service][issue-boards-service] is a good example of this pattern. ## Style guide Please refer to the Vue section of our [style guide](style_guide_js.md#vuejs) for best practices while writing your Vue components and templates. +## Testing Vue Components + +Each Vue component has a unique output. This output is always present in the render function. + +Although we can test each method of a Vue component individually, our goal must be to test the output +of the render/template function, which represents the state at all times. + +Make use of Vue Resource Interceptors to mock data returned by the service. + +Here's how we would test the Todo App above: + +```javascript +import component from 'todos_main_component'; + +describe('Todos App', () => { + it('should render the loading state while the request is being made', () => { + const Component = Vue.extend(component); + + const vm = new Component().$mount(); + + expect(vm.$el.querySelector('i.fa-spin')).toBeDefined(); + }); + + describe('with data', () => { + // Mock the service to return data + const interceptor = (request, next) => { + next(request.respondWith(JSON.stringify([{ + title: 'This is a todo', + body: 'This is the text' + }]), { + status: 200, + })); + }; + + let vm; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + + const Component = Vue.extend(component); + + vm = new Component().$mount(); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + + it('should render todos', (done) => { + setTimeout(() => { + expect(vm.$el.querySelectorAll('.js-todo-list div').length).toBe(1); + done(); + }, 0); + }); + }); + + describe('add todo', () => { + let vm; + beforeEach(() => { + const Component = Vue.extend(component); + vm = new Component().$mount(); + }); + it('should add a todos', (done) => { + setTimeout(() => { + vm.$el.querySelector('.js-add-todo').click(); + + // Add a new interceptor to mock the add Todo request + Vue.nextTick(() => { + expect(vm.$el.querySelectorAll('.js-todo-list div').length).toBe(2); + }); + }, 0); + }); + }); +}); +``` + +### Stubbing API responses +[Vue Resource Interceptors][vue-resource-interceptor] allow us to add a interceptor with +the response we need: + +```javascript + // Mock the service to return data + const interceptor = (request, next) => { + next(request.respondWith(JSON.stringify([{ + title: 'This is a todo', + body: 'This is the text' + }]), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + it('should do something', (done) => { + setTimeout(() => { + // Test received data + done(); + }, 0); + }); +``` + [vue-docs]: http://vuejs.org/guide/index.html [issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards @@ -100,5 +410,8 @@ for best practices while writing your Vue components and templates. [page_specific_javascript]: https://docs.gitlab.com/ce/development/frontend.html#page-specific-javascript [component-system]: https://vuejs.org/v2/guide/#Composing-with-Components [state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch +[one-way-data-flow]: https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow [vue-resource-repo]: https://github.com/pagekit/vue-resource +[vue-resource-interceptor]: https://github.com/pagekit/vue-resource/blob/develop/docs/http.md#interceptors [issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6 +[flux]: https://facebook.github.io/flux diff --git a/doc/user/admin_area/user_cohorts.md b/doc/user/admin_area/user_cohorts.md index 1671487bc8c..e25e7a8bbc3 100644 --- a/doc/user/admin_area/user_cohorts.md +++ b/doc/user/admin_area/user_cohorts.md @@ -1,30 +1,37 @@ # Cohorts > **Notes:** -- [Introduced][ce-23361] in GitLab 9.1. +> [Introduced][ce-23361] in GitLab 9.1. As a benefit of having the [usage ping active](settings/usage_statistics.md), GitLab lets you analyze the users' activities of your GitLab installation. Under `/admin/cohorts`, when the usage ping is active, GitLab will show the monthly cohorts of new users and their activities over time. +## Overview + How do we read the user cohorts table? Let's take an example with the following user cohorts. ![User cohort example](img/cohorts.png) -For the cohort of June 2016, 163 users have been created on this server. One -month later, in July 2016, 155 users (or 95% of the June cohort) are still -active. Two months later, 139 users (or 85%) are still active. 9 months later, -we can see that only 6% of this cohort are still active. +For the cohort of June 2016, 163 users have been added on this server and have +been active since this month. One month later, in July 2016, out of +these 163 users, 155 users (or 95% of the June cohort) are still active. Two +months later, 139 users (or 85%) are still active. 9 months later, we can see +that only 6% of this cohort are still active. + +The Inactive users column shows the number of users who have been added during +the month, but who have never actually had any activity in the instance. How do we measure the activity of users? GitLab considers a user active if: + * the user signs in * the user has Git activity (whether push or pull). -### Setup +## Setup -1. Activate the usage ping as defined [in the documentation](settings/usage_statistics.md) +1. [Activate the usage ping](settings/usage_statistics.md) 2. Go to `/admin/cohorts` to see the user cohorts of the server [ce-23361]: https://gitlab.com/gitlab-org/gitlab-ce/issues/23361 diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 3122e95fc0e..637967510f3 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -7,6 +7,9 @@ project itself, the highest permission level is used. On public and internal projects the Guest role is not enforced. All users will be able to create issues, leave comments, and pull or download the project code. +When a member leaves the team the all assigned Issues and Merge Requests +will be unassigned automatically. + GitLab administrators receive all permissions. To add or import a user, you can follow the [project users and members diff --git a/doc/workflow/groups.md b/doc/workflow/groups.md index 882747e14e9..1cb3c940f00 100644 --- a/doc/workflow/groups.md +++ b/doc/workflow/groups.md @@ -1,6 +1,6 @@ # GitLab Groups -GitLab groups allow you to group projects into directories and give users to several projects at once. +GitLab groups allow you to group projects into directories and give users access to several projects at once. When you create a new project in GitLab, the default namespace for the project is the personal namespace associated with your GitLab user. In this document we will see how to create groups, put projects in groups and manage who can access the projects in a group. diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index 27fa67c1843..4dee0cd23dc 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -177,9 +177,3 @@ Feature: Project Issues And I should not see labels field And I submit new issue "500 error on profile" Then I should see issue "500 error on profile" - - @javascript - Scenario: Another user adds a comment to issue I'm currently viewing - Given I visit issue page "Release 0.4" - And another user adds a comment with text "Yay!" to issue "Release 0.4" - Then I should see a new comment with text "Yay!" diff --git a/features/project/merge_requests/revert.feature b/features/project/merge_requests/revert.feature index ec6666f227f..aaac5fd7209 100644 --- a/features/project/merge_requests/revert.feature +++ b/features/project/merge_requests/revert.feature @@ -25,7 +25,5 @@ Feature: Revert Merge Requests @javascript Scenario: I revert a merge request in a new merge request Given I click on the revert button - And I am on the Merge Request detail page - And I click on the revert button And I revert the changes in a new merge request Then I should see the new merge request notice diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index 894c4a96bb8..536c24b6882 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -135,7 +135,7 @@ Feature: Project Source Browse Files And I fill the commit message And I click on "Commit changes" Then I am on the new file page - And I see a commit error message + And I see "Path can contain only..." @javascript Scenario: I can create file with a directory name diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb index adaf375453c..dcbf36df656 100644 --- a/features/steps/group/members.rb +++ b/features/steps/group/members.rb @@ -87,7 +87,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps end step 'I click on the "Remove User From Group" button for "John Doe"' do - find(:css, 'li', text: "John Doe").find(:css, 'a.btn-remove').click + find(:css, '.project-members-page li', text: "John Doe").find(:css, 'a.btn-remove').click # poltergeist always confirms popups. end @@ -97,7 +97,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps end step 'I should not see the "Remove User From Group" button for "John Doe"' do - expect(find(:css, 'li', text: "John Doe")).not_to have_selector(:css, 'a.btn-remove') + expect(find(:css, '.project-members-page li', text: "John Doe")).not_to have_selector(:css, 'a.btn-remove') # poltergeist always confirms popups. end diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index c0dc48f1bb2..637e6568267 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -345,17 +345,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end end - step 'another user adds a comment with text "Yay!" to issue "Release 0.4"' do - issue = Issue.find_by!(title: 'Release 0.4') - create(:note_on_issue, noteable: issue, project: project, note: 'Yay!') - end - - step 'I should see a new comment with text "Yay!"' do - page.within '#notes' do - expect(page).to have_content('Yay!') - end - end - def filter_issue(text) fill_in 'issuable_search', with: text end diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 5bd3c1a1246..d52fa10c337 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -284,7 +284,11 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I see "Unable to create directory"' do - expect(page).to have_content('Directory already exists') + expect(page).to have_content('A directory with this name already exists') + end + + step 'I see "Path can contain only..."' do + expect(page).to have_content('Path can contain only') end step 'I see a commit error message' do diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 66b37fd2bcc..621b9dcecd9 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -62,7 +62,7 @@ module API post ":id/repository/commits" do authorize! :push_code, user_project - attrs = declared_params.merge(start_branch: declared_params[:branch], target_branch: declared_params[:branch]) + attrs = declared_params.merge(start_branch: declared_params[:branch], branch_name: declared_params[:branch]) result = ::Files::MultiService.new(user_project, current_user, attrs).execute @@ -140,7 +140,7 @@ module API commit_params = { commit: commit, start_branch: params[:branch], - target_branch: params[:branch] + branch_name: params[:branch] } result = ::Commits::CherryPickService.new(user_project, current_user, commit_params).execute diff --git a/lib/api/files.rb b/lib/api/files.rb index 33fc970dc09..e6ea12c5ab7 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -5,7 +5,7 @@ module API { file_path: attrs[:file_path], start_branch: attrs[:branch], - target_branch: attrs[:branch], + branch_name: attrs[:branch], commit_message: attrs[:commit_message], file_content: attrs[:content], file_content_encoding: attrs[:encoding], @@ -130,7 +130,7 @@ module API authorize! :push_code, user_project file_params = declared_params(include_missing: false) - result = ::Files::DestroyService.new(user_project, current_user, commit_params(file_params)).execute + result = ::Files::DeleteService.new(user_project, current_user, commit_params(file_params)).execute if result[:status] != :success render_api_error!(result[:message], 400) diff --git a/lib/api/users.rb b/lib/api/users.rb index 9e0faff6c05..46f221f68fe 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -39,10 +39,13 @@ module API params do # CE optional :username, type: String, desc: 'Get a single user with a specific username' + optional :extern_uid, type: String, desc: 'Get a single user with a specific external authentication provider UID' + optional :provider, type: String, desc: 'The external provider' optional :search, type: String, desc: 'Search for a username' optional :active, type: Boolean, default: false, desc: 'Filters only active users' optional :external, type: Boolean, default: false, desc: 'Filters only external users' optional :blocked, type: Boolean, default: false, desc: 'Filters only blocked users' + all_or_none_of :extern_uid, :provider use :pagination end @@ -51,14 +54,17 @@ module API render_api_error!("Not authorized.", 403) end - if params[:username].present? - users = User.where(username: params[:username]) - else - users = User.all - users = users.active if params[:active] - users = users.search(params[:search]) if params[:search].present? - users = users.blocked if params[:blocked] - users = users.external if params[:external] && current_user.admin? + authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?) + + users = User.all + users = User.where(username: params[:username]) if params[:username] + users = users.active if params[:active] + users = users.search(params[:search]) if params[:search].present? + users = users.blocked if params[:blocked] + + if current_user.admin? + users = users.joins(:identities).merge(Identity.with_extern_uid(params[:provider], params[:extern_uid])) if params[:extern_uid] && params[:provider] + users = users.external if params[:external] end entity = current_user.admin? ? Entities::UserPublic : Entities::UserBasic diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb index 3414a2883e5..674de592f0a 100644 --- a/lib/api/v3/commits.rb +++ b/lib/api/v3/commits.rb @@ -53,7 +53,7 @@ module API attrs = declared_params.dup branch = attrs.delete(:branch_name) - attrs.merge!(branch: branch, start_branch: branch, target_branch: branch) + attrs.merge!(start_branch: branch, branch_name: branch) result = ::Files::MultiService.new(user_project, current_user, attrs).execute @@ -131,7 +131,7 @@ module API commit_params = { commit: commit, start_branch: params[:branch], - target_branch: params[:branch] + branch_name: params[:branch] } result = ::Commits::CherryPickService.new(user_project, current_user, commit_params).execute diff --git a/lib/api/v3/files.rb b/lib/api/v3/files.rb index 13542b0c71c..c76acc86504 100644 --- a/lib/api/v3/files.rb +++ b/lib/api/v3/files.rb @@ -6,7 +6,7 @@ module API { file_path: attrs[:file_path], start_branch: attrs[:branch], - target_branch: attrs[:branch], + branch_name: attrs[:branch], commit_message: attrs[:commit_message], file_content: attrs[:content], file_content_encoding: attrs[:encoding], @@ -123,7 +123,7 @@ module API file_params = declared_params(include_missing: false) file_params[:branch] = file_params.delete(:branch_name) - result = ::Files::DestroyService.new(user_project, current_user, commit_params(file_params)).execute + result = ::Files::DeleteService.new(user_project, current_user, commit_params(file_params)).execute if result[:status] == :success status(200) diff --git a/lib/banzai/filter/issuable_state_filter.rb b/lib/banzai/filter/issuable_state_filter.rb index 1a9d03beb51..327ea9449a1 100644 --- a/lib/banzai/filter/issuable_state_filter.rb +++ b/lib/banzai/filter/issuable_state_filter.rb @@ -15,8 +15,8 @@ module Banzai issuables = extractor.extract([doc]) issuables.each do |node, issuable| - if VISIBLE_STATES.include?(issuable.state) && node.children.present? - node.add_child(Nokogiri::XML::Text.new(" [#{issuable.state}]", doc)) + if VISIBLE_STATES.include?(issuable.state) && node.inner_html == issuable.reference_link_text(project) + node.content += " (#{issuable.state})" end end diff --git a/lib/banzai/filter/plantuml_filter.rb b/lib/banzai/filter/plantuml_filter.rb index b2537117558..5325819d828 100644 --- a/lib/banzai/filter/plantuml_filter.rb +++ b/lib/banzai/filter/plantuml_filter.rb @@ -7,14 +7,14 @@ module Banzai # class PlantumlFilter < HTML::Pipeline::Filter def call - return doc unless doc.at('pre.plantuml') && settings.plantuml_enabled + return doc unless doc.at('pre > code[lang="plantuml"]') && settings.plantuml_enabled plantuml_setup - doc.css('pre.plantuml').each do |el| + doc.css('pre > code[lang="plantuml"]').each do |node| img_tag = Nokogiri::HTML::DocumentFragment.parse( - Asciidoctor::PlantUml::Processor.plantuml_content(el.content, {})) - el.replace img_tag + Asciidoctor::PlantUml::Processor.plantuml_content(node.content, {})) + node.parent.replace(img_tag) end doc diff --git a/lib/gitlab/git/index.rb b/lib/gitlab/git/index.rb index af1744c9c46..1add037fa5f 100644 --- a/lib/gitlab/git/index.rb +++ b/lib/gitlab/git/index.rb @@ -1,8 +1,12 @@ module Gitlab module Git class Index + IndexError = Class.new(StandardError) + DEFAULT_MODE = 0o100644 + ACTIONS = %w(create create_dir update move delete).freeze + attr_reader :repository, :raw_index def initialize(repository) @@ -23,9 +27,8 @@ module Gitlab def create(options) options = normalize_options(options) - file_entry = get(options[:file_path]) - if file_entry - raise Gitlab::Git::Repository::InvalidBlobName.new("Filename already exists") + if get(options[:file_path]) + raise IndexError, "A file with this name already exists" end add_blob(options) @@ -34,13 +37,12 @@ module Gitlab def create_dir(options) options = normalize_options(options) - file_entry = get(options[:file_path]) - if file_entry - raise Gitlab::Git::Repository::InvalidBlobName.new("Directory already exists as a file") + if get(options[:file_path]) + raise IndexError, "A file with this name already exists" end if dir_exists?(options[:file_path]) - raise Gitlab::Git::Repository::InvalidBlobName.new("Directory already exists") + raise IndexError, "A directory with this name already exists" end options = options.dup @@ -55,7 +57,7 @@ module Gitlab file_entry = get(options[:file_path]) unless file_entry - raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist") + raise IndexError, "A file with this name doesn't exist" end add_blob(options, mode: file_entry[:mode]) @@ -66,7 +68,11 @@ module Gitlab file_entry = get(options[:previous_path]) unless file_entry - raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist") + raise IndexError, "A file with this name doesn't exist" + end + + if get(options[:file_path]) + raise IndexError, "A file with this name already exists" end raw_index.remove(options[:previous_path]) @@ -77,9 +83,8 @@ module Gitlab def delete(options) options = normalize_options(options) - file_entry = get(options[:file_path]) - unless file_entry - raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist") + unless get(options[:file_path]) + raise IndexError, "A file with this name doesn't exist" end raw_index.remove(options[:file_path]) @@ -95,10 +100,20 @@ module Gitlab end def normalize_path(path) + unless path + raise IndexError, "You must provide a file path" + end + pathname = Gitlab::Git::PathHelper.normalize_path(path.dup) - if pathname.each_filename.include?('..') - raise Gitlab::Git::Repository::InvalidBlobName.new('Invalid path') + pathname.each_filename do |segment| + if segment == '..' + raise IndexError, 'Path cannot include directory traversal' + end + + unless segment =~ Gitlab::Regex.file_name_regex + raise IndexError, "Path #{Gitlab::Regex.file_name_regex_message}" + end end pathname.to_s @@ -106,6 +121,10 @@ module Gitlab def add_blob(options, mode: nil) content = options[:content] + unless content + raise IndexError, "You must provide content" + end + content = Base64.decode64(content) if options[:encoding] == 'base64' detect = CharlockHolmes::EncodingDetector.new.detect(content) @@ -119,7 +138,7 @@ module Gitlab raw_index.add(path: options[:file_path], oid: oid, mode: mode || DEFAULT_MODE) rescue Rugged::IndexError => e - raise Gitlab::Git::Repository::InvalidBlobName.new(e.message) + raise IndexError, e.message end end end diff --git a/lib/gitlab/markup_helper.rb b/lib/gitlab/markup_helper.rb index dda371e6554..49285e35251 100644 --- a/lib/gitlab/markup_helper.rb +++ b/lib/gitlab/markup_helper.rb @@ -1,6 +1,11 @@ module Gitlab module MarkupHelper - module_function + extend self + + MARKDOWN_EXTENSIONS = %w(mdown mkd mkdn md markdown).freeze + ASCIIDOC_EXTENSIONS = %w(adoc ad asciidoc).freeze + OTHER_EXTENSIONS = %w(textile rdoc org creole wiki mediawiki rst).freeze + EXTENSIONS = MARKDOWN_EXTENSIONS + ASCIIDOC_EXTENSIONS + OTHER_EXTENSIONS # Public: Determines if a given filename is compatible with GitHub::Markup. # @@ -8,10 +13,7 @@ module Gitlab # # Returns boolean def markup?(filename) - gitlab_markdown?(filename) || - asciidoc?(filename) || - filename.downcase.end_with?(*%w(.textile .rdoc .org .creole .wiki - .mediawiki .rst)) + EXTENSIONS.include?(extension(filename)) end # Public: Determines if a given filename is compatible with @@ -21,7 +23,7 @@ module Gitlab # # Returns boolean def gitlab_markdown?(filename) - filename.downcase.end_with?(*%w(.mdown .mkd .mkdn .md .markdown)) + MARKDOWN_EXTENSIONS.include?(extension(filename)) end # Public: Determines if the given filename has AsciiDoc extension. @@ -30,7 +32,7 @@ module Gitlab # # Returns boolean def asciidoc?(filename) - filename.downcase.end_with?(*%w(.adoc .ad .asciidoc)) + ASCIIDOC_EXTENSIONS.include?(extension(filename)) end # Public: Determines if the given filename is plain text. @@ -39,12 +41,17 @@ module Gitlab # # Returns boolean def plain?(filename) - filename.downcase.end_with?('.txt') || - filename.casecmp('readme').zero? + extension(filename) == 'txt' || filename.casecmp('readme').zero? end def previewable?(filename) markup?(filename) end + + private + + def extension(filename) + File.extname(filename).downcase.delete('.') + end end end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index e599dd4a656..08b061d5e31 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -73,22 +73,6 @@ module Gitlab "can contain only letters, digits, '_', '-', '@', '+' and '.'." end - def file_path_regex - @file_path_regex ||= /\A[[[:alnum:]]_\-\.\/\@]*\z/.freeze - end - - def file_path_regex_message - "can contain only letters, digits, '_', '-', '@' and '.'. Separate directories with a '/'." - end - - def directory_traversal_regex - @directory_traversal_regex ||= /\.{2}/.freeze - end - - def directory_traversal_regex_message - "cannot include directory traversal." - end - def archive_formats_regex # |zip|tar| tar.gz | tar.bz2 | @archive_formats_regex ||= /(zip|tar|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/.freeze diff --git a/package.json b/package.json index a17399ddb8f..e65f30eea77 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "eslint-plugin-filenames": "^1.1.0", "eslint-plugin-import": "^2.2.0", "eslint-plugin-jasmine": "^2.1.0", + "eslint-plugin-promise": "^3.5.0", "istanbul": "^0.4.5", "jasmine-core": "^2.5.2", "jasmine-jquery": "^2.1.1", diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index 3e9f272a0d8..0fd09d156c4 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -106,7 +106,7 @@ describe Projects::BlobController do namespace_id: project.namespace, project_id: project, id: 'master/CHANGELOG', - target_branch: 'master', + branch_name: 'master', content: 'Added changes', commit_message: 'Update CHANGELOG' } @@ -178,7 +178,7 @@ describe Projects::BlobController do context 'when editing on the original repository' do it "redirects to forked project new merge request" do - default_params[:target_branch] = "fork-test-1" + default_params[:branch_name] = "fork-test-1" default_params[:create_merge_request] = 1 put :update, default_params diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb index ab94e292e48..a43dad5756d 100644 --- a/spec/controllers/projects/tree_controller_spec.rb +++ b/spec/controllers/projects/tree_controller_spec.rb @@ -97,29 +97,29 @@ describe Projects::TreeController do project_id: project, id: 'master', dir_name: path, - target_branch: target_branch, + branch_name: branch_name, commit_message: 'Test commit message') end context 'successful creation' do let(:path) { 'files/new_dir'} - let(:target_branch) { 'master-test'} + let(:branch_name) { 'master-test'} it 'redirects to the new directory' do expect(subject). - to redirect_to("/#{project.path_with_namespace}/tree/#{target_branch}/#{path}") + to redirect_to("/#{project.path_with_namespace}/tree/#{branch_name}/#{path}") expect(flash[:notice]).to eq('The directory has been successfully created.') end end context 'unsuccessful creation' do let(:path) { 'README.md' } - let(:target_branch) { 'master'} + let(:branch_name) { 'master'} it 'does not allow overwriting of existing files' do expect(subject). to redirect_to("/#{project.path_with_namespace}/tree/master") - expect(flash[:alert]).to eq('Directory already exists as a file') + expect(flash[:alert]).to eq('A file with this name already exists') end end end diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 361f9dac191..253a025af48 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -40,6 +40,10 @@ FactoryGirl.define do state :closed end + trait :opened do + state :opened + end + trait :reopened do state :reopened end diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index 87a8f62687a..9d205104ebe 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -109,7 +109,7 @@ describe "Admin::Projects", feature: true do expect(page).to have_content('Developer') end - find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click + find(:css, '.content-list li', text: current_user.name).find(:css, 'a.btn-remove').click expect(page).not_to have_selector(:css, '.content-list') end diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb new file mode 100644 index 00000000000..daa2c6afd63 --- /dev/null +++ b/spec/features/groups/milestone_spec.rb @@ -0,0 +1,36 @@ +require 'rails_helper' + +feature 'Group milestones', :feature, :js do + let(:group) { create(:group) } + let!(:project) { create(:project_empty_repo, group: group) } + let(:user) { create(:group_member, :master, user: create(:user), group: group ).user } + + before do + Timecop.freeze + + login_as(user) + end + + after do + Timecop.return + end + + context 'create a milestone' do + before do + visit new_group_milestone_path(group) + end + + it 'creates milestone with start date' do + fill_in 'Title', with: 'testing' + find('#milestone_start_date').click + + page.within(find('.pika-single')) do + click_button '1' + end + + click_button 'Create milestone' + + expect(find('.start_date')).to have_content(Date.today.at_beginning_of_month.strftime('%b %-d, %Y')) + end + end +end diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index 7b9d4534ada..85585587fb1 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -120,6 +120,20 @@ feature 'Issue Sidebar', feature: true do end end + context 'as a allowed mobile user', js: true do + before do + project.team << [user, :developer] + resize_screen_xs + visit_issue(project, issue) + end + + context 'mobile sidebar' do + it 'collapses the sidebar for small screens' do + expect(page).not_to have_css('aside.right-sidebar.right-sidebar-collapsed') + end + end + end + context 'as a guest' do before do project.team << [user, :guest] diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb index f5cfe2d666e..378f6de1a78 100644 --- a/spec/features/issues/note_polling_spec.rb +++ b/spec/features/issues/note_polling_spec.rb @@ -1,17 +1,15 @@ require 'spec_helper' -feature 'Issue notes polling' do - let!(:project) { create(:project, :public) } - let!(:issue) { create(:issue, project: project) } +feature 'Issue notes polling', :feature, :js do + let(:project) { create(:empty_project, :public) } + let(:issue) { create(:issue, project: project) } - background do + before do visit namespace_project_issue_path(project.namespace, project, issue) end - scenario 'Another user adds a comment to an issue', js: true do - note = create(:note, noteable: issue, project: project, - note: 'Looks good!') - + it 'should display the new comment' do + note = create(:note, noteable: issue, project: project, note: 'Looks good!') page.execute_script('notes.refresh();') expect(page).to have_selector("#note_#{note.id}", text: 'Looks good!') diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb index a62c5435748..4e128cd4a7d 100644 --- a/spec/features/merge_requests/widget_spec.rb +++ b/spec/features/merge_requests/widget_spec.rb @@ -141,6 +141,27 @@ describe 'Merge request', :feature, :js do end end + context 'view merge request with MWPS enabled but automatically merge fails' do + before do + merge_request.update( + merge_when_pipeline_succeeds: true, + merge_user: merge_request.author, + merge_error: 'Something went wrong' + ) + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'shows information about the merge error' do + # Wait for the `ci_status` and `merge_check` requests + wait_for_ajax + + page.within('.mr-widget-body') do + expect(page).to have_content('Something went wrong') + end + end + end + context 'merge error' do before do allow_any_instance_of(Repository).to receive(:merge).and_return(false) diff --git a/spec/features/projects/blobs/user_create_spec.rb b/spec/features/projects/blobs/user_create_spec.rb index fa1a753afcb..6ea149956fe 100644 --- a/spec/features/projects/blobs/user_create_spec.rb +++ b/spec/features/projects/blobs/user_create_spec.rb @@ -77,7 +77,7 @@ feature 'New blob creation', feature: true, js: true do project, user, start_branch: 'master', - target_branch: 'master', + branch_name: 'master', commit_message: 'Create file', file_path: 'feature.rb', file_content: content @@ -87,7 +87,7 @@ feature 'New blob creation', feature: true, js: true do end scenario 'shows error message' do - expect(page).to have_content('Your changes could not be committed because a file with the same name already exists') + expect(page).to have_content('A file with this name already exists') expect(page).to have_content('New file') expect(page).to have_content('NextFeature') end diff --git a/spec/features/projects/files/creating_a_file_spec.rb b/spec/features/projects/files/creating_a_file_spec.rb index 5d7bd3dc4ce..de6905f2b58 100644 --- a/spec/features/projects/files/creating_a_file_spec.rb +++ b/spec/features/projects/files/creating_a_file_spec.rb @@ -29,16 +29,16 @@ feature 'User wants to create a file', feature: true do scenario 'directory name contains Chinese characters' do submit_new_file(file_name: 'ä¸æ–‡/测试.md') - expect(page).to have_content 'The file has been successfully created.' + expect(page).to have_content 'The file has been successfully created' end scenario 'file name contains invalid characters' do submit_new_file(file_name: '\\') - expect(page).to have_content 'Your changes could not be committed, because the file name can contain only' + expect(page).to have_content 'Path can contain only' end scenario 'file name contains directory traversal' do submit_new_file(file_name: '../README.md') - expect(page).to have_content 'Your changes could not be committed, because the file name cannot include directory traversal.' + expect(page).to have_content 'Path cannot include directory traversal' end end diff --git a/spec/features/projects/files/editing_a_file_spec.rb b/spec/features/projects/files/editing_a_file_spec.rb index 3e544316f28..4da34108b46 100644 --- a/spec/features/projects/files/editing_a_file_spec.rb +++ b/spec/features/projects/files/editing_a_file_spec.rb @@ -8,7 +8,7 @@ feature 'User wants to edit a file', feature: true do let(:commit_params) do { start_branch: project.default_branch, - target_branch: project.default_branch, + branch_name: project.default_branch, commit_message: "Committing First Update", file_path: ".gitignore", file_content: "First Update", diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb index ce5c5f21167..34c6a10950f 100644 --- a/spec/features/projects/view_on_env_spec.rb +++ b/spec/features/projects/view_on_env_spec.rb @@ -25,7 +25,7 @@ describe 'View on environment', js: true do project, user, start_branch: branch_name, - target_branch: branch_name, + branch_name: branch_name, commit_message: "Add .gitlab/route-map.yml", file_path: '.gitlab/route-map.yml', file_content: route_map @@ -36,7 +36,7 @@ describe 'View on environment', js: true do project, user, start_branch: branch_name, - target_branch: branch_name, + branch_name: branch_name, commit_message: "Update feature", file_path: file_path, file_content: "# Noop" diff --git a/spec/javascripts/blob/sketch/index_spec.js b/spec/javascripts/blob/sketch/index_spec.js index 0e4431548c4..79f40559817 100644 --- a/spec/javascripts/blob/sketch/index_spec.js +++ b/spec/javascripts/blob/sketch/index_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable no-new */ +/* eslint-disable no-new, promise/catch-or-return */ import JSZip from 'jszip'; import SketchLoader from '~/blob/sketch'; diff --git a/spec/javascripts/droplab/constants_spec.js b/spec/javascripts/droplab/constants_spec.js index 35239e4fb8e..fd153a49fcd 100644 --- a/spec/javascripts/droplab/constants_spec.js +++ b/spec/javascripts/droplab/constants_spec.js @@ -26,4 +26,10 @@ describe('constants', function () { expect(constants.ACTIVE_CLASS).toBe('droplab-item-active'); }); }); + + describe('IGNORE_CLASS', function () { + it('should be `droplab-item-ignore`', function() { + expect(constants.IGNORE_CLASS).toBe('droplab-item-ignore'); + }); + }); }); diff --git a/spec/javascripts/droplab/drop_down_spec.js b/spec/javascripts/droplab/drop_down_spec.js index 802e2435672..7516b301917 100644 --- a/spec/javascripts/droplab/drop_down_spec.js +++ b/spec/javascripts/droplab/drop_down_spec.js @@ -2,7 +2,7 @@ import DropDown from '~/droplab/drop_down'; import utils from '~/droplab/utils'; -import { SELECTED_CLASS } from '~/droplab/constants'; +import { SELECTED_CLASS, IGNORE_CLASS } from '~/droplab/constants'; describe('DropDown', function () { describe('class constructor', function () { @@ -128,9 +128,10 @@ describe('DropDown', function () { describe('clickEvent', function () { beforeEach(function () { + this.classList = jasmine.createSpyObj('classList', ['contains']); this.list = { dispatchEvent: () => {} }; this.dropdown = { hide: () => {}, list: this.list, addSelectedClass: () => {} }; - this.event = { preventDefault: () => {}, target: {} }; + this.event = { preventDefault: () => {}, target: { classList: this.classList } }; this.customEvent = {}; this.closestElement = {}; @@ -140,6 +141,7 @@ describe('DropDown', function () { spyOn(this.event, 'preventDefault'); spyOn(window, 'CustomEvent').and.returnValue(this.customEvent); spyOn(utils, 'closest').and.returnValues(this.closestElement, undefined); + this.classList.contains.and.returnValue(false); DropDown.prototype.clickEvent.call(this.dropdown, this.event); }); @@ -164,15 +166,35 @@ describe('DropDown', function () { expect(window.CustomEvent).toHaveBeenCalledWith('click.dl', jasmine.any(Object)); }); + it('should call .classList.contains checking for IGNORE_CLASS', function () { + expect(this.classList.contains).toHaveBeenCalledWith(IGNORE_CLASS); + }); + it('should call .dispatchEvent with the customEvent', function () { expect(this.list.dispatchEvent).toHaveBeenCalledWith(this.customEvent); }); describe('if the target is a UL element', function () { beforeEach(function () { - this.event = { preventDefault: () => {}, target: { tagName: 'UL' } }; + this.event = { preventDefault: () => {}, target: { tagName: 'UL', classList: this.classList } }; + + spyOn(this.event, 'preventDefault'); + utils.closest.calls.reset(); + + DropDown.prototype.clickEvent.call(this.dropdown, this.event); + }); + + it('should return immediately', function () { + expect(utils.closest).not.toHaveBeenCalled(); + }); + }); + + describe('if the target has the IGNORE_CLASS class', function () { + beforeEach(function () { + this.event = { preventDefault: () => {}, target: { tagName: 'LI', classList: this.classList } }; spyOn(this.event, 'preventDefault'); + this.classList.contains.and.returnValue(true); utils.closest.calls.reset(); DropDown.prototype.clickEvent.call(this.dropdown, this.event); diff --git a/spec/javascripts/environments/environment_external_url_spec.js b/spec/javascripts/environments/environment_external_url_spec.js index 9af218a27ff..056d68a26e9 100644 --- a/spec/javascripts/environments/environment_external_url_spec.js +++ b/spec/javascripts/environments/environment_external_url_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import externalUrlComp from '~/environments/components/environment_external_url'; +import externalUrlComp from '~/environments/components/environment_external_url.vue'; describe('External URL Component', () => { let ExternalUrlComponent; diff --git a/spec/javascripts/environments/environment_monitoring_spec.js b/spec/javascripts/environments/environment_monitoring_spec.js index fc451cce641..0f3dba66230 100644 --- a/spec/javascripts/environments/environment_monitoring_spec.js +++ b/spec/javascripts/environments/environment_monitoring_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import monitoringComp from '~/environments/components/environment_monitoring'; +import monitoringComp from '~/environments/components/environment_monitoring.vue'; describe('Monitoring Component', () => { let MonitoringComponent; diff --git a/spec/javascripts/environments/environment_rollback_spec.js b/spec/javascripts/environments/environment_rollback_spec.js index 7cb39d9df03..25397714a76 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js +++ b/spec/javascripts/environments/environment_rollback_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import rollbackComp from '~/environments/components/environment_rollback'; +import rollbackComp from '~/environments/components/environment_rollback.vue'; describe('Rollback Component', () => { const retryURL = 'https://gitlab.com/retry'; diff --git a/spec/javascripts/environments/environment_stop_spec.js b/spec/javascripts/environments/environment_stop_spec.js index 01055e3f255..942e4aaabd4 100644 --- a/spec/javascripts/environments/environment_stop_spec.js +++ b/spec/javascripts/environments/environment_stop_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import stopComp from '~/environments/components/environment_stop'; +import stopComp from '~/environments/components/environment_stop.vue'; describe('Stop Component', () => { let StopComponent; diff --git a/spec/javascripts/environments/environment_terminal_button_spec.js b/spec/javascripts/environments/environment_terminal_button_spec.js index be2289edc2b..858472af4b6 100644 --- a/spec/javascripts/environments/environment_terminal_button_spec.js +++ b/spec/javascripts/environments/environment_terminal_button_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import terminalComp from '~/environments/components/environment_terminal_button'; +import terminalComp from '~/environments/components/environment_terminal_button.vue'; describe('Stop Component', () => { let TerminalComponent; diff --git a/spec/javascripts/filtered_search/services/recent_searches_service_spec.js b/spec/javascripts/filtered_search/services/recent_searches_service_spec.js index 2a58fb3a7df..c255bf7c939 100644 --- a/spec/javascripts/filtered_search/services/recent_searches_service_spec.js +++ b/spec/javascripts/filtered_search/services/recent_searches_service_spec.js @@ -1,3 +1,5 @@ +/* eslint-disable promise/catch-or-return */ + import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; describe('RecentSearchesService', () => { diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 56aabc16382..a00efa10119 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -1,3 +1,5 @@ +/* eslint-disable promise/catch-or-return */ + require('~/lib/utils/common_utils'); (() => { diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index d81a5bbb6a5..ca8ee04d955 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -72,5 +72,157 @@ require('~/lib/utils/text_utility'); expect(this.autoSizeSpy).toHaveBeenTriggered(); }); }); + + describe('renderNote', () => { + let notes; + let note; + let $notesList; + + beforeEach(() => { + note = { + discussion_html: null, + valid: true, + html: '<div></div>', + }; + $notesList = jasmine.createSpyObj('$notesList', ['find']); + + notes = jasmine.createSpyObj('notes', [ + 'refresh', + 'isNewNote', + 'collapseLongCommitList', + 'updateNotesCount', + ]); + notes.taskList = jasmine.createSpyObj('tasklist', ['init']); + notes.note_ids = []; + + spyOn(window, '$').and.returnValue($notesList); + spyOn(gl.utils, 'localTimeAgo'); + spyOn(Notes, 'animateAppendNote'); + notes.isNewNote.and.returnValue(true); + + Notes.prototype.renderNote.call(notes, note); + }); + + it('should query for the notes list', () => { + expect(window.$).toHaveBeenCalledWith('ul.main-notes-list'); + }); + + it('should call .animateAppendNote', () => { + expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, $notesList); + }); + }); + + describe('renderDiscussionNote', () => { + let discussionContainer; + let note; + let notes; + let $form; + let row; + + beforeEach(() => { + note = { + html: '<li></li>', + discussion_html: '<div></div>', + discussion_id: 1, + discussion_resolvable: false, + diff_discussion_html: false, + }; + $form = jasmine.createSpyObj('$form', ['closest', 'find']); + row = jasmine.createSpyObj('row', ['prevAll', 'first', 'find']); + + notes = jasmine.createSpyObj('notes', [ + 'isNewNote', + 'isParallelView', + 'updateNotesCount', + ]); + notes.note_ids = []; + + spyOn(gl.utils, 'localTimeAgo'); + spyOn(Notes, 'animateAppendNote'); + notes.isNewNote.and.returnValue(true); + notes.isParallelView.and.returnValue(false); + row.prevAll.and.returnValue(row); + row.first.and.returnValue(row); + row.find.and.returnValue(row); + }); + + describe('Discussion root note', () => { + let $notesList; + let body; + + beforeEach(() => { + body = jasmine.createSpyObj('body', ['attr']); + discussionContainer = { length: 0 }; + + spyOn(window, '$').and.returnValues(discussionContainer, body, $notesList); + $form.closest.and.returnValues(row, $form); + $form.find.and.returnValues(discussionContainer); + body.attr.and.returnValue(''); + + Notes.prototype.renderDiscussionNote.call(notes, note, $form); + }); + + it('should query for the notes list', () => { + expect(window.$.calls.argsFor(2)).toEqual(['ul.main-notes-list']); + }); + + it('should call Notes.animateAppendNote', () => { + expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.discussion_html, $notesList); + }); + }); + + describe('Discussion sub note', () => { + beforeEach(() => { + discussionContainer = { length: 1 }; + + spyOn(window, '$').and.returnValues(discussionContainer); + $form.closest.and.returnValues(row); + + Notes.prototype.renderDiscussionNote.call(notes, note, $form); + }); + + it('should query foor the discussion container', () => { + expect(window.$).toHaveBeenCalledWith(`.notes[data-discussion-id="${note.discussion_id}"]`); + }); + + it('should call Notes.animateAppendNote', () => { + expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, discussionContainer); + }); + }); + }); + + describe('animateAppendNote', () => { + let noteHTML; + let $note; + let $notesList; + + beforeEach(() => { + noteHTML = '<div></div>'; + $note = jasmine.createSpyObj('$note', ['addClass', 'renderGFM', 'removeClass']); + $notesList = jasmine.createSpyObj('$notesList', ['append']); + + spyOn(window, '$').and.returnValue($note); + spyOn(window, 'setTimeout').and.callThrough(); + $note.addClass.and.returnValue($note); + $note.renderGFM.and.returnValue($note); + + Notes.animateAppendNote(noteHTML, $notesList); + }); + + it('should init the note jquery object', () => { + expect(window.$).toHaveBeenCalledWith(noteHTML); + }); + + it('should call addClass', () => { + expect($note.addClass).toHaveBeenCalledWith('fade-in'); + }); + it('should call renderGFM', () => { + expect($note.renderGFM).toHaveBeenCalledWith(); + }); + + it('should append note to the notes list', () => { + expect($notesList.append).toHaveBeenCalledWith($note); + }); + }); }); }).call(window); diff --git a/spec/lib/banzai/filter/issuable_state_filter_spec.rb b/spec/lib/banzai/filter/issuable_state_filter_spec.rb index 0df259333e4..600f3c123ed 100644 --- a/spec/lib/banzai/filter/issuable_state_filter_spec.rb +++ b/spec/lib/banzai/filter/issuable_state_filter_spec.rb @@ -34,17 +34,41 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do expect(doc.css('a').last.text).to eq('') end - it 'adds text with standard formatting' do + it 'ignores issuable links with custom anchor' do issue = create(:issue, :closed) + link = create_link('something', issue: issue.id, reference_type: 'issue') + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq('something') + end + + it 'ignores issuable links to specific comments' do + issue = create(:issue, :closed) + link = create_link("#{issue.to_reference} (comment 1)", issue: issue.id, reference_type: 'issue') + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq("#{issue.to_reference} (comment 1)") + end + + it 'ignores merge request links to diffs tab' do + merge_request = create(:merge_request, :closed) link = create_link( - 'something <strong>else</strong>'.html_safe, - issue: issue.id, - reference_type: 'issue' + "#{merge_request.to_reference} (diffs)", + merge_request: merge_request.id, + reference_type: 'merge_request' ) doc = filter(link, context) - expect(doc.css('a').last.inner_html). - to eq('something <strong>else</strong> [closed]') + expect(doc.css('a').last.text).to eq("#{merge_request.to_reference} (diffs)") + end + + it 'handles cross project references' do + issue = create(:issue, :closed) + project = create(:empty_project) + link = create_link(issue.to_reference(project), issue: issue.id, reference_type: 'issue') + doc = filter(link, context.merge(project: project)) + + expect(doc.css('a').last.text).to eq("#{issue.to_reference(project)} (closed)") end it 'does not append state when filter is not enabled' do @@ -59,68 +83,88 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do context 'for issue references' do it 'ignores open issue references' do issue = create(:issue) - link = create_link('text', issue: issue.id, reference_type: 'issue') + link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue') doc = filter(link, context) - expect(doc.css('a').last.text).to eq('text') + expect(doc.css('a').last.text).to eq(issue.to_reference) end it 'ignores reopened issue references' do - reopened_issue = create(:issue, :reopened) - link = create_link('text', issue: reopened_issue.id, reference_type: 'issue') + issue = create(:issue, :reopened) + link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue') doc = filter(link, context) - expect(doc.css('a').last.text).to eq('text') + expect(doc.css('a').last.text).to eq(issue.to_reference) end - it 'appends [closed] to closed issue references' do - closed_issue = create(:issue, :closed) - link = create_link('text', issue: closed_issue.id, reference_type: 'issue') + it 'appends state to closed issue references' do + issue = create(:issue, :closed) + link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue') doc = filter(link, context) - expect(doc.css('a').last.text).to eq('text [closed]') + expect(doc.css('a').last.text).to eq("#{issue.to_reference} (closed)") end end context 'for merge request references' do it 'ignores open merge request references' do - mr = create(:merge_request) - link = create_link('text', merge_request: mr.id, reference_type: 'merge_request') + merge_request = create(:merge_request) + link = create_link( + merge_request.to_reference, + merge_request: merge_request.id, + reference_type: 'merge_request' + ) doc = filter(link, context) - expect(doc.css('a').last.text).to eq('text') + expect(doc.css('a').last.text).to eq(merge_request.to_reference) end it 'ignores reopened merge request references' do - mr = create(:merge_request, :reopened) - link = create_link('text', merge_request: mr.id, reference_type: 'merge_request') + merge_request = create(:merge_request, :reopened) + link = create_link( + merge_request.to_reference, + merge_request: merge_request.id, + reference_type: 'merge_request' + ) doc = filter(link, context) - expect(doc.css('a').last.text).to eq('text') + expect(doc.css('a').last.text).to eq(merge_request.to_reference) end it 'ignores locked merge request references' do - mr = create(:merge_request, :locked) - link = create_link('text', merge_request: mr.id, reference_type: 'merge_request') + merge_request = create(:merge_request, :locked) + link = create_link( + merge_request.to_reference, + merge_request: merge_request.id, + reference_type: 'merge_request' + ) doc = filter(link, context) - expect(doc.css('a').last.text).to eq('text') + expect(doc.css('a').last.text).to eq(merge_request.to_reference) end - it 'appends [closed] to closed merge request references' do - mr = create(:merge_request, :closed) - link = create_link('text', merge_request: mr.id, reference_type: 'merge_request') + it 'appends state to closed merge request references' do + merge_request = create(:merge_request, :closed) + link = create_link( + merge_request.to_reference, + merge_request: merge_request.id, + reference_type: 'merge_request' + ) doc = filter(link, context) - expect(doc.css('a').last.text).to eq('text [closed]') + expect(doc.css('a').last.text).to eq("#{merge_request.to_reference} (closed)") end - it 'appends [merged] to merged merge request references' do - mr = create(:merge_request, :merged) - link = create_link('text', merge_request: mr.id, reference_type: 'merge_request') + it 'appends state to merged merge request references' do + merge_request = create(:merge_request, :merged) + link = create_link( + merge_request.to_reference, + merge_request: merge_request.id, + reference_type: 'merge_request' + ) doc = filter(link, context) - expect(doc.css('a').last.text).to eq('text [merged]') + expect(doc.css('a').last.text).to eq("#{merge_request.to_reference} (merged)") end end end diff --git a/spec/lib/banzai/filter/plantuml_filter_spec.rb b/spec/lib/banzai/filter/plantuml_filter_spec.rb index f85a5dcbd8b..9b8ecb201f3 100644 --- a/spec/lib/banzai/filter/plantuml_filter_spec.rb +++ b/spec/lib/banzai/filter/plantuml_filter_spec.rb @@ -5,7 +5,7 @@ describe Banzai::Filter::PlantumlFilter, lib: true do it 'should replace plantuml pre tag with img tag' do stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080") - input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>' + input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>' output = '<div class="imageblock"><div class="content"><img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq"></div></div>' doc = filter(input) @@ -14,8 +14,8 @@ describe Banzai::Filter::PlantumlFilter, lib: true do it 'should not replace plantuml pre tag with img tag if disabled' do stub_application_setting(plantuml_enabled: false) - input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>' - output = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre></pre></pre>' + input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>' + output = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>' doc = filter(input) expect(doc.to_s).to eq output @@ -23,7 +23,7 @@ describe Banzai::Filter::PlantumlFilter, lib: true do it 'should not replace plantuml pre tag with img tag if url is invalid' do stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid") - input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>' + input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>' output = '<div class="listingblock"><div class="content"><pre class="plantuml plantuml-error"> PlantUML Error: cannot connect to PlantUML server at "invalid"</pre></div></div>' doc = filter(input) diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb index 994995b57b8..c166f83664a 100644 --- a/spec/lib/gitlab/diff/position_tracer_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -100,7 +100,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do project, current_user, start_branch: branch_name, - target_branch: branch_name, + branch_name: branch_name, commit_message: "Create file", file_path: file_name, file_content: content @@ -113,7 +113,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do project, current_user, start_branch: branch_name, - target_branch: branch_name, + branch_name: branch_name, commit_message: "Update file", file_path: file_name, file_content: content @@ -122,11 +122,11 @@ describe Gitlab::Diff::PositionTracer, lib: true do end def delete_file(branch_name, file_name) - Files::DestroyService.new( + Files::DeleteService.new( project, current_user, start_branch: branch_name, - target_branch: branch_name, + branch_name: branch_name, commit_message: "Delete file", file_path: file_name ).execute diff --git a/spec/lib/gitlab/git/index_spec.rb b/spec/lib/gitlab/git/index_spec.rb index 07d71f6777d..21b71654251 100644 --- a/spec/lib/gitlab/git/index_spec.rb +++ b/spec/lib/gitlab/git/index_spec.rb @@ -33,7 +33,7 @@ describe Gitlab::Git::Index, seed_helper: true do end it 'raises an error' do - expect { index.create(options) }.to raise_error('Filename already exists') + expect { index.create(options) }.to raise_error('A file with this name already exists') end end @@ -89,7 +89,7 @@ describe Gitlab::Git::Index, seed_helper: true do end it 'raises an error' do - expect { index.create_dir(options) }.to raise_error('Directory already exists as a file') + expect { index.create_dir(options) }.to raise_error('A file with this name already exists') end end @@ -99,7 +99,7 @@ describe Gitlab::Git::Index, seed_helper: true do end it 'raises an error' do - expect { index.create_dir(options) }.to raise_error('Directory already exists') + expect { index.create_dir(options) }.to raise_error('A directory with this name already exists') end end end @@ -118,7 +118,7 @@ describe Gitlab::Git::Index, seed_helper: true do end it 'raises an error' do - expect { index.update(options) }.to raise_error("File doesn't exist") + expect { index.update(options) }.to raise_error("A file with this name doesn't exist") end end @@ -156,7 +156,15 @@ describe Gitlab::Git::Index, seed_helper: true do it 'raises an error' do options[:previous_path] = 'documents/story.txt' - expect { index.move(options) }.to raise_error("File doesn't exist") + expect { index.move(options) }.to raise_error("A file with this name doesn't exist") + end + end + + context 'when a file at the new path already exists' do + it 'raises an error' do + options[:file_path] = 'CHANGELOG' + + expect { index.move(options) }.to raise_error("A file with this name already exists") end end @@ -203,7 +211,7 @@ describe Gitlab::Git::Index, seed_helper: true do end it 'raises an error' do - expect { index.delete(options) }.to raise_error("File doesn't exist") + expect { index.delete(options) }.to raise_error("A file with this name doesn't exist") end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 703b41f95ac..d8b72615fab 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -211,7 +211,7 @@ describe Gitlab::GitAccess, lib: true do target_branch = project.repository.lookup('feature') source_branch = project.repository.create_file( user, - 'John Doe', + 'filename', 'This is the file content', message: 'This is a good commit message', branch_name: unprotected_branch) diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index ba45e2d758c..127cd8c78d8 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -32,12 +32,6 @@ describe Gitlab::Regex, lib: true do it { is_expected.to match('foo@bar') } end - describe '.file_path_regex' do - subject { described_class.file_path_regex } - - it { is_expected.to match('foo@/bar') } - end - describe '.environment_slug_regex' do subject { described_class.environment_slug_regex } diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index d057c9cf6e9..11befd4edfe 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -361,7 +361,10 @@ describe Issue, models: true do it 'updates when assignees change' do user1 = create(:user) user2 = create(:user) - issue = create(:issue, assignee: user1) + project = create(:empty_project) + issue = create(:issue, assignee: user1, project: project) + project.add_developer(user1) + project.add_developer(user2) expect(user1.assigned_open_issues_count).to eq(1) expect(user2.assigned_open_issues_count).to eq(0) diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 90b3a2ba42d..415d3e7b200 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -820,15 +820,17 @@ describe MergeRequest, models: true do user1 = create(:user) user2 = create(:user) mr = create(:merge_request, assignee: user1) + mr.project.add_developer(user1) + mr.project.add_developer(user2) - expect(user1.assigned_open_merge_request_count).to eq(1) - expect(user2.assigned_open_merge_request_count).to eq(0) + expect(user1.assigned_open_merge_requests_count).to eq(1) + expect(user2.assigned_open_merge_requests_count).to eq(0) mr.assignee = user2 mr.save - expect(user1.assigned_open_merge_request_count).to eq(0) - expect(user2.assigned_open_merge_request_count).to eq(1) + expect(user1.assigned_open_merge_requests_count).to eq(0) + expect(user2.assigned_open_merge_requests_count).to eq(1) end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6af5ef1018c..0a2860f2505 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -24,9 +24,7 @@ describe User, models: true do it { is_expected.to have_many(:recent_events).class_name('Event') } it { is_expected.to have_many(:issues).dependent(:restrict_with_exception) } it { is_expected.to have_many(:notes).dependent(:destroy) } - it { is_expected.to have_many(:assigned_issues).dependent(:nullify) } it { is_expected.to have_many(:merge_requests).dependent(:destroy) } - it { is_expected.to have_many(:assigned_merge_requests).dependent(:nullify) } it { is_expected.to have_many(:identities).dependent(:destroy) } it { is_expected.to have_many(:spam_logs).dependent(:destroy) } it { is_expected.to have_many(:todos).dependent(:destroy) } diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index a10d876ffad..42dbab586cd 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -599,8 +599,7 @@ describe API::Commits, api: true do post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'markdown' expect(response).to have_http_status(400) - expect(json_response['message']).to eq('Sorry, we cannot cherry-pick this commit automatically. - A cherry-pick may have already been performed with this commit, or a more recent commit may have updated some of its content.') + expect(json_response['message']).to include('Sorry, we cannot cherry-pick this commit automatically.') end it 'returns 400 if you are not allowed to push to the target branch' do diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index 8012530f139..6db2faed76b 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -205,7 +205,7 @@ describe API::Files, api: true do it "returns a 400 if editor fails to create file" do allow_any_instance_of(Repository).to receive(:create_file). - and_return(false) + and_raise(Repository::CommitError, 'Cannot create file') post api(route("any%2Etxt"), user), valid_params @@ -299,8 +299,8 @@ describe API::Files, api: true do expect(response).to have_http_status(400) end - it "returns a 400 if fails to create file" do - allow_any_instance_of(Repository).to receive(:delete_file).and_return(false) + it "returns a 400 if fails to delete file" do + allow_any_instance_of(Repository).to receive(:delete_file).and_raise(Repository::CommitError, 'Cannot delete file') delete api(route(file_path), user), valid_params diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index ea9b886e995..165ab389917 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -72,6 +72,12 @@ describe API::Users, api: true do expect(json_response).to be_an Array expect(json_response.first['username']).to eq(omniauth_user.username) end + + it "returns a 403 when non-admin user searches by external UID" do + get api("/users?extern_uid=#{omniauth_user.identities.first.extern_uid}&provider=#{omniauth_user.identities.first.provider}", user) + + expect(response).to have_http_status(403) + end end context "when admin" do @@ -100,6 +106,27 @@ describe API::Users, api: true do expect(json_response).to be_an Array expect(json_response).to all(include('external' => true)) end + + it "returns one user by external UID" do + get api("/users?extern_uid=#{omniauth_user.identities.first.extern_uid}&provider=#{omniauth_user.identities.first.provider}", admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.first['username']).to eq(omniauth_user.username) + end + + it "returns 400 error if provider with no extern_uid" do + get api("/users?extern_uid=#{omniauth_user.identities.first.extern_uid}", admin) + + expect(response).to have_http_status(400) + end + + it "returns 400 error if provider with no extern_uid" do + get api("/users?provider=#{omniauth_user.identities.first.provider}", admin) + + expect(response).to have_http_status(400) + end end end diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb index adba3a787aa..0a28cb9bddb 100644 --- a/spec/requests/api/v3/commits_spec.rb +++ b/spec/requests/api/v3/commits_spec.rb @@ -485,8 +485,7 @@ describe API::V3::Commits, api: true do post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'markdown' expect(response).to have_http_status(400) - expect(json_response['message']).to eq('Sorry, we cannot cherry-pick this commit automatically. - A cherry-pick may have already been performed with this commit, or a more recent commit may have updated some of its content.') + expect(json_response['message']).to include('Sorry, we cannot cherry-pick this commit automatically.') end it 'returns 400 if you are not allowed to push to the target branch' do diff --git a/spec/requests/api/v3/files_spec.rb b/spec/requests/api/v3/files_spec.rb index 349fd6b3415..c45e2028e1d 100644 --- a/spec/requests/api/v3/files_spec.rb +++ b/spec/requests/api/v3/files_spec.rb @@ -129,7 +129,7 @@ describe API::V3::Files, api: true do it "returns a 400 if editor fails to create file" do allow_any_instance_of(Repository).to receive(:create_file). - and_return(false) + and_raise(Repository::CommitError, 'Cannot create file') post v3_api("/projects/#{project.id}/repository/files", user), valid_params @@ -229,8 +229,8 @@ describe API::V3::Files, api: true do expect(response).to have_http_status(400) end - it "returns a 400 if fails to create file" do - allow_any_instance_of(Repository).to receive(:delete_file).and_return(false) + it "returns a 400 if fails to delete file" do + allow_any_instance_of(Repository).to receive(:delete_file).and_raise(Repository::CommitError, 'Cannot delete file') delete v3_api("/projects/#{project.id}/repository/files", user), valid_params diff --git a/spec/services/delete_merged_branches_service_spec.rb b/spec/services/delete_merged_branches_service_spec.rb index a41a421fa6e..7b921f606f8 100644 --- a/spec/services/delete_merged_branches_service_spec.rb +++ b/spec/services/delete_merged_branches_service_spec.rb @@ -42,6 +42,19 @@ describe DeleteMergedBranchesService, services: true do expect { described_class.new(project, user).execute }.to raise_error(Gitlab::Access::AccessDeniedError) end end + + context 'open merge requests' do + it 'does not delete branches from open merge requests' do + fork_link = create(:forked_project_link, forked_from_project: project) + create(:merge_request, :reopened, source_project: project, target_project: project, source_branch: 'branch-merged', target_branch: 'master') + create(:merge_request, :opened, source_project: fork_link.forked_to_project, target_project: project, target_branch: 'improve/awesome', source_branch: 'master') + + service.execute + + expect(project.repository.branch_names).to include('branch-merged') + expect(project.repository.branch_names).to include('improve/awesome') + end + end end context '#async_execute' do diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb index 26aa5b432d4..16bca66766a 100644 --- a/spec/services/files/update_service_spec.rb +++ b/spec/services/files/update_service_spec.rb @@ -7,7 +7,7 @@ describe Files::UpdateService do let(:user) { create(:user) } let(:file_path) { 'files/ruby/popen.rb' } let(:new_contents) { 'New Content' } - let(:target_branch) { project.default_branch } + let(:branch_name) { project.default_branch } let(:last_commit_sha) { nil } let(:commit_params) do @@ -19,7 +19,7 @@ describe Files::UpdateService do last_commit_sha: last_commit_sha, start_project: project, start_branch: project.default_branch, - target_branch: target_branch + branch_name: branch_name } end @@ -73,7 +73,7 @@ describe Files::UpdateService do end context 'when target branch is different than source branch' do - let(:target_branch) { "#{project.default_branch}-new" } + let(:branch_name) { "#{project.default_branch}-new" } it 'fires hooks only once' do expect(GitHooksService).to receive(:new).once.and_call_original diff --git a/spec/services/members/authorized_destroy_service_spec.rb b/spec/services/members/authorized_destroy_service_spec.rb new file mode 100644 index 00000000000..3b35a3b8e3a --- /dev/null +++ b/spec/services/members/authorized_destroy_service_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe Members::AuthorizedDestroyService, services: true do + let(:member_user) { create(:user) } + let(:project) { create(:empty_project, :public) } + let(:group) { create(:group, :public) } + let(:group_project) { create(:empty_project, :public, group: group) } + + def number_of_assigned_issuables(user) + Issue.assigned_to(user).count + MergeRequest.assigned_to(user).count + end + + context 'Group member' do + it "unassigns issues and merge requests" do + group.add_developer(member_user) + + issue = create :issue, project: group_project, assignee: member_user + create :issue, assignee: member_user + merge_request = create :merge_request, target_project: group_project, source_project: group_project, assignee: member_user + create :merge_request, target_project: project, source_project: project, assignee: member_user + + member = group.members.find_by(user_id: member_user.id) + + expect { described_class.new(member, member_user).execute } + .to change { number_of_assigned_issuables(member_user) }.from(4).to(2) + + expect(issue.reload.assignee_id).to be_nil + expect(merge_request.reload.assignee_id).to be_nil + end + end + + context 'Project member' do + it "unassigns issues and merge requests" do + project.team << [member_user, :developer] + + create :issue, project: project, assignee: member_user + create :merge_request, target_project: project, source_project: project, assignee: member_user + + member = project.members.find_by(user_id: member_user.id) + + expect { described_class.new(member, member_user).execute } + .to change { number_of_assigned_issuables(member_user) }.from(2).to(0) + end + end +end diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb index 1a061ef069e..bb4542b1683 100644 --- a/spec/support/features/discussion_comments_shared_example.rb +++ b/spec/support/features/discussion_comments_shared_example.rb @@ -73,9 +73,15 @@ shared_examples 'discussion comments' do |resource_name| expect(page).not_to have_selector menu_selector end - it 'clicking the ul padding should not change the text' do + it 'clicking the ul padding or divider should not change the text' do find(menu_selector).trigger 'click' + expect(page).to have_selector menu_selector + expect(find(dropdown_selector)).to have_content 'Comment' + + find("#{menu_selector} .divider").trigger 'click' + + expect(page).to have_selector menu_selector expect(find(dropdown_selector)).to have_content 'Comment' end diff --git a/spec/support/mobile_helpers.rb b/spec/support/mobile_helpers.rb index 20d5849bcab..431f20a2a5c 100644 --- a/spec/support/mobile_helpers.rb +++ b/spec/support/mobile_helpers.rb @@ -1,4 +1,8 @@ module MobileHelpers + def resize_screen_xs + resize_window(767, 768) + end + def resize_screen_sm resize_window(900, 768) end diff --git a/yarn.lock b/yarn.lock index e16cd9c3673..90ba39a3251 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1988,6 +1988,10 @@ eslint-plugin-jasmine@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/eslint-plugin-jasmine/-/eslint-plugin-jasmine-2.2.0.tgz#7135879383c39a667c721d302b9f20f0389543de" +eslint-plugin-promise@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-3.5.0.tgz#78fbb6ffe047201627569e85a6c5373af2a68fca" + eslint@^3.10.1: version "3.15.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.15.0.tgz#bdcc6a6c5ffe08160e7b93c066695362a91e30f2" |